Skip to content

Commit

Permalink
Permet d'écrire des billets ou articles avec une structure de tutoriel (
Browse files Browse the repository at this point in the history
#6550)

* Permet d'écrire des billets ou articles avec une structure de tutoriel
* Refactorise les patterns d'URL pour les tutoriels
* Fix can_add_container
* Fix get_list_of_chapters
* Ajoute les tests de route basiques pour les tutoriels
* Mise à jour documentation
* Modifie la génération des fixtures
  • Loading branch information
Arnaud-D authored Sep 9, 2024
1 parent cefa2cb commit 29b75e0
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 124 deletions.
39 changes: 14 additions & 25 deletions doc/source/back-end/contents.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
=========================
Les tutoriels et articles
=========================
================================
Les tutoriels, articles, billets
================================

Vocabulaire et définitions
==========================
Expand Down Expand Up @@ -50,38 +50,36 @@ Un contenu

Un **contenu** est un agencement particulier de conteneurs et d'extraits. Il
est décrit par des métadonnées (*metadata*), détaillées
`ici <./contents_manifest.html>`__. Une de ces métadonnées est le type : article
ou tutoriel. Leur visée pédagogique diffère, mais aussi leur structure : un
article ne peut comporter de conteneurs, seulement des extraits, ce qui n'est
pas le cas d'un tutoriel.
`ici <./contents_manifest.html>`__. Une de ces métadonnées est le type : article, tutoriel ou billet.

Les exemples suivants devraient éclairer ces notions.
Techniquement, le type n'a pas d'influence sur la structure du contenu. On rencontre cependant
des structures typiques selon la longueur du contenu.

Communément appelé « mini-tutoriel » :
Structure typique pour un contenu court :

.. sourcecode:: none

+ Tutoriel
+ Contenu
+ Section
+ Section
+ Section

Communément appelé « moyen-tutoriel » :
Structure typique pour un contenu de taille moyenne :

.. sourcecode:: none

+ Tutoriel
+ Contenu
+ Partie
+ Section
+ Partie
+ Section
+ Section

Communément appelé « big-tutoriel » :
Structure typique pour un contenu long :

.. sourcecode:: none

+ Tutoriel
+ Contenu
+ Partie
+ Chapitre
+ Section
Expand All @@ -93,7 +91,7 @@ Communément appelé « big-tutoriel » :
+ Section
+ Section

On peut aussi faire un mélange des conteneurs :
Des structures plus complexes sont possibles, avec des niveaux de conteneurs différents selon les parties :

.. sourcecode:: none

Expand All @@ -107,7 +105,7 @@ On peut aussi faire un mélange des conteneurs :
+ Chapitre
+ Section

Mais pas de conteneurs et d'extraits adjacents :
Il n'est pas possible d'avoir à la fois des conteneurs et des extraits au même niveau :

.. sourcecode:: none

Expand All @@ -122,15 +120,6 @@ Mais pas de conteneurs et d'extraits adjacents :
+ Section
+ Section /!\ Impossible !

Pour finir, un article. Même structure qu'un mini-tutoriel, mais vocation
pédagogique différente :

.. sourcecode:: none

+ Article
+ Section
+ Section

D'autre part, tout contenu se voit attribuer un identifiant unique sous la
forme d'un entier naturel (en anglais : *pk*, pour *primary key*). Cet
identifiant apparaît dans les URL, qui sont de la forme
Expand Down
7 changes: 0 additions & 7 deletions zds/tutorialv2/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,13 @@
# verbose_name_plural User-friendly pluralized type name
# category_name User-friendly category name which contains this content
# requires_validation Boolean; whether this content has to be validated before publication
# single_container Boolean; True if the content is a single container
# beta Boolean; True if the content can be in beta
{
"name": "TUTORIAL",
"verbose_name": "tutoriel",
"verbose_name_plural": "tutoriels",
"category_name": "tutoriel",
"requires_validation": True,
"single_container": False,
"beta": True,
},
{
Expand All @@ -25,7 +23,6 @@
"verbose_name_plural": "articles",
"category_name": "article",
"requires_validation": True,
"single_container": True,
"beta": True,
},
{
Expand All @@ -34,7 +31,6 @@
"verbose_name_plural": "billets",
"category_name": "tribune",
"requires_validation": False,
"single_container": True,
"beta": False,
},
)
Expand All @@ -49,9 +45,6 @@
# a list of contents which have to be validated before publication
CONTENT_TYPES_REQUIRING_VALIDATION = [content["name"] for content in CONTENT_TYPES if content["requires_validation"]]

