Skip to content

Commit

Permalink
Ajoute un formulaire pour modifier les catégories d'une publication (#…
Browse files Browse the repository at this point in the history
…6603)

* Supprime la gestion des catégories du formulaire "Éditer"
* Ajoute un formulaire pour modifier les catégories d'une publication
* Ajoute un lien vers la modification des catégories
  • Loading branch information
Arnaud-D committed Apr 26, 2024
1 parent 7b70da3 commit f98b5b7
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 35 deletions.
26 changes: 26 additions & 0 deletions templates/tutorialv2/edit/categories.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{% extends "tutorialv2/base.html" %}
{% load crispy_forms_tags %}
{% load i18n %}

{% 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 %}
25 changes: 22 additions & 3 deletions templates/tutorialv2/includes/headline/categories.part.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{% load i18n %}
{% load pluralize_fr %}
{% 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 +13,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

0 comments on commit f98b5b7

Please sign in to comment.