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

Premier jalon pour la classification par objectif des contenus #6354

Merged
merged 6 commits into from
Jul 19, 2022
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
28 changes: 28 additions & 0 deletions fixtures/goals.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
- model: tutorialv2.Goal
pk: 1
fields:
name: Comprendre
description: Publications pour comprendre quelque chose à quelque chose d'autre.
position: 0
slug: comprendre
- model: tutorialv2.Goal
pk: 2
fields:
name: Apprendre
description: Qui n'apprend rien n'a rien.
position: 1
slug: apprendre
- model: tutorialv2.Goal
pk: 3
fields:
name: Exprimer une opinion
description: Ayez un avis sur tout.
position: 2
slug: exprimer-opinion
- model: tutorialv2.Goal
pk: 4
fields:
name: Nouvelles de la communauté
description: Nous sommes des communards, après tout.
position: 3
slug: nouvelles-communaute
4 changes: 4 additions & 0 deletions templates/tutorialv2/events/descriptions.part.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@
<a href="{{ performer_href }}">{{ event.performer }}</a> a modifié les tags du contenu.


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


{% elif event.type == "suggestions_management" %}
{% if event.action == "add" %}
<a href="{{ performer_href }}">{{ event.performer }}</a> a ajouté une suggestion de contenu.
Expand Down
11 changes: 10 additions & 1 deletion templates/tutorialv2/includes/editorialization.part.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,20 @@ <h3>Éditorialisation</h3>
<ul>
<li>
<a href="#edit-tags" class="open-modal ico-after gear blue">
Modifier les tags
{% trans "Modifier les tags" %}
</a>
{% crispy form_edit_tags %}
</li>

{% if is_staff %}
<li>
<a href="#edit-goals" class="open-modal ico-after gear blue">
{% trans "Modifier les objectifs" %}
</a>
{% crispy form_edit_goals %}
</li>
{% endif %}

{% if is_staff and not content.is_opinion %}
<li>
<a href="#add-suggestion" class="open-modal ico-after more blue">
Expand Down
7 changes: 7 additions & 0 deletions zds/tutorialv2/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
ContentContributionRole,
)
from zds.tutorialv2.models.events import Event
from zds.tutorialv2.models.goals import Goal
from zds.tutorialv2.models.help_requests import HelpWriting


Expand Down Expand Up @@ -112,6 +113,11 @@ class ContentReviewTypeAdmin(admin.ModelAdmin):
ordering = ["position"]


class GoalAdmin(admin.ModelAdmin):
list_display = ["name", "description"]
ordering = ["position"]


admin.site.register(PublishableContent, PublishableContentAdmin)
admin.site.register(PublishedContent, PublishedContentAdmin)
admin.site.register(Validation, ValidationAdmin)
Expand All @@ -122,3 +128,4 @@ class ContentReviewTypeAdmin(admin.ModelAdmin):
admin.site.register(ContentContributionRole, ContentReviewTypeAdmin)
admin.site.register(HelpWriting)
admin.site.register(Event)
admin.site.register(Goal, GoalAdmin)
34 changes: 34 additions & 0 deletions zds/tutorialv2/migrations/0034_goals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 3.2.13 on 2022-07-15 09:27

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("tutorialv2", "0033_move_helpwriting"),
]

operations = [
migrations.CreateModel(
name="Goal",
fields=[
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("name", models.CharField(max_length=80, verbose_name="Nom")),
("description", models.TextField(blank=True, verbose_name="Description")),
("position", models.IntegerField(db_index=True, default=0, verbose_name="Position")),
("slug", models.SlugField(max_length=80, unique=True)),
],
options={
"verbose_name": "Objectif",
"verbose_name_plural": "Objectifs",
},
),
migrations.AddField(
model_name="publishablecontent",
name="goals",
field=models.ManyToManyField(
blank=True, db_index=True, to="tutorialv2.Goal", verbose_name="Objectifs du contenu"
),
),
]
4 changes: 3 additions & 1 deletion zds/tutorialv2/models/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
)
from zds.tutorialv2.managers import PublishedContentManager, PublishableContentManager, ReactionManager
from zds.tutorialv2.models import TYPE_CHOICES, STATUS_CHOICES, CONTENT_TYPES_REQUIRING_VALIDATION, PICK_OPERATIONS
from zds.tutorialv2.models.goals import Goal
from zds.tutorialv2.models.mixins import TemplatableContentModelMixin, OnlineLinkableContentMixin
from zds.tutorialv2.models.versioned import NotAPublicVersion
from zds.tutorialv2.utils import get_content_from_json, BadManifestError, get_blob
Expand Down Expand Up @@ -71,8 +72,9 @@ class Meta:
authors = models.ManyToManyField(User, verbose_name="Auteurs", db_index=True)
old_pk = models.IntegerField(db_index=True, default=0)
subcategory = models.ManyToManyField(SubCategory, verbose_name="Sous-Catégorie", blank=True, db_index=True)

tags = models.ManyToManyField(Tag, verbose_name="Tags du contenu", blank=True, db_index=True)
goals = models.ManyToManyField(Goal, verbose_name="Objectifs du contenu", blank=True, db_index=True)

