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/tests/tests_views/shareable_links/__init__.py b/zds/tutorialv2/tests/tests_views/shareable_links/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/zds/tutorialv2/tests/tests_views/shareable_links/tests_createshareablelinkview.py b/zds/tutorialv2/tests/tests_views/shareable_links/tests_createshareablelinkview.py new file mode 100644 index 0000000000..181f59afe7 --- /dev/null +++ b/zds/tutorialv2/tests/tests_views/shareable_links/tests_createshareablelinkview.py @@ -0,0 +1,47 @@ +from django.test import TestCase +from django.urls import reverse + +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory +from zds.tutorialv2.models.shareable_links import ShareableLink +from zds.tutorialv2.tests.factories import PublishableContentFactory +from zds.tutorialv2.views.shareable_links import CreateShareableLinkView + + +class CreateShareableLinkTests(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:create-shareable-link", kwargs={"pk": self.content.pk}) + self.redirect_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.post(self.url) + self.assertRedirects(response, self.login_url) + + def test_authenticated_author(self): + self.client.force_login(self.author) + n_links_before = ShareableLink.objects.all().count() + data = {"description": "Ceci n'est pas le lien vers La Blague", "expiration": "2042-08-01", "type": "BETA"} + response = self.client.post(self.url, data=data) + self.assertRedirects(response, self.redirect_url, target_status_code=200) + n_links_after = ShareableLink.objects.all().count() + self.assertEqual(n_links_after, n_links_before + 1) + + def test_authenticated_staff(self): + self.client.force_login(self.staff) + response = self.client.post(self.url) + self.assertEqual(response.status_code, 403) + + def test_authenticated_outsider(self): + self.client.force_login(self.outsider) + response = self.client.post(self.url) + self.assertEqual(response.status_code, 403) diff --git a/zds/tutorialv2/tests/tests_views/shareable_links/tests_deactivateshareablelinkview.py b/zds/tutorialv2/tests/tests_views/shareable_links/tests_deactivateshareablelinkview.py new file mode 100644 index 0000000000..0b99eec4ea --- /dev/null +++ b/zds/tutorialv2/tests/tests_views/shareable_links/tests_deactivateshareablelinkview.py @@ -0,0 +1,48 @@ +from django.test import TestCase +from django.urls import reverse + +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory +from zds.tutorialv2.models.shareable_links import ShareableLink +from zds.tutorialv2.tests.factories import PublishableContentFactory +from zds.tutorialv2.views.shareable_links import DeactivateShareableLinkView + + +class DeactivateShareableLinkTests(TestCase): + def setUp(self): + # Create users + self.author = ProfileFactory().user + self.staff = StaffProfileFactory().user + self.outsider = ProfileFactory().user + + # Create a content and a link + self.content = PublishableContentFactory(author_list=[self.author]) + self.link = ShareableLink(content=self.content) + self.link.save() + + # Get information to be reused in tests + self.url = reverse("content:deactivate-shareable-link", kwargs={"id": self.link.id}) + self.redirect_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.post(self.url) + self.assertRedirects(response, self.login_url) + + def test_authenticated_author(self): + self.client.force_login(self.author) + response = self.client.post(self.url, follow=True) + self.assertRedirects(response, self.redirect_url, target_status_code=200) + self.assertContains(response, DeactivateShareableLinkView.success_message) + self.link.refresh_from_db() + self.assertFalse(self.link.active) + + def test_authenticated_staff(self): + self.client.force_login(self.staff) + response = self.client.post(self.url) + self.assertEqual(response.status_code, 403) + + def test_authenticated_outsider(self): + self.client.force_login(self.outsider) + response = self.client.post(self.url) + self.assertEqual(response.status_code, 403) diff --git a/zds/tutorialv2/tests/tests_views/shareable_links/tests_deleteshareablelinkview.py b/zds/tutorialv2/tests/tests_views/shareable_links/tests_deleteshareablelinkview.py new file mode 100644 index 0000000000..0ee3f2ecce --- /dev/null +++ b/zds/tutorialv2/tests/tests_views/shareable_links/tests_deleteshareablelinkview.py @@ -0,0 +1,48 @@ +from django.test import TestCase +from django.urls import reverse + +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory +from zds.tutorialv2.models.shareable_links import ShareableLink +from zds.tutorialv2.tests.factories import PublishableContentFactory +from zds.tutorialv2.views.shareable_links import DeleteShareableLinkView + + +class Tests(TestCase): + def setUp(self): + # Create users + self.author = ProfileFactory().user + self.staff = StaffProfileFactory().user + self.outsider = ProfileFactory().user + + # Create content and links + self.content = PublishableContentFactory(author_list=[self.author]) + self.link = ShareableLink(content=self.content) + self.link.save() + + # Get information to be reused in tests + self.url = reverse("content:delete-shareable-link", kwargs={"id": self.link.id}) + self.redirect_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.post(self.url) + self.assertRedirects(response, self.login_url) + + def test_authenticated_author(self): + self.client.force_login(self.author) + response = self.client.post(self.url, follow=True) + self.assertRedirects(response, self.redirect_url, target_status_code=200) + self.assertContains(response, DeleteShareableLinkView.success_message) + with self.assertRaises(ShareableLink.DoesNotExist): + ShareableLink.objects.get(id=self.link.id) + + def test_authenticated_staff(self): + self.client.force_login(self.staff) + response = self.client.post(self.url) + self.assertEqual(response.status_code, 403) + + def test_authenticated_outsider(self): + self.client.force_login(self.outsider) + response = self.client.post(self.url) + self.assertEqual(response.status_code, 403) diff --git a/zds/tutorialv2/tests/tests_views/shareable_links/tests_editshareablelinkview.py b/zds/tutorialv2/tests/tests_views/shareable_links/tests_editshareablelinkview.py new file mode 100644 index 0000000000..c488d8cbe9 --- /dev/null +++ b/zds/tutorialv2/tests/tests_views/shareable_links/tests_editshareablelinkview.py @@ -0,0 +1,53 @@ +from datetime import datetime + +from django.test import TestCase +from django.urls import reverse + +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory +from zds.tutorialv2.models.shareable_links import ShareableLink +from zds.tutorialv2.tests.factories import PublishableContentFactory +from zds.tutorialv2.views.shareable_links import EditShareableLinkView + + +class EditShareableLinkTests(TestCase): + def setUp(self): + # Create users + self.author = ProfileFactory().user + self.staff = StaffProfileFactory().user + self.outsider = ProfileFactory().user + + # Create a content and a link + self.content = PublishableContentFactory(author_list=[self.author]) + self.link = ShareableLink(content=self.content) + self.link.save() + + # Get information to be reused in tests + self.url = reverse("content:edit-shareable-link", kwargs={"id": self.link.id}) + self.redirect_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.post(self.url) + self.assertRedirects(response, self.login_url) + + def test_authenticated_author(self): + self.client.force_login(self.author) + data = {"description": "Ceci n'est pas le lien vers La Blague", "expiration": "2042-08-01", "type": "BETA"} + response = self.client.post(self.url, data=data, follow=True) + self.assertRedirects(response, self.redirect_url, target_status_code=200) + self.assertContains(response, EditShareableLinkView.success_message) + self.link.refresh_from_db() + self.assertEqual(self.link.description, data["description"]) + self.assertEqual(self.link.expiration, datetime.strptime(data["expiration"], "%Y-%m-%d")) + self.assertEqual(self.link.type, data["type"]) + + def test_authenticated_staff(self): + self.client.force_login(self.staff) + response = self.client.post(self.url) + self.assertEqual(response.status_code, 403) + + def test_authenticated_outsider(self): + self.client.force_login(self.outsider) + response = self.client.post(self.url) + self.assertEqual(response.status_code, 403) diff --git a/zds/tutorialv2/tests/tests_views/shareable_links/tests_listshareablelinksview.py b/zds/tutorialv2/tests/tests_views/shareable_links/tests_listshareablelinksview.py new file mode 100644 index 0000000000..df067b42af --- /dev/null +++ b/zds/tutorialv2/tests/tests_views/shareable_links/tests_listshareablelinksview.py @@ -0,0 +1,66 @@ +from django.test import TestCase +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory +from zds.tutorialv2.models.shareable_links import ShareableLink +from zds.tutorialv2.tests import TutorialTestMixin +from zds.tutorialv2.tests.factories import PublishableContentFactory + + +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, '