# a list of contents which have one big container containing at least one small container
SINGLE_CONTAINER_CONTENT_TYPES = [content["name"] for content in CONTENT_TYPES if content["single_container"]]

# a list of contents which can be in beta
CONTENT_TYPES_BETA = [content["name"] for content in CONTENT_TYPES if content["beta"]]

Expand Down
39 changes: 16 additions & 23 deletions zds/tutorialv2/models/versioned.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from django.template.loader import render_to_string

from zds.tutorialv2.models.mixins import TemplatableContentModelMixin
from zds.tutorialv2.models import SINGLE_CONTAINER_CONTENT_TYPES, CONTENT_TYPES_REQUIRING_VALIDATION
from zds.tutorialv2.models import CONTENT_TYPES_REQUIRING_VALIDATION
from zds.tutorialv2.utils import default_slug_pool, export_content, get_commit_author, InvalidOperationError
from zds.tutorialv2.utils import get_blob
from zds.utils.validators import InvalidSlugError, check_slug
Expand Down Expand Up @@ -258,16 +258,14 @@ def long_slug(self):
long_slug = self.parent.long_slug() + "__"
return long_slug + self.slug

def can_add_container(self):
def can_add_container(self) -> bool:
"""
:return: ``True`` if this container accepts child containers, ``False`` otherwise
:rtype: bool
Return `True` if adding child containers is allowed.
Adding subcontainers is forbidden:
* if the container already has extracts as children,
* or if the limit of nested containers has been reached.
"""
if not self.has_extracts():
if self.get_tree_depth() < settings.ZDS_APP["content"]["max_tree_depth"] - 1:
if not self.top_container().type in SINGLE_CONTAINER_CONTENT_TYPES:
return True
return False
return not self.has_extracts() and self.get_tree_depth() < settings.ZDS_APP["content"]["max_tree_depth"] - 1

def can_add_extract(self):
"""Return ``True`` if this container can contain extracts, i.e doesn't
Expand Down Expand Up @@ -1331,21 +1329,16 @@ def get_prod_path(self, relative=False, file_ext="html"):

return path

def get_list_of_chapters(self):
"""
:return: a list of chapters (Container which contains Extracts) in the reading order
:rtype: list[Container]
"""
def get_list_of_chapters(self) -> list[Container]:
continuous_list = []
if self.type not in SINGLE_CONTAINER_CONTENT_TYPES: # cannot be paginated
if len(self.children) != 0 and isinstance(self.children[0], Container): # children must be Containers!
for child in self.children:
if len(child.children) != 0:
if isinstance(child.children[0], Extract):
continuous_list.append(child) # it contains Extract, this is a chapter, so paginated
else: # Container is a part
for sub_child in child.children:
continuous_list.append(sub_child) # even if empty `sub_child.childreen`, it's chapter
if len(self.children) != 0 and isinstance(self.children[0], Container): # children must be Containers!
for child in self.children:
if len(child.children) != 0:
if isinstance(child.children[0], Extract):
continuous_list.append(child) # it contains Extract, this is a chapter, so paginated
else: # Container is a part
for sub_child in child.children:
continuous_list.append(sub_child) # even if `sub_child.children` is empty, it's a chapter
return continuous_list

def get_json(self):
Expand Down
8 changes: 0 additions & 8 deletions zds/tutorialv2/tests/tests_opinion_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,14 +177,6 @@ def test_accessible_ui_for_author(self):
self.assertNotContains(resp, "{}?subcategory=".format(reverse("publication:list")))
self.assertContains(resp, "{}?category=".format(reverse("opinion:list")))

def test_no_help_for_tribune(self):
self.client.force_login(self.user_author)

def test_help_for_article(self):
self.client.force_login(self.user_author)
resp = self.client.get(reverse("content:create-content", kwargs={"created_content_type": "ARTICLE"}))
self.assertEqual(200, resp.status_code)

def test_opinion_publication_staff(self):
"""
Test the publication of PublishableContent where type is OPINION (with staff).
Expand Down
78 changes: 78 additions & 0 deletions zds/tutorialv2/tests/tests_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from django.test import TestCase
from django.urls import reverse

