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..8f6280df7b --- /dev/null +++ b/templates/tutorialv2/view/list_shareable_links.part.html @@ -0,0 +1,76 @@ +{% load i18n %} + + diff --git a/zds/tutorialv2/migrations/0042_shareablelink.py b/zds/tutorialv2/migrations/0042_shareablelink.py new file mode 100644 index 0000000000..4ff78de8c9 --- /dev/null +++ b/zds/tutorialv2/migrations/0042_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", "0041_remove_must_reindex"), + ] + + 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/mixins.py b/zds/tutorialv2/mixins.py index 520309efdb..da27e8b03d 100644 --- a/zds/tutorialv2/mixins.py +++ b/zds/tutorialv2/mixins.py @@ -8,7 +8,6 @@ from django.views.generic import DetailView, FormView from django.views.generic import View -from zds.forum.models import Topic from zds.tutorialv2.models.database import PublishableContent, PublishedContent, ContentRead from zds.tutorialv2.utils import mark_read from zds.tutorialv2.models.help_requests import HelpWriting @@ -48,6 +47,7 @@ class SingleContentViewMixin: sha = None must_be_author = True authorized_for_staff = True + authorized_for_all = False # used for shareable links is_staff = False is_author = False must_redirect = False @@ -97,7 +97,7 @@ def get_versioned_object(self): is_beta = self.object.is_beta(self.sha) is_public = self.object.is_public(self.sha) and self.public_is_prioritary - if not is_beta and not is_public and not self.is_author: + if not is_beta and not is_public and not self.is_author and not self.authorized_for_all: if not self.is_staff or (not self.authorized_for_staff and self.must_be_author): raise PermissionDenied diff --git a/zds/tutorialv2/models/__init__.py b/zds/tutorialv2/models/__init__.py index e96c506d1f..d153119888 100644 --- a/zds/tutorialv2/models/__init__.py +++ b/zds/tutorialv2/models/__init__.py @@ -62,3 +62,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..878605a932 --- /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-view", 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 1e062eeef2..37588e7a7a 100644 --- a/zds/tutorialv2/models/versioned.py +++ b/zds/tutorialv2/models/versioned.py @@ -385,6 +385,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): """ @@ -948,6 +951,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..ec0ed11d70 --- /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, '