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

Ajoute un formulaire pour modifier les catégories d'une publication #6603

Merged
merged 10 commits into from
Apr 26, 2024
28 changes: 28 additions & 0 deletions templates/tutorialv2/edit/categories.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{% extends "tutorialv2/base.html" %}
{% load crispy_forms_tags %}
{% load thumbnail %}
Arnaud-D marked this conversation as resolved.
Show resolved Hide resolved
{% load i18n %}
{% load feminize %}
Arnaud-D marked this conversation as resolved.
Show resolved Hide resolved

{% block title %}
{% trans "Modifier les catégories de " %}{{ content.title }}
{% endblock %}

{% block breadcrumb %}
<li><a href="{{ content.get_absolute_url }}">{{ content.title }}</a></li>
<li>{% trans "Modifier les catégories" %}</li>
{% endblock %}

{% block headline %}
<h1 {% if content.image %}class="illu"{% endif %}>
{% if content.image %}
<img src="{{content.image.physical.tutorial_illu.url }}" alt="">
{% endif %}
{% blocktrans with title=content.title %}Modifier les catégories de « {{ title }} »{% endblocktrans %}
</h1>
{% endblock %}


{% block content %}
{% crispy form %}
{% endblock %}
24 changes: 22 additions & 2 deletions templates/tutorialv2/includes/headline/categories.part.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{% load i18n %}
{% load pluralize_fr %}
Arnaud-D marked this conversation as resolved.
Show resolved Hide resolved
{% load captureas %}

{% captureas categories_list %}
<p>
{% trans "Catégorie" %}{{ content.subcategory.all|pluralize }} :

{% trans "Dans" %}
{% for category in content.subcategory.all %}
{% if forloop.first %}{% elif forloop.last %} {% trans "et" %}{% else %},{% endif %}
{% if content.is_opinion %}
Expand All @@ -13,3 +14,22 @@
{% endif %}
{% endfor %}
</p>
{% endcaptureas %}

{% url "content:edit-categories" content.pk as edit_url %}

{% if show_form %}
{% if content.subcategory.all %}
<div class="editable-element">
{{ categories_list }}

{% if show_form %}
<a href="{{ edit_url }}" class="edit-button"><span class="visuallyhidden">{% trans "Modifier" %}</span></a>
{% endif %}
</div>
{% else %}
<a href="{{ edit_url }}">{% trans "Choisissez les catégories !" %}</span></a>
{% endif %}
{% elif content.subcategory.all %}
{{ categories_list }}
{% endif %}
2 changes: 1 addition & 1 deletion templates/tutorialv2/includes/headline/header.part.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
{% include "tutorialv2/includes/headline/authors.part.html" with db_content=db_content edit_authors=display_config.draft_actions.show_authors_management online_mode=display_config.online_config.enable_authors_online_mode %}
{% include "tutorialv2/includes/headline/licence.part.html" with licence=content.licence show_form=display_config.draft_actions.show_license_edit form=form_edit_license %}
{% include "tutorialv2/includes/headline/contributions.part.html" %}
{% include "tutorialv2/includes/headline/categories.part.html" %}
{% include "tutorialv2/includes/headline/categories.part.html" with content=content show_form=display_config.draft_actions.show_categories_management %}
{% include "tutorialv2/includes/headline/goals.part.html" with goals=publishablecontent.goals.all %}
{% include "tutorialv2/includes/headline/labels.part.html" with labels=publishablecontent.labels.all %}

Expand Down
16 changes: 4 additions & 12 deletions zds/tutorialv2/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,13 +124,6 @@ class ContentForm(ContainerForm):

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

subcategory = forms.ModelMultipleChoiceField(
label=_("Sélectionnez les catégories qui correspondent à votre contenu."),
queryset=SubCategory.objects.order_by("title").all(),
required=False,
widget=forms.CheckboxSelectMultiple(),
)