from zds.tutorialv2.models.database import PublishableContent
from zds.tutorialv2.publication_utils import publish_content
from zds.tutorialv2.tests import override_for_contents, TutorialTestMixin
from zds.tutorialv2.tests.factories import PublishableContentFactory, ContainerFactory


def mock_publication_process(content: PublishableContent):
published = publish_content(content, content.load_version())
content.sha_public = content.sha_draft
content.public_version = published
content.save()


class BasicRouteTestsMixin(TutorialTestMixin):
def setUp(self):
self.build_content(self.content_type)
mock_publication_process(self.content)

def build_content(self, type):
self.content = PublishableContentFactory()
self.content.type = type
self.content.save()
content_versioned = self.content.load_version()
self.container = ContainerFactory(parent=content_versioned, db_object=self.content)
self.subcontainer = ContainerFactory(parent=self.container, db_object=self.content)

def test_view(self):
route_args = {
"pk": self.content.pk,
"slug": self.content.slug,
}
self.assert_can_be_reached(self.view_name, route_args)

def test_view_container_one_level_deep(self):
route_args = {
"pk": self.content.pk,
"slug": self.content.slug,
"container_slug": self.container.slug,
}
self.assert_can_be_reached(self.container_view_name, route_args)

def test_view_container_two_level_deep(self):
route_args = {
"pk": self.content.pk,
"slug": self.content.slug,
"parent_container_slug": self.container.slug,
"container_slug": self.subcontainer.slug,
}
self.assert_can_be_reached(self.container_view_name, route_args)

def assert_can_be_reached(self, route, route_args):
url = reverse(route, kwargs=route_args)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)


@override_for_contents()
class OpinionDisplayRoutesTests(BasicRouteTestsMixin, TestCase):
content_type = "OPINION"
view_name = "opinion:view"
container_view_name = "opinion:view-container"


@override_for_contents()
class ArticlesDisplayRoutesTests(BasicRouteTestsMixin, TestCase):
content_type = "ARTICLE"
view_name = "article:view"
container_view_name = "article:view-container"


@override_for_contents()
class TutorialsDisplayRoutesTests(BasicRouteTestsMixin, TestCase):
content_type = "TUTORIAL"
view_name = "tutorial:view"
container_view_name = "tutorial:view-container"
25 changes: 19 additions & 6 deletions zds/tutorialv2/urls/urls_articles.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,33 @@
from zds.tutorialv2.views.contributors import ContentOfContributors
from zds.tutorialv2.views.lists import TagsListView, ContentOfAuthor
from zds.tutorialv2.views.download_online import DownloadOnlineArticle
from zds.tutorialv2.views.display import ArticleOnlineView
from zds.tutorialv2.views.display import ArticleOnlineView, ContainerOnlineView
from zds.tutorialv2.feeds import LastArticlesFeedRSS, LastArticlesFeedATOM