# store the thumbnail for tutorial or article
image = models.ForeignKey(Image, verbose_name="Image du tutoriel", blank=True, null=True, on_delete=models.SET_NULL)

Expand Down
12 changes: 12 additions & 0 deletions zds/tutorialv2/models/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from zds.tutorialv2.views.beta import ManageBetaContent
from zds.tutorialv2.views.contributors import AddContributorToContent, RemoveContributorFromContent
from zds.tutorialv2.views.editorialization import EditContentTags, AddSuggestion, RemoveSuggestion
from zds.tutorialv2.views.goals import EditGoals
from zds.tutorialv2.views.help import ChangeHelp
from zds.tutorialv2.views.validations_contents import (
ReserveValidation,
Expand All @@ -25,6 +26,7 @@
# * Addition
# 1. Add a key in `types`.
# 2. Modify the template "events/description.part.html" so that it is displayed properly.
# 3. Add the appropriate receiver.
#
# * Deletion
# 1. Remove the key in `types` and the corresponding `@receiver`.
Expand All @@ -44,6 +46,7 @@
signals.beta_management: "beta_management",
signals.validation_management: "validation_management",
signals.tags_management: "tags_management",
signals.goals_management: "goals_management",
signals.suggestions_management: "suggestions_management",
signals.help_management: "help_management",
signals.jsfiddle_management: "jsfiddle_management",
Expand Down Expand Up @@ -147,6 +150,15 @@ def record_event_suggestion_management(sender, performer, signal, content, actio
).save()


@receiver(signals.goals_management, sender=EditGoals)
def record_event_goals_management(sender, performer, signal, content, **_):
Event(
performer=performer,
type=types[signal],
content=content,
).save()


Arnaud-D marked this conversation as resolved.
Show resolved Hide resolved
@receiver(signals.help_management, sender=ChangeHelp)
def record_event_help_management(sender, performer, signal, content, **_):
Event(
Expand Down
22 changes: 22 additions & 0 deletions zds/tutorialv2/models/goals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django.db import models


class Goal(models.Model):
"""
This model represents the categories used for the goal-based classification of publications.
The goals are categories like "understand", "discover", "learn", "give an opinion", etc.
They are thus distinct from the thematic categories and subcategories (physics,
computer science, etc.) or the tags (even more precise).
"""

class Meta:
verbose_name = "Objectif"
verbose_name_plural = "Objectifs"

name = models.CharField("Nom", max_length=80)
description = models.TextField("Description", blank=True)
position = models.IntegerField("Position", default=0, db_index=True)
slug = models.SlugField(max_length=80, unique=True)

def __str__(self):
return self.name
4 changes: 4 additions & 0 deletions zds/tutorialv2/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
# Action is either "add" or "remove".
suggestions_management = Signal()

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

# Help management
# For the signal below, the arguments "performer" and "content" shall be provided.
help_management = Signal()
Expand Down
13 changes: 13 additions & 0 deletions zds/tutorialv2/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from zds.forum.tests.factories import PostFactory, TopicFactory
from zds.gallery.tests.factories import GalleryFactory, UserGalleryFactory
from zds.tutorialv2.models.goals import Goal
from zds.tutorialv2.models.help_requests import HelpWriting
from zds.utils import old_slugify
from zds.utils.tests.factories import LicenceFactory, SubCategoryFactory
Expand Down Expand Up @@ -299,3 +300,15 @@ def _create(cls, target_class, *args, **kwargs):
kwargs.pop("fixture_image_path", None)

return super()._create(target_class, *args, **kwargs)


class GoalFactory(factory.django.DjangoModelFactory):
"""Factory that create a goal for use in tests."""

class Meta:
model = Goal

name = factory.Sequence("Mon objectif n°{}".format)
description = factory.Sequence("Très belle description n°{}".format)
position = factory.Sequence(lambda n: n)
slug = factory.Sequence("mon-objectif-{}".format)
91 changes: 91 additions & 0 deletions zds/tutorialv2/tests/tests_views/tests_editgoals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from unittest.mock import patch

from django.test import TestCase
from django.urls import reverse

from zds.member.tests.factories import ProfileFactory, StaffProfileFactory
from zds.tutorialv2.tests.factories import PublishableContentFactory, GoalFactory


class EditGoalsPermissionTests(TestCase):
def setUp(self):
self.user = ProfileFactory().user
self.author = ProfileFactory().user
self.staff = StaffProfileFactory().user
self.content = PublishableContentFactory()
self.content.authors.add(self.author)
self.good_url = reverse("content:edit-goals", kwargs={"pk": self.content.pk})
self.bad_url = reverse("content:edit-goals", kwargs={"pk": 42})
self.content_url = reverse("content:view", kwargs={"pk": self.content.pk, "slug": self.content.slug})
self.success_url = self.content_url

def test_display(self):
"""We shall display the form only for staff, not for authors."""
fragment = "Modifier les objectifs"

self.client.force_login(self.author)
response = self.client.get(self.content_url)
self.assertNotContains(response, fragment)

self.client.force_login(self.staff)
response = self.client.get(self.content_url)
self.assertContains(response, fragment)

def test_get_method(self):
"""
GET is forbidden, since the view processes the form but do not display anything.
Actually, all methods except POST are forbidden, but the test is good enough as is.
"""
self.client.force_login(self.staff)
response = self.client.get(self.good_url)
self.assertEqual(response.status_code, 405)

def test_unauthenticated_not_existing_pk(self):
"""Invalid pks in URL"""
self.client.logout()
response = self.client.post(self.bad_url)
self.assertEqual(response.status_code, 404)

def test_unauthenticated_redirected(self):
"""As login is required, unauthenticated users shall be redirected to the login page."""
self.client.logout()
response = self.client.post(self.good_url)
self.login_url = f"{reverse('member-login')}?next={self.good_url}"
self.assertRedirects(response, self.login_url)

def test_simple_user_forbidden(self):
"""Simple users shall not be able to access to the view."""
self.client.force_login(self.user)
response = self.client.post(self.good_url)
self.assertEqual(response.status_code, 403)

def test_staff_authorized(self):
"""Staff shall have access to the view."""
self.client.force_login(self.staff)
response = self.client.post(self.good_url)
self.assertRedirects(response, self.success_url)


class EditGoalsFunctionalTests(TestCase):
def setUp(self):
self.staff = StaffProfileFactory().user
self.content = PublishableContentFactory()
self.content = PublishableContentFactory()
self.url = reverse("content:edit-goals", kwargs={"pk": self.content.pk})
self.goals = [GoalFactory() for _ in range(3)]

@patch("zds.tutorialv2.signals.goals_management")
def test_goals_updated(self, goals_management):
self.client.force_login(self.staff)
response = self.client.post(self.url, {"goals": [goal.pk for goal in self.goals]}, follow=True)
self.assertEqual(list(self.content.goals.all()), self.goals)
self.assertContains(response, "alert-box success")
self.assertEqual(goals_management.send.call_count, 1)

@patch("zds.tutorialv2.signals.goals_management")
def test_invalid_parameters(self, goals_management):
self.client.force_login(self.staff)
response = self.client.post(self.url, {"goals": [42]}, follow=True)
self.assertEqual(list(self.content.goals.all()), [])
self.assertContains(response, "alert-box alert")
self.assertFalse(goals_management.send.called)
3 changes: 3 additions & 0 deletions zds/tutorialv2/urls/urls_contents.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from zds.tutorialv2.views.contents import DisplayContent, CreateContent, EditContent, EditContentLicense, DeleteContent
from zds.tutorialv2.views.events import EventsList
from zds.tutorialv2.views.goals import EditGoals
from zds.tutorialv2.views.validations_contents import ActivateJSFiddleInContent
from zds.tutorialv2.views.containers_extracts import (
CreateContainer,
Expand Down Expand Up @@ -184,4 +185,6 @@
path("", RedirectView.as_view(pattern_name="publication:list", permanent=True), name="list"),
# Journal of events
path("evenements/<int:pk>/", EventsList.as_view(), name="events"),
# Goal-based classification
path("modifier-objectifs/<int:pk>/", EditGoals.as_view(), name="edit-goals"),
]
2 changes: 2 additions & 0 deletions zds/tutorialv2/views/contents.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from zds.tutorialv2.models.database import PublishableContent, Validation, ContentContribution, ContentSuggestion
from zds.tutorialv2.utils import init_new_repo
from zds.tutorialv2.views.authors import RemoveAuthorFromContent
from zds.tutorialv2.views.goals import EditGoalsForm
from zds.utils.models import get_hat_from_settings
from zds.mp.utils import send_mp, send_message_mp
from zds.utils.uuslug_wrapper import slugify
Expand Down Expand Up @@ -180,6 +181,7 @@ def get_forms(self, context):
context["formJs"] = form_js
context["form_edit_license"] = EditContentLicenseForm(self.versioned_object)
context["form_edit_tags"] = EditContentTagsForm(self.versioned_object, self.object)
context["form_edit_goals"] = EditGoalsForm(self.object)

if self.versioned_object.requires_validation:
context["formPublication"] = PublicationForm(self.versioned_object, initial={"source": self.object.source})
Expand Down
2 changes: 2 additions & 0 deletions zds/tutorialv2/views/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from zds.tutorialv2.utils import search_container_or_404, last_participation_is_old, mark_read
from zds.tutorialv2.views.containers_extracts import DisplayContainer
from zds.tutorialv2.views.contents import DisplayContent
from zds.tutorialv2.views.goals import EditGoalsForm
from zds.utils.models import CommentVote
from zds.utils.paginator import make_pagination

Expand Down Expand Up @@ -115,6 +116,7 @@ def get_context_data(self, **kwargs):
)

context["form_edit_tags"] = EditContentTagsForm(self.versioned_object, self.object)
context["form_edit_goals"] = EditGoalsForm(self.object)

# pagination of comments
make_pagination(
Expand Down
Loading