source = forms.URLField(
label=_(
"""Si votre contenu est publié en dehors de Zeste de Savoir (blog, site personnel, etc.),
Expand Down Expand Up @@ -166,7 +159,6 @@ def _create_layout(self):
),
Field("last_hash"),
Field("source"),
Field("subcategory", template="crispy/checkboxselectmultiple.html"),
)

self.helper.layout.append(Field("msg_commit"))
Expand Down Expand Up @@ -517,9 +509,9 @@ def __init__(self, content, *args, **kwargs):
no_category_msg = HTML(
_(
"""<p><strong>Votre publication n'est dans aucune catégorie.
Vous devez <a href="{}#{}">choisir une catégorie</a>
Vous devez <a href="{}">choisir une catégorie</a>
avant de demander la validation.</strong></p>""".format(
reverse("content:edit", kwargs={"pk": content.pk, "slug": content.slug}), "div_id_subcategory"
reverse("content:edit-categories", kwargs={"pk": content.pk}),
)
)
)
Expand Down Expand Up @@ -892,9 +884,9 @@ def __init__(self, content, *args, **kwargs):
no_category_msg = HTML(
_(
"""<p><strong>Votre publication n'est dans aucune catégorie.
Vous devez <a href="{}#{}">choisir une catégorie</a>
Vous devez <a href="{}">choisir une catégorie</a>
avant de publier.</strong></p>""".format(
reverse("content:edit", kwargs={"pk": content.pk, "slug": content.slug}), "div_id_subcategory"
reverse("content:edit-categories", kwargs={"pk": content.pk})
)
)
)
Expand Down
4 changes: 1 addition & 3 deletions zds/tutorialv2/tests/tests_front.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,7 @@ def test_the_editor_forgets_its_content_on_form_submission(self):
"content:create-content", kwargs={"created_content_type": "ARTICLE"}
)
selenium.get(new_article_url)
WebDriverWait(self.selenium, 10).until(
ec.element_to_be_clickable((By.CSS_SELECTOR, "input[type=checkbox][name=subcategory]"))
).click()
WebDriverWait(self.selenium, 10).until(ec.element_to_be_clickable((By.CSS_SELECTOR, "#id_title"))).click()

self.find_element("#id_title").send_keys("Oulipo")

Expand Down
133 changes: 133 additions & 0 deletions zds/tutorialv2/tests/tests_views/tests_editcategoriesview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
from django.test import TestCase
from django.urls import reverse
from django.utils.html import escape

from zds.tutorialv2.publication_utils import publish_content
from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents
from zds.member.tests.factories import ProfileFactory, StaffProfileFactory
from zds.tutorialv2.tests.factories import PublishableContentFactory
from zds.tutorialv2.views.categories import EditCategoriesForm
from zds.utils.tests.factories import SubCategoryFactory


def publish(content):
"""Emulate the publication of a content."""
published = publish_content(content, content.load_version())
content.public_version = published
content.save()


@override_for_contents()
class PermissionTests(TutorialTestMixin, TestCase):
"""Test permissions and associated behaviors, such as redirections and status codes."""

def setUp(self):
self.author = ProfileFactory().user
self.category = SubCategoryFactory()
content = PublishableContentFactory(author_list=[self.author])

self.target_url = reverse("content:edit-categories", kwargs={"pk": content.pk})
self.form_data = {"subcategory": self.category.pk}
self.login_url = reverse("member-login") + "?next=" + self.target_url
self.content_url = reverse("content:view", kwargs={"pk": content.pk, "slug": content.slug})

def get(self):
return self.client.get(self.target_url)

def post(self):
return self.client.post(self.target_url, self.form_data)

def test_not_authenticated(self):
"""Test that unauthenticated users are redirected to the login page."""
self.client.logout() # ensure no user is authenticated

with self.subTest(msg="GET"):
response = self.get()
self.assertRedirects(response, self.login_url)

with self.subTest(msg="POST"):
response = self.post()
self.assertRedirects(response, self.login_url)

def test_authenticated_author(self):
"""Test that authors can reach the page."""
self.client.force_login(self.author)

with self.subTest(msg="GET"):
response = self.get()
self.assertEqual(response.status_code, 200)

with self.subTest(msg="POST"):
response = self.post()
self.assertRedirects(response, self.content_url)

def test_authenticated_staff(self):
"""Test that staffs can reach the page."""
staff = StaffProfileFactory().user
self.client.force_login(staff)

with self.subTest(msg="GET"):
response = self.get()
self.assertEqual(response.status_code, 200)

with self.subTest(msg="POST"):
response = self.post()
self.assertRedirects(response, self.content_url)

def test_authenticated_outsider(self):
"""Test that unauthorized users get a 403."""
outsider = ProfileFactory().user
self.client.force_login(outsider)

with self.subTest(msg="GET"):
response = self.get()
self.assertEqual(response.status_code, 403)

with self.subTest(msg="POST"):
response = self.get()
self.assertEqual(response.status_code, 403)


@override_for_contents()
class FunctionalTests(TutorialTestMixin, TestCase):
"""Test the behavior of the feature."""

def setUp(self):
self.author = StaffProfileFactory().user
self.content = PublishableContentFactory(author_list=[self.author], add_category=False)

self.category_0 = SubCategoryFactory()
self.category_1 = SubCategoryFactory()

self.url = reverse("content:edit-categories", kwargs={"pk": self.content.pk})

self.client.force_login(self.author)

def test_add_category(self):
form_data = {"subcategory": [str(self.category_0.pk)]}
self.client.post(self.url, form_data)

categories_real = self.content.subcategory.all()
categories_expected = [self.category_0]
self.assertQuerysetEqual(categories_real, categories_expected)

def test_remove_category(self):
self.content.subcategory.add(self.category_0)
self.assertQuerysetEqual(self.content.subcategory.all(), [self.category_0])

form_data = {"subcategory": []}
self.client.post(self.url, form_data)

categories_real = self.content.subcategory.all()
categories_expected = []
self.assertQuerysetEqual(categories_real, categories_expected)

def test_remove_published(self):
self.content.subcategory.add(self.category_0)
self.assertQuerysetEqual(self.content.subcategory.all(), [self.category_0])
publish(self.content)

form_data = {"subcategory": []}
response = self.client.post(self.url, form_data, follow=True)
self.assertContains(response, escape(EditCategoriesForm.error_messages["no_category_but_public"]))
self.assertQuerysetEqual(self.content.subcategory.all(), [self.category_0])
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.categories import EditCategoriesView
from zds.tutorialv2.views.contents import (
CreateContent,
EditContent,
Expand Down Expand Up @@ -220,6 +221,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 categories
path("modifier-categories/<int:pk>/", EditCategoriesView.as_view(), name="edit-categories"),
# beta:
path("activer-beta/<int:pk>/<slug:slug>/", ManageBetaContent.as_view(action="set"), name="set-beta"),
path(
Expand Down
74 changes: 74 additions & 0 deletions zds/tutorialv2/views/categories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from crispy_forms.bootstrap import StrictButton
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Field

from django import forms
from django.core.exceptions import ValidationError
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.views.generic import FormView

from zds.member.decorator import LoggedWithReadWriteHability
from zds.tutorialv2.mixins import SingleContentFormViewMixin
from zds.tutorialv2.models.database import PublishableContent
from zds.utils.models import SubCategory


class EditCategoriesForm(forms.Form):
subcategory = forms.ModelMultipleChoiceField(
label=_("Sélectionnez les catégories qui correspondent à la publication."),
queryset=SubCategory.objects.order_by("title").all(),
required=False,
widget=forms.CheckboxSelectMultiple(),
)

error_messages = {
"no_category_but_public": _("Vous devez choisir au moins une catégorie, car ce contenu est déjà publié.")
}

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

self.content = content

self.helper = FormHelper()
self.helper.form_class = "content-wrapper"
self.helper.form_method = "post"
self.helper.layout = Layout(
Field("subcategory", template="crispy/checkboxselectmultiple.html"),
StrictButton(_("Valider"), type="submit"),
)

def clean_subcategory(self):
subcategory = self.cleaned_data["subcategory"]
# Forbid removing all categories of a validated content
if self.content.in_public() and not subcategory:
raise ValidationError(message=self.error_messages["no_category_but_public"])
return subcategory


class EditCategoriesView(LoggedWithReadWriteHability, SingleContentFormViewMixin, FormView):
template_name = "tutorialv2/edit/categories.html"
model = PublishableContent
form_class = EditCategoriesForm

def get_initial(self):
initial = super().get_initial()
initial["subcategory"] = self.object.subcategory.all()
return initial

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

def form_valid(self, form):
content = self.object

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

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

return super().form_valid(form)
16 changes: 0 additions & 16 deletions zds/tutorialv2/views/contents.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,6 @@ def form_valid(self, form):

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(
Expand Down Expand Up @@ -165,13 +160,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)

# 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)
publishable.source = form.cleaned_data["source"]

Expand Down Expand Up @@ -209,10 +197,6 @@ def form_valid(self, form):
# 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])
Expand Down
Loading