urlpatterns = [
# Flux
feed_patterns = [
path("flux/rss/", LastArticlesFeedRSS(), name="feed-rss"),
path("flux/atom/", LastArticlesFeedATOM(), name="feed-atom"),
# View
]

display_patterns = [
path("<int:pk>/<slug:slug>/", ArticleOnlineView.as_view(), name="view"),
# Downloads
path(
"<int:pk>/<slug:slug>/<slug:parent_container_slug>/<slug:container_slug>/",
ContainerOnlineView.as_view(),
name="view-container",
),
path("<int:pk>/<slug:slug>/<slug:container_slug>/", ContainerOnlineView.as_view(), name="view-container"),
]

download_patterns = [
path("md/<int:pk>/<slug:slug>.md", DownloadOnlineArticle.as_view(requested_file="md"), name="download-md"),
path("pdf/<int:pk>/<slug:slug>.pdf", DownloadOnlineArticle.as_view(requested_file="pdf"), name="download-pdf"),
path("tex/<int:pk>/<slug:slug>.tex", DownloadOnlineArticle.as_view(requested_file="tex"), name="download-tex"),
path("epub/<int:pk>/<slug:slug>.epub", DownloadOnlineArticle.as_view(requested_file="epub"), name="download-epub"),
path("zip/<int:pk>/<slug:slug>.zip", DownloadOnlineArticle.as_view(requested_file="zip"), name="download-zip"),
# Listing
]

listing_patterns = [
path("", RedirectView.as_view(pattern_name="publication:list", permanent=True)),
re_path(r"tags/*", TagsListView.as_view(displayed_types=["ARTICLE"]), name="tags"),
path(
Expand All @@ -33,3 +44,5 @@
name="find-contributions-article",
),
]

urlpatterns = feed_patterns + display_patterns + download_patterns + listing_patterns
25 changes: 19 additions & 6 deletions zds/tutorialv2/urls/urls_opinions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,38 @@
from zds.tutorialv2.feeds import LastOpinionsFeedRSS, LastOpinionsFeedATOM
from zds.tutorialv2.views.lists import ListOpinions, ContentOfAuthor
from zds.tutorialv2.views.download_online import DownloadOnlineOpinion
from zds.tutorialv2.views.display import OpinionOnlineView
from zds.tutorialv2.views.display import OpinionOnlineView, ContainerOnlineView

urlpatterns = [
# Flux
feed_patterns = [
path("flux/rss/", LastOpinionsFeedRSS(), name="feed-rss"),
path("flux/atom/", LastOpinionsFeedATOM(), name="feed-atom"),
# View
]

display_patterns = [
path("<int:pk>/<slug:slug>/", OpinionOnlineView.as_view(), name="view"),
# downloads:
path(
"<int:pk>/<slug:slug>/<slug:parent_container_slug>/<slug:container_slug>/",
ContainerOnlineView.as_view(),
name="view-container",
),
path("<int:pk>/<slug:slug>/<slug:container_slug>/", ContainerOnlineView.as_view(), name="view-container"),
]

download_patterns = [
path("md/<int:pk>/<slug:slug>.md", DownloadOnlineOpinion.as_view(requested_file="md"), name="download-md"),
path("pdf/<int:pk>/<slug:slug>.pdf", DownloadOnlineOpinion.as_view(requested_file="pdf"), name="download-pdf"),
path("epub/<int:pk>/<slug:slug>.epub", DownloadOnlineOpinion.as_view(requested_file="epub"), name="download-epub"),
path("zip/<int:pk>/<slug:slug>.zip", DownloadOnlineOpinion.as_view(requested_file="zip"), name="download-zip"),
path("tex/<int:pk>/<slug:slug>.tex", DownloadOnlineOpinion.as_view(requested_file="tex"), name="download-tex"),
# Listing
]

listing_patterns = [
path("", ListOpinions.as_view(), name="list"),
path(
"voir/<str:username>/",
ContentOfAuthor.as_view(type="OPINION", context_object_name="opinions"),
name="find-opinion",
),
]

urlpatterns = feed_patterns + display_patterns + download_patterns + listing_patterns
Loading

0 comments on commit 29b75e0

Please sign in to comment.