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 %}
+
+{% endif %}
+{% if child.text %}
+ {# child is an extract #}
+ {% if child.get_text.strip|length == 0 %}
+
+ {% trans "Cette section est actuellement vide." %}
+
+ {% else %}
+
+ {% endif %}
+{% else %}
+ {# child is a container #}
+
+ {% if child.has_extracts %}
+
+ {% for extract in child.children %}
+
+ {{ extract.title }}
+
+ {% endfor %}
+
+ {% elif child.has_sub_containers %}
+
+ {% for subchild in child.children %}
+
+
+
+ {% for extract in subchild.children %}
+
+
+
+ {% endfor %}
+
+
+ {% 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 %}
+
+ partage de la dernière bêta ou du dernier brouillon ;
+ validité temporaire ou permanente ;
+ désactivation et réactivation à volonté.
+
+{% 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 %}
+
+
+ {% for link, edit_form in active_links_and_forms reversed %}
+ {% include 'tutorialv2/view/list_shareable_links.part.html' with link=link edit_form=edit_form content=content section="active" %}
+ {% endfor %}
+
+
+{% 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 %}
+
+
+ {% for link, edit_form in expired_links_and_forms reversed %}
+ {% include 'tutorialv2/view/list_shareable_links.part.html' with link=link edit_form=edit_form content=content section="expired" %}
+ {% endfor %}
+
+
+{% 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 %}
+
+
+ {% for link, edit_form in inactive_links_and_forms reversed %}
+ {% include 'tutorialv2/view/list_shareable_links.part.html' with link=link edit_form=edit_form content=content section="inactive" %}
+ {% endfor %}
+
+
+{% 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 %}
+
+
+ {{ link.description }}
+
+
+ {% if link.type == "DRAFT" %}
+ {% trans "Lien vers le dernier brouillon " %}
+ {% elif link.type == "BETA" %}
+ {% trans "Lien vers la dernière bêta " %}
+ {% else %}
+ {% trans "Lien de type inconnu " %}
+ {% endif %}
+
+
+
+ {% if link.expiration and link.expired %}
+ {% blocktrans with date=link.expiration %}
+ Lien expiré depuis le {{ date }}
+ {% endblocktrans %}
+ {% elif link.expiration and not link.expired %}
+ {% blocktrans with date=link.expiration %}
+ Valide jusqu'au {{ date }}
+ {% endblocktrans %}
+ {% else %}
+ {% trans "Valide indéfiniment " %}
+ {% endif %}
+
+
+
+ {% trans "Modifier" %}
+
+
+
+
+
+
+ {% trans "Supprimer" %}
+
+
+
+
+ {% if section == "active" or section == "expired" %}
+
+ {% else %}
+
+ {% endif %}
+
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, '', count=1)
+
+ def test_two_links(self):
+ self.client.force_login(self.author)
+ ShareableLink(content=self.content).save()
+ 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, ' ', count=2)
+
+
+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 = len(ShareableLink.objects.all())
+ 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)
+ n_links_after = len(ShareableLink.objects.all())
+ 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)
+
+
+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)
+ self.assertRedirects(response, self.redirect_url)
+ link = ShareableLink.objects.get(id=self.link.id)
+ self.assertEqual(link.description, data["description"])
+ self.assertEqual(link.expiration, datetime.strptime(data["expiration"], "%Y-%m-%d"))
+ self.assertEqual(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)
+
+
+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)
+ self.assertRedirects(response, self.redirect_url)
+ link = ShareableLink.objects.get(id=self.link.id)
+ self.assertFalse(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)
+
+
+class ReactivateShareableLinkTests(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:reactivate-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)
+ self.assertRedirects(response, self.redirect_url)
+ link = ShareableLink.objects.get(id=self.link.id)
+ self.assertTrue(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)
+
+
+class DeleteShareableLinkTests(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)
+ self.assertRedirects(response, self.redirect_url)
+ 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)
+
+
+class DisplaySharedContentPermissionTest(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.link_url = reverse("content:shareable-link-view", kwargs={"id": self.link.id})
+
+ def test_not_authenticated(self):
+ self.client.logout()
+ response = self.client.get(self.link_url)
+ self.assertEqual(response.status_code, 200)
+
+ def test_authenticated_author(self):
+ self.client.force_login(self.author)
+ response = self.client.get(self.link_url)
+ self.assertEqual(response.status_code, 200)
+
+ def test_authenticated_staff(self):
+ self.client.force_login(self.staff)
+ response = self.client.get(self.link_url)
+ self.assertEqual(response.status_code, 200)
+
+ def test_authenticated_outsider(self):
+ self.client.force_login(self.outsider)
+ response = self.client.get(self.link_url)
+ self.assertEqual(response.status_code, 200)
+
+ def test_authenticated_author_inactive(self):
+ self.client.force_login(self.author)
+ self.link.deactivate()
+ response = self.client.get(self.link_url)
+ self.assertEqual(response.status_code, 403)
diff --git a/zds/tutorialv2/urls/urls_contents.py b/zds/tutorialv2/urls/urls_contents.py
index 89da39e1e1..1a781eb73d 100644
--- a/zds/tutorialv2/urls/urls_contents.py
+++ b/zds/tutorialv2/urls/urls_contents.py
@@ -11,6 +11,7 @@
EditIntroductionView,
EditConclusionView,
)
+from zds.tutorialv2.views.shared_content import ContentSharedView, ContainerSharedView
from zds.tutorialv2.views.thumbnail import EditThumbnailView
from zds.tutorialv2.views.display.container import ContainerValidationView
from zds.tutorialv2.views.display.content import ContentValidationView
@@ -18,6 +19,14 @@
from zds.tutorialv2.views.goals import EditGoals, MassEditGoals, ViewContentsByGoal
from zds.tutorialv2.views.labels import EditLabels, ViewContentsByLabel
from zds.tutorialv2.views.licence import EditContentLicense
+from zds.tutorialv2.views.shareable_links import (
+ ListShareableLinks,
+ CreateShareableLink,
+ DeactivateShareableLink,
+ EditShareableLink,
+ ReactivateShareableLink,
+ DeleteShareableLink,
+)
from zds.tutorialv2.views.validations_contents import ActivateJSFiddleInContent
from zds.tutorialv2.views.containers_extracts import (
CreateContainer,
@@ -265,5 +274,23 @@ def get_version_pages():
# Label-based classification
path("modifier-labels//", EditLabels.as_view(), name="edit-labels"),
path("labels//", ViewContentsByLabel.as_view(), name="view-labels"),
+ # Shareable links
+ path("partage/gerer//", ListShareableLinks.as_view(), name="list-shareable-links"),
+ path("partage/creer//", CreateShareableLink.as_view(), name="create-shareable-link"),
+ path("partage/modifier//", EditShareableLink.as_view(), name="edit-shareable-link"),
+ path("partage/desactiver//", DeactivateShareableLink.as_view(), name="deactivate-shareable-link"),
+ path("partage/reactiver//", ReactivateShareableLink.as_view(), name="reactivate-shareable-link"),
+ path("partage/supprimer//", DeleteShareableLink.as_view(), name="delete-shareable-link"),
+ path("partage//", ContentSharedView.as_view(), name="shareable-link-view"),
+ path(
+ "partage////",
+ ContainerSharedView.as_view(),
+ name="shareable-link-container",
+ ),
+ path(
+ "partage///",
+ ContainerSharedView.as_view(),
+ name="shareable-link-container",
+ ),
]
)
diff --git a/zds/tutorialv2/views/shareable_links.py b/zds/tutorialv2/views/shareable_links.py
new file mode 100644
index 0000000000..89227a476f
--- /dev/null
+++ b/zds/tutorialv2/views/shareable_links.py
@@ -0,0 +1,138 @@
+from django import forms
+from django.contrib import messages
+from django.contrib.auth.decorators import login_required
+from django.core.exceptions import PermissionDenied
+from django.shortcuts import get_object_or_404, redirect
+from django.urls import reverse
+from django.utils.decorators import method_decorator
+from django.utils.translation import gettext_lazy as _
+from django.views import View
+from django.views.generic import TemplateView
+
+from zds.member.decorator import LoginRequiredMixin
+from zds.tutorialv2.mixins import SingleContentDetailViewMixin, ModalFormView
+from zds.tutorialv2.models import SHAREABLE_LINK_TYPES
+from zds.tutorialv2.models.database import PublishableContent
+from zds.tutorialv2.models.shareable_links import ShareableLink
+
+
+class ListShareableLinks(LoginRequiredMixin, SingleContentDetailViewMixin, TemplateView):
+ template_name = "tutorialv2/view/list_shareable_links.html"
+ authorized_for_staff = False
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ content = self.get_object()
+ context["active_links_and_forms"] = self.get_active_links_and_forms(content)
+ context["expired_links_and_forms"] = self.get_expired_links_and_forms(content)
+ context["inactive_links_and_forms"] = self.get_inactive_links_and_forms(content)
+ context["create_form"] = ShareableLinkForm()
+ return context
+
+ def get_active_links_and_forms(self, content):
+ return self.get_links_and_forms(ShareableLink.objects.active_and_for_content(content))
+
+ def get_expired_links_and_forms(self, content):
+ return self.get_links_and_forms(ShareableLink.objects.expired_and_for_content(content))
+
+ def get_inactive_links_and_forms(self, content):
+ return self.get_links_and_forms(ShareableLink.objects.inactive_and_for_content(content))
+
+ @staticmethod
+ def get_links_and_forms(links):
+ edit_forms = [ShareableLinkForm(initial=initial) for initial in links.values()]
+ return list(zip(links, edit_forms))
+
+
+class ShareableLinkForm(forms.Form):
+ description = forms.CharField(label=_("Description"), initial=_("Lien de partage"))
+ expiration = forms.DateTimeField(
+ label=_("Date d'expiration (laisser vide pour une durée illimitée)"),
+ widget=forms.DateInput(attrs={"type": "date"}),
+ required=False,
+ )
+ type = forms.ChoiceField(choices=SHAREABLE_LINK_TYPES)
+
+
+class PermissionMixin:
+ http_method_names = ["post"]
+
+ @method_decorator(login_required)
+ def dispatch(self, *args, **kwargs):
+ if self.request.user not in self.content.authors.all():
+ raise PermissionDenied
+ self.redirect_url = reverse("content:list-shareable-links", kwargs={"pk": self.content.pk})
+ return super().dispatch(*args, *kwargs)
+
+
+class ContentMixin(PermissionMixin):
+ def dispatch(self, *args, **kwargs):
+ self.content = get_object_or_404(PublishableContent, pk=self.kwargs["pk"])
+ return super().dispatch(*args, *kwargs)
+
+
+class LinkMixin(PermissionMixin):
+ def dispatch(self, *args, **kwargs):
+ self.link = get_object_or_404(ShareableLink, id=self.kwargs["id"])
+ self.content = self.link.content
+ return super().dispatch(*args, *kwargs)
+
+
+class CreateShareableLink(ContentMixin, ModalFormView):
+ http_method_names = ["post"]
+ form_class = ShareableLinkForm
+ modal_form = True
+
+ def form_invalid(self, form):
+ form.previous_page_url = self.redirect_url
+ return super().form_invalid(form)
+
+ def form_valid(self, form):
+ ShareableLink(
+ content=self.content,
+ description=form.cleaned_data["description"],
+ expiration=form.cleaned_data["expiration"],
+ type=form.cleaned_data["type"],
+ ).save()
+ self.success_url = self.redirect_url
+ messages.success(self.request, _("Le lien a été créé."))
+ return super().form_valid(form)
+
+
+class EditShareableLink(LinkMixin, ModalFormView):
+ form_class = ShareableLinkForm
+ modal_form = True
+
+ def form_invalid(self, form):
+ form.previous_page_url = self.redirect_url
+ return super().form_invalid(form)
+
+ def form_valid(self, form):
+ self.link.description = form.cleaned_data["description"]
+ self.link.expiration = form.cleaned_data["expiration"]
+ self.link.type = form.cleaned_data["type"]
+ self.link.save()
+ self.success_url = self.redirect_url
+ messages.success(self.request, "Le lien a été modifié.")
+ return super().form_valid(form)
+
+
+class DeactivateShareableLink(LinkMixin, View):
+ def post(self, *args, **kwargs):
+ self.link.deactivate()
+ messages.success(self.request, "Le lien a été désactivé.")
+ return redirect(self.redirect_url)
+
+
+class ReactivateShareableLink(LinkMixin, View):
+ def post(self, *args, **kwargs):
+ self.link.reactivate()
+ messages.success(self.request, "Le lien a été réactivé.")
+ return redirect(self.redirect_url)
+
+
+class DeleteShareableLink(LinkMixin, View):
+ def post(self, *args, **kwargs):
+ self.link.delete()
+ messages.success(self.request, "Le lien a été supprimé.")
+ return redirect(self.redirect_url)
diff --git a/zds/tutorialv2/views/shared_content.py b/zds/tutorialv2/views/shared_content.py
new file mode 100644
index 0000000000..628847e117
--- /dev/null
+++ b/zds/tutorialv2/views/shared_content.py
@@ -0,0 +1,79 @@
+from django.core.exceptions import PermissionDenied
+from django.shortcuts import get_object_or_404
+from django.urls import reverse
+
+from zds.tutorialv2.models.database import PublishableContent
+from zds.tutorialv2.models.shareable_links import ShareableLink
+from zds.tutorialv2.models.versioned import VersionedContent
+from zds.tutorialv2.views.display.config import ViewConfig
+from zds.tutorialv2.views.display.container import ContainerBaseView
+from zds.tutorialv2.views.display.content import ContentBaseView
+
+
+class ConfigForSharedView(ViewConfig):
+ def __init__(self, user, content: PublishableContent, versioned_content: VersionedContent):
+ super().__init__(user, content, versioned_content)
+ self.beta_actions.enabled = False
+ self.draft_actions.enabled = False
+ self.online_config.enabled = False
+ self.info_config.enabled = False
+ self.public_actions.enabled = False
+ self.administration_actions.enabled = False
+ self.validation_actions.enabled = False
+
+
+class ContentSharedView(ContentBaseView):
+ must_be_author = False
+ authorized_for_all = True
+ sha = None
+
+ def get_object(self, queryset=None):
+ self.link = get_object_or_404(ShareableLink, id=self.kwargs["id"])
+ if not self.link.active:
+ raise PermissionDenied
+
+ self.content = self.link.content
+
+ # TODO manage DRAFT/BETA/etc.
+ self.versioned_content = self.content.load_version_or_404(sha=self.content.sha_draft)
+
+ return self.content
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["display_config"] = ConfigForSharedView(self.request.user, self.object, self.versioned_object)
+ context["link"] = self.link
+ context["content"] = self.versioned_content
+ return context
+
+ def get_base_url(self):
+ route_parameters = {"id": self.link.id}
+ url = reverse("content:shareable-link-view", kwargs=route_parameters)
+ return url
+
+
+class ContainerSharedView(ContainerBaseView):
+ must_be_author = False
+ authorized_for_all = True
+
+ def get_object(self, queryset=None):
+ self.link = get_object_or_404(ShareableLink, id=self.kwargs["id"])
+ if not self.link.active:
+ raise PermissionDenied
+
+ self.content = self.link.content
+
+ # TODO manage DRAFT/BETA/etc.
+ self.versioned_content = self.content.load_version_or_404(sha=self.content.sha_draft)
+
+ return self.content
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["display_config"] = ConfigForSharedView(self.request.user, self.object, self.versioned_object)
+ return context
+
+ def get_base_url(self):
+ route_parameters = {"id": self.link.id}
+ url = reverse("content:shareable-link-view", kwargs=route_parameters)
+ return url