diff --git a/pyproject.toml b/pyproject.toml index 0dcc434..c91fc7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "wagtaildraftsharing" -version = "0.1.2" +version = "0.1.3" description = "Share wagtail drafts with private URLs." readme = "README.md" requires-python = ">=3.9" diff --git a/wagtaildraftsharing/models.py b/wagtaildraftsharing/models.py index d30e4a0..ed71d7f 100644 --- a/wagtaildraftsharing/models.py +++ b/wagtaildraftsharing/models.py @@ -5,9 +5,43 @@ from django.db import models from django.urls import reverse from django.utils.html import format_html +from django.utils.timezone import timedelta +from wagtail.log_actions import log + +from wagtaildraftsharing.actions import WAGTAILDRAFTSHARING_CREATE_SHARING_LINK +from wagtaildraftsharing.utils import tz_aware_utc_now from . import settings as draftsharing_settings +max_age = draftsharing_settings.WAGTAILDRAFTSHARING_MAX_AGE + + +class WagtaildraftsharingLinkManager(models.Manager): + def get_or_create_for_revision(self, *, revision, user): + key = uuid.uuid4() + if max_age > 0: + active_until = tz_aware_utc_now() + timedelta(seconds=max_age) + else: + active_until = None + sharing_link, created = WagtaildraftsharingLink.objects.get_or_create( + revision=revision, + defaults={ + "key": key, + "created_by": user, + "active_until": active_until, + }, + ) + if created: + log( + instance=revision.content_object, + action=WAGTAILDRAFTSHARING_CREATE_SHARING_LINK, + user=user, + revision=revision, + data={"revision": revision.id}, + ) + + return sharing_link + class WagtaildraftsharingLink(models.Model): key = models.UUIDField( @@ -36,6 +70,8 @@ class WagtaildraftsharingLink(models.Model): editable=False, ) + objects = WagtaildraftsharingLinkManager() + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/wagtaildraftsharing/tests/test_models.py b/wagtaildraftsharing/tests/test_models.py index 357e31f..c1c7f6c 100644 --- a/wagtaildraftsharing/tests/test_models.py +++ b/wagtaildraftsharing/tests/test_models.py @@ -1,11 +1,71 @@ +import datetime from textwrap import dedent from unittest.mock import patch import wagtail +from django.contrib.auth.models import User from django.test import TestCase +from django.utils.timezone import is_aware +from freezegun import freeze_time from wagtail_factories import PageFactory -from ..models import WagtaildraftsharingLink +import wagtaildraftsharing.models +from wagtaildraftsharing.models import WagtaildraftsharingLink + +FROZEN_TIME_ISOFORMATTED = "2024-01-02 12:34:56.123456+00:00" + + +class TestWagtaildraftsharingLinkManager(TestCase): + def setUp(self): + self.test_user = User.objects.create( + username="test", email="testuser@example.com" + ) + + def create_revision(self): + page = PageFactory() + + # create the first revision + page.save_revision().publish() + + old_title = page.title + new_title = f"New {old_title}" + page.title = new_title + + # create the second revision with a new title + page.save_revision().publish() + + page.refresh_from_db() + earliest_revision = page.revisions.earliest("created_at") + return earliest_revision + + @freeze_time(FROZEN_TIME_ISOFORMATTED) + def test_create_sharing_link_view__max_age_from_settings(self): + frozen_time = datetime.datetime.fromisoformat(FROZEN_TIME_ISOFORMATTED) + + # Ensure we've got a level playing field: that the time is TZ-aware + if not is_aware(frozen_time): + self.fail("frozen_time was a naive datetime but it should not be") + + max_ages_and_expected_expiries = ( + (300, frozen_time + datetime.timedelta(seconds=300)), + (1250000, frozen_time + datetime.timedelta(seconds=1250000)), + (-1, None), + ) + + for max_age, expected_expiry in max_ages_and_expected_expiries: + with self.subTest(max_age=max_age, expected_expiry=expected_expiry): + with patch.object(wagtaildraftsharing.models, "max_age", max_age): + revision = self.create_revision() + + link = WagtaildraftsharingLink.objects.get_or_create_for_revision( + revision=revision, + user=self.test_user, + ) + + assert link.active_until == expected_expiry, ( + link.active_until, + expected_expiry, + ) class TestWagtaildraftsharingLinkModel(TestCase): diff --git a/wagtaildraftsharing/tests/test_views.py b/wagtaildraftsharing/tests/test_views.py index f233e09..8baedd5 100644 --- a/wagtaildraftsharing/tests/test_views.py +++ b/wagtaildraftsharing/tests/test_views.py @@ -1,18 +1,14 @@ import datetime import json -from unittest.mock import patch from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from django.http import Http404 from django.test import RequestFactory, TestCase from django.urls import reverse -from django.utils.timezone import is_aware from django.utils.timezone import now as timezone_now -from freezegun import freeze_time from wagtail_factories import PageFactory -import wagtaildraftsharing.views from wagtaildraftsharing.models import WagtaildraftsharingLink from wagtaildraftsharing.views import CreateSharingLinkView, SharingLinkView @@ -107,37 +103,6 @@ def test_create_sharing_link_view__anonymous_user_not_allowed(self): ) self.assertEqual(WagtaildraftsharingLink.objects.count(), 0) - @freeze_time(FROZEN_TIME_ISOFORMATTED) - def test_create_sharing_link_view__max_age_from_settings(self): - frozen_time = datetime.datetime.fromisoformat(FROZEN_TIME_ISOFORMATTED) - - # Ensure we've got a level playing field: that the time is TZ-aware - if not is_aware(frozen_time): - self.fail("frozen_time was a naive datetime but it should not be") - - max_ages_and_expected_expiries = ( - (300, frozen_time + datetime.timedelta(seconds=300)), - (1250000, frozen_time + datetime.timedelta(seconds=1250000)), - (-1, None), - ) - - for max_age, expected_expiry in max_ages_and_expected_expiries: - with self.subTest(max_age=max_age, expected_expiry=expected_expiry): - with patch.object(wagtaildraftsharing.views, "max_age", max_age): - revision = self.create_revision() - request = self.factory.post("/create/", {"revision": revision.id}) - request.user = self.superuser - - response = CreateSharingLinkView.as_view()(request) - self.assertEqual(response.status_code, 200) - - link = WagtaildraftsharingLink.objects.last() - - assert link.active_until == expected_expiry, ( - link.active_until, - expected_expiry, - ) - class SharingLinkViewTests(TestCase): @classmethod diff --git a/wagtaildraftsharing/utils.py b/wagtaildraftsharing/utils.py new file mode 100644 index 0000000..f7671ea --- /dev/null +++ b/wagtaildraftsharing/utils.py @@ -0,0 +1,13 @@ +from datetime import timezone + +from django.utils.timezone import is_aware, make_aware +from django.utils.timezone import now as timezone_now + + +def tz_aware_utc_now(): + now = timezone_now() + # Depending on your version of Django and/or setting.TZ_NOW, timezone_now() + # may not actually be TZ aware, but we always want it to be for these links + if not is_aware(now): + now = make_aware(now, timezone.utc) + return now diff --git a/wagtaildraftsharing/views.py b/wagtaildraftsharing/views.py index 58ed6ed..a31b87f 100644 --- a/wagtaildraftsharing/views.py +++ b/wagtaildraftsharing/views.py @@ -1,39 +1,20 @@ -import uuid -from datetime import timezone - from django.http import Http404, JsonResponse from django.shortcuts import get_object_or_404 from django.utils.decorators import method_decorator -from django.utils.timezone import is_aware, make_aware, timedelta -from django.utils.timezone import now as timezone_now from django.views.generic import CreateView from wagtail.admin.auth import user_has_any_page_permission, user_passes_test from wagtail.admin.views.generic.preview import PreviewRevision -from wagtail.log_actions import log from wagtail.models import Page, Revision -from wagtaildraftsharing.actions import WAGTAILDRAFTSHARING_CREATE_SHARING_LINK from wagtaildraftsharing.forms import CreateWagtaildraftsharingLinkForm from wagtaildraftsharing.models import WagtaildraftsharingLink - -from . import settings as draftsharing_settings - -max_age = draftsharing_settings.WAGTAILDRAFTSHARING_MAX_AGE - - -def _tz_aware_utc_now(): - now = timezone_now() - # Depending on your version of Django and/or setting.TZ_NOW, timezone_now() - # may not actually be TZ aware, but we always want it to be for these links - if not is_aware(now): - now = make_aware(now, timezone.utc) - return now +from wagtaildraftsharing.utils import tz_aware_utc_now class SharingLinkView(PreviewRevision): def setup(self, request, *args, **kwargs): key = kwargs.pop("key") - now = _tz_aware_utc_now() + now = tz_aware_utc_now() sharing_link = get_object_or_404( WagtaildraftsharingLink, @@ -59,28 +40,10 @@ class CreateSharingLinkView(CreateView): form_class = CreateWagtaildraftsharingLinkForm def form_valid(self, form): - revision = form.cleaned_data["revision"] - key = uuid.uuid4() - if max_age > 0: - active_until = _tz_aware_utc_now() + timedelta(seconds=max_age) - else: - active_until = None - sharing_link, created = WagtaildraftsharingLink.objects.get_or_create( - revision=revision, - defaults={ - "key": key, - "created_by": self.request.user, - "active_until": active_until, - }, + sharing_link = WagtaildraftsharingLink.objects.get_or_create_for_revision( + revision=form.cleaned_data["revision"], + user=self.request.user, ) - if created: - log( - instance=revision.content_object, - action=WAGTAILDRAFTSHARING_CREATE_SHARING_LINK, - user=self.request.user, - revision=revision, - data={"revision": revision.id}, - ) return JsonResponse({"url": sharing_link.url}) def form_invalid(self, form):