diff --git a/templates/tutorialv2/includes/shared_content_child.html b/templates/tutorialv2/includes/shared_content_child.html new file mode 100644 index 0000000000..9450173084 --- /dev/null +++ b/templates/tutorialv2/includes/shared_content_child.html @@ -0,0 +1,63 @@ +{% load emarkdown %} +{% load i18n %} +{% load times %} +{% load target_tree %} + +{% if not hide_title %} +

+ + {{ child.title }} + +

+{% endif %} +{% if child.text %} + {# child is an extract #} + {% if child.get_text.strip|length == 0 %} +

+ {% trans "Cette section est actuellement vide." %} +

+ {% else %} +
+ {{ child.get_text|emarkdown }} +
+ {% endif %} +{% else %} + {# child is a container #} + + {% if child.has_extracts %} +
    + {% for extract in child.children %} +
  1. + {{ extract.title }} +
  2. + {% endfor %} +
+ {% elif child.has_sub_containers %} +
    + {% for subchild in child.children %} +
  1. +

    + {{ subchild.title }} +

    +
      + {% for extract in subchild.children %} +
    1. +

      + {{ extract.title }} +

      +
    2. + {% endfor %} +
    +
  2. + {% endfor %} +
+ {% endif %} +{% endif %} + + +{% if not child.has_sub_containers %} + +{% endif %} diff --git a/templates/tutorialv2/view/list_shareable_links.html b/templates/tutorialv2/view/list_shareable_links.html new file mode 100644 index 0000000000..a95d3e5d99 --- /dev/null +++ b/templates/tutorialv2/view/list_shareable_links.html @@ -0,0 +1,106 @@ +{% extends "tutorialv2/base.html" %} +{% load i18n %} + +{% block content %} + +

{% blocktrans %} Liens de partage pour « {{ content }} » {% endblocktrans %}

+ +

{% trans "Diffusez votre contenu en partageant un simple lien accessible sans incription sur le site." %}

+ +

{% trans "Les liens de partages offrent les fonctionnalités suivantes :" %}

+ +{% blocktrans %} + +{% endblocktrans %} + + + {% trans "Créer un lien de partage" %} + + + + + + +

{% trans "Liens actifs" %}

+ +

+ {% blocktrans %} + Les personnes disposant d'un lien actif peuvent l'utiliser pour lire le contenu. + Il est possible de désactiver un lien temporairement pour en interdire son usage, et le réactiver plus tard. + {% endblocktrans %} +

+ +{% if not active_links_and_forms %} + +

{% trans "Vous n'avez pas de liens de partage actifs." %}

+ +{% else %} + + + +{% endif %} + + +

{% trans "Liens expirés" %}

+ +

+ {% blocktrans %} + Un lien de partage expiré ne permet pas de lire le contenu. + Si un lien est expiré, vous pouvez modifier sa date d'expiration pour qu'il fonctionne de nouveau. + {% endblocktrans %} +

+ +{% if not expired_links_and_forms %} + +

{% trans "Vous n'avez pas de liens de partage expirés." %}

+ +{% else %} + + + +{% endif %} + +

{% trans "Liens inactifs" %}

+ +

+ {% blocktrans %} + Un lien de partage inactif ne permet pas de lire le contenu. + Vous pouvez le réactiver quand vous le souhaitez pour autoriser de nouveau son usage. + {% endblocktrans %} +

+ +{% if not inactive_links_and_forms %} + +

{% trans "Vous n'avez pas de liens de partage inactifs." %}

+ +{% else %} + + + +{% endif %} + +{% endblock %} diff --git a/templates/tutorialv2/view/list_shareable_links.part.html b/templates/tutorialv2/view/list_shareable_links.part.html new file mode 100644 index 0000000000..d8375b4371 --- /dev/null +++ b/templates/tutorialv2/view/list_shareable_links.part.html @@ -0,0 +1,76 @@ +{% load i18n %} + + diff --git a/templates/tutorialv2/view/shared_container.html b/templates/tutorialv2/view/shared_container.html new file mode 100644 index 0000000000..4c2dfcdc47 --- /dev/null +++ b/templates/tutorialv2/view/shared_container.html @@ -0,0 +1,98 @@ +{% extends "tutorialv2/base.html" %} +{% load set %} +{% load thumbnail %} +{% load emarkdown %} +{% load i18n %} +{% load times %} +{% load feminize %} +{% load pluralize_fr %} + +{% block title %} + {{ container.title }} - {{ content.title }} +{% endblock %} + + + +{% block breadcrumb %} + + {% if container.parent.parent %} +
  • + {{ container.parent.parent.title }} +
  • + {% endif %} + + {% if container.parent %} +
  • + {{ container.parent.title }} +
  • + {% endif %} + +
  • {{ container.title }}
  • + +{% endblock %} + + +{% block headline %} + + {% if content.licence %} +

    {{ content.licence }}

    + {% endif %} + +

    {{ container.title }}

    + + {% include 'tutorialv2/includes/tags_authors.part.html' with publishablecontent=content online=False %} + +{% endblock %} + + +{% block content %} + + {% include "tutorialv2/includes/chapter_pager.part.html" with position="top" %} + + {% if container.introduction and container.get_introduction %} + {{ container.get_introduction|emarkdown:is_js }} + {% endif %} + + {% if container.has_extracts %} + + {% endif %} + + {% for child in container.children %} + {% include "tutorialv2/includes/shared_content_child.html" with child=child %} + {% empty %} + {% if not container.is_chapter %} +
    +

    + {{ "Ce"|feminize:container.get_level_as_string }} {{ container.get_level_as_string|lower }} {% trans " est actuellement vide." %} +

    +
    + {% endif %} + {% endfor %} + +
    + + {% if container.conclusion and container.get_conclusion %} + {{ container.get_conclusion|emarkdown }} + {% endif %} + + {% include "tutorialv2/includes/chapter_pager.part.html" with position="bottom" %} + + {% if content.is_beta and container.has_extracts %} + {% include "tutorialv2/includes/warn_typo.part.html" with content=content %} + {% endif %} +{% endblock %} + + +{% block sidebar_blocks %} + + {% include "tutorialv2/includes/summary.part.html" with current_container=container %} + +{% endblock %} diff --git a/templates/tutorialv2/view/shared_content.html b/templates/tutorialv2/view/shared_content.html new file mode 100644 index 0000000000..bbcaf4535f --- /dev/null +++ b/templates/tutorialv2/view/shared_content.html @@ -0,0 +1,97 @@ +{% extends "tutorialv2/base_online.html" %} +{% load i18n %} +{% load captureas %} + +{% block title %} + {{ content.title }} +{% endblock %} + +{% block breadcrumb %} +
  • {{ content.title }}
  • +{% endblock %} + +{% block headline %} + {% if content.licence %} + + {{ content.licence }} + + {% endif %} + +

    + {% if content.image %} + + {% endif %} + {{ content.title }} +

    + + {% if content.description %} +

    + {{ content.description }} +

    + {% endif %} + + {% include 'tutorialv2/includes/tags_authors.part.html' with publishablecontent=content online=False %} + {% if content.is_opinion %} + {% if content.converted_to %} + {% if content.converted_to.get_absolute_url_online %} +
    + {% blocktrans with url_article=content.converted_to.get_absolute_url_online %} + Ce billet a été promu en article. + {% endblocktrans %} +
    + {% elif is_staff %} +
    + {% blocktrans with url_article=content.converted_to.get_absolute_url %} + Ce billet a fait l’objet d’une demande de publication en tant qu’article. Il est donc présent dans la zone de validation en attente de prise en charge par un validateur. + {% endblocktrans %} +
    + {% endif %} + {% endif %} + {% endif %} + +{% endblock %} + +{% block content %} + + {% captureas content_pager %} + {% include "tutorialv2/includes/content_pager.part.html" with content=content %} + {% endcaptureas %} + + {{ content_pager }} + + {% if content.has_extracts %} + {{ content.get_content_online|safe }} + {% else %} + {% if content.introduction %} + {{ content.get_introduction|default:""|safe }} + {% endif %} + + {% if not content.has_sub_containers %} +
      + {% endif %} + + {% captureas url %} + {% url "content:shareable-link" link.id %} + {% endcaptureas %} + + {% for child in content.children %} + {% include "tutorialv2/includes/shared_content_child.html" with url=url child=child %} + {% endfor %} + + {% if not content.has_sub_containers %} +
    + {% endif %} + +
    +
    + + {% if content.conclusion %} + {{ content.get_conclusion_online|default:""|safe }} + {% endif %} + + {% endif %} + {{ content_pager }} + {% include "tutorialv2/includes/alert.html" with content=content current_content_type=current_content_type %} + {% include "tutorialv2/includes/warn_typo.part.html" with content=content %} + +{% endblock %} diff --git a/zds/tutorialv2/migrations/0036_shareablelink.py b/zds/tutorialv2/migrations/0036_shareablelink.py new file mode 100644 index 0000000000..193f9500ce --- /dev/null +++ b/zds/tutorialv2/migrations/0036_shareablelink.py @@ -0,0 +1,40 @@ +# Generated by Django 3.2.15 on 2022-09-29 22:07 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ("tutorialv2", "0035_alter_publishablecontent_goals"), + ] + + operations = [ + migrations.CreateModel( + name="ShareableLink", + fields=[ + ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ("active", models.BooleanField(default=True)), + ("expiration", models.DateTimeField(null=True)), + ("description", models.CharField(default="Lien de partage", max_length=150)), + ( + "type", + models.CharField( + choices=[("DRAFT", "Lien vers le dernier brouillon"), ("BETA", "Lien vers la dernière bêta")], + default="DRAFT", + max_length=10, + ), + ), + ( + "content", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="tutorialv2.publishablecontent", + verbose_name="Contenu", + ), + ), + ], + ), + ] diff --git a/zds/tutorialv2/models/__init__.py b/zds/tutorialv2/models/__init__.py index b636226aa8..2dfa21b17c 100644 --- a/zds/tutorialv2/models/__init__.py +++ b/zds/tutorialv2/models/__init__.py @@ -69,3 +69,8 @@ ("REJECT", _("Rejeté")), ("CANCEL", _("Annulé")), ) + +SHAREABLE_LINK_TYPES = ( + ("DRAFT", _("Lien vers le dernier brouillon")), + ("BETA", _("Lien vers la dernière bêta")), +) diff --git a/zds/tutorialv2/models/shareable_links.py b/zds/tutorialv2/models/shareable_links.py new file mode 100644 index 0000000000..6c0700c52c --- /dev/null +++ b/zds/tutorialv2/models/shareable_links.py @@ -0,0 +1,61 @@ +import uuid +from datetime import datetime + +from django.conf import settings +from django.db import models +from django.db.models import Q +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from zds.tutorialv2.models import SHAREABLE_LINK_TYPES +from zds.tutorialv2.models.database import PublishableContent + + +class ShareableLinkQuerySet(models.QuerySet): + def for_content(self, content): + return self.filter(content=content) + + def active_and_for_content(self, content): + return self.for_content(content).active() + + def expired_and_for_content(self, content): + return self.for_content(content).expired() + + def inactive_and_for_content(self, content): + return self.for_content(content).inactive() + + def active(self): + pivot_date = datetime.now() + return self.filter(Q(active=True) & (Q(expiration__gte=pivot_date) | Q(expiration=None))) + + def expired(self): + pivot_date = datetime.now() + return self.filter(active=True, expiration__lt=pivot_date) + + def inactive(self): + return self.filter(active=False) + + +class ShareableLink(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + content = models.ForeignKey(PublishableContent, verbose_name="Contenu", on_delete=models.CASCADE) + active = models.BooleanField(default=True) + expiration = models.DateTimeField(null=True) + description = models.CharField(default=_("Lien de partage"), max_length=150) + # Types + # DRAFT: always points to the last draft version + # BETA: always points to the last beta version + type = models.CharField(max_length=10, choices=SHAREABLE_LINK_TYPES, default="DRAFT") + + objects = ShareableLinkQuerySet.as_manager() + + def full_url(self): + return settings.ZDS_APP["site"]["url"] + reverse("content:shareable-link", kwargs={"id": self.id}) + + def deactivate(self): + self.active = False + self.save() + + def reactivate(self): + self.active = True + self.save() diff --git a/zds/tutorialv2/models/versioned.py b/zds/tutorialv2/models/versioned.py index b43753a1f7..820a42f212 100644 --- a/zds/tutorialv2/models/versioned.py +++ b/zds/tutorialv2/models/versioned.py @@ -387,6 +387,9 @@ def get_absolute_url(self): """ return self.top_container().get_absolute_url() + self.get_path(relative=True, os_sensitive=False) + "/" + def get_relative_url(self): + return self.get_path(relative=True, os_sensitive=False) + "/" + def get_absolute_url_online(self): """ @@ -950,6 +953,9 @@ def get_absolute_url(self): """ return f"{self.container.get_absolute_url()}#{self.position_in_parent}-{self.slug}" + def get_relative_url(self): + return f"{self.container.get_relative_url()}#{self.position_in_parent}-{self.slug}" + def get_absolute_url_online(self): """ :return: the url to access the tutorial when online diff --git a/zds/tutorialv2/tests/tests_views/tests_shareable_links.py b/zds/tutorialv2/tests/tests_views/tests_shareable_links.py new file mode 100644 index 0000000000..2434b39ffa --- /dev/null +++ b/zds/tutorialv2/tests/tests_views/tests_shareable_links.py @@ -0,0 +1,313 @@ +from datetime import datetime + +from django.test import TestCase +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.tutorialv2.factories import PublishableContentFactory +from zds.tutorialv2.models.shareable_links import ShareableLink +from zds.tutorialv2.tests import TutorialTestMixin + + +class ListShareableLinksTests(TutorialTestMixin, TestCase): + 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.url = reverse("content:list-shareable-links", kwargs={"pk": self.content.pk}) + self.login_url = reverse("member-login") + "?next=" + self.url + + def test_not_authenticated(self): + self.client.logout() + response = self.client.get(self.url) + self.assertRedirects(response, self.login_url) + + def test_authenticated_author(self): + self.client.force_login(self.author) + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + + def test_authenticated_staff(self): + self.client.force_login(self.staff) + response = self.client.get(self.url) + self.assertEqual(response.status_code, 403) + + def test_authenticated_outsider(self): + self.client.force_login(self.outsider) + response = self.client.get(self.url) + self.assertEqual(response.status_code, 403) + + def test_no_link(self): + self.client.force_login(self.author) + response = self.client.get(self.url) + self.assertContains(response, _("Vous n'avez pas de liens de partage actifs.")) + self.assertContains(response, _("Créer un lien de partage")) + + def test_one_link(self): + self.client.force_login(self.author) + ShareableLink(content=self.content).save() + response = self.client.get(self.url) + self.assertContains(response, _("Liens actifs")) + self.assertContains(response, _("Créer un lien de partage")) + self.assertContains(response, '