Allow sharing drafts publicly with a draft key

This commit is contained in:
Gabriel Augendre 2020-12-27 20:00:41 +01:00
parent 6b52f6b2b2
commit d1eced4f1f
No known key found for this signature in database
GPG key ID: 1E693F4CE4AEE7B4
6 changed files with 70 additions and 4 deletions

View file

@ -36,6 +36,7 @@ class ArticleAdmin(admin.ModelAdmin):
("created_at", "updated_at"),
"views_count",
"has_code",
"draft_public_url",
]
},
),
@ -57,6 +58,7 @@ class ArticleAdmin(admin.ModelAdmin):
"views_count",
"status",
"published_at",
"draft_public_url",
]
prepopulated_fields = {"slug": ("title",)}
change_form_template = "articles/article_change_form.html"
@ -80,7 +82,17 @@ class ArticleAdmin(admin.ModelAdmin):
messages.success(request, f"{len(queryset)} articles unpublished.")
unpublish.short_description = "Unpublish selected articles"
actions = [publish, unpublish]
def refresh_draft_key(self, request, queryset):
if not request.user.has_perm("articles.change_article"):
messages.warning(request, "You're not allowed to do this.")
return
for article in queryset:
article.refresh_draft_key()
messages.success(request, f"{len(queryset)} draft keys refreshed.")
refresh_draft_key.short_description = "Refresh draft key of selected articles"
actions = [publish, unpublish, refresh_draft_key]
class Media:
css = {"all": ("admin_articles.css",)}

View file

@ -0,0 +1,20 @@
# Generated by Django 3.1.4 on 2020-12-27 18:43
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("articles", "0025_article_custom_css"),
]
operations = [
migrations.AddField(
model_name="article",
name="draft_key",
field=models.UUIDField(default=uuid.uuid4),
),
]

View file

@ -1,4 +1,5 @@
import re
import uuid
from functools import cached_property
import html2text
@ -12,6 +13,7 @@ from django.utils import timezone
from markdown.extensions.codehilite import CodeHiliteExtension
from articles.markdown import LazyLoadingImageExtension
from articles.utils import build_full_absolute_url
class User(AbstractUser):
@ -47,6 +49,7 @@ class Article(AdminUrlMixin, models.Model):
has_code = models.BooleanField(default=False, blank=True)
is_home = models.BooleanField(default=False, blank=True)
custom_css = models.TextField(blank=True)
draft_key = models.UUIDField(default=uuid.uuid4)
class Meta:
ordering = ["-published_at"]
@ -108,3 +111,12 @@ class Article(AdminUrlMixin, models.Model):
if not self.slug:
self.slug = slugify(self.title)
return super().save(*args, **kwargs)
@property
def draft_public_url(self):
url = self.get_absolute_url() + f"?draft_key={self.draft_key}"
return build_full_absolute_url(request=None, url=url)
def refresh_draft_key(self):
self.draft_key = uuid.uuid4()
self.save()

View file

@ -1,3 +1,5 @@
import uuid
import pytest
from django.core.management import call_command
from django.utils import timezone
@ -40,6 +42,7 @@ def unpublished_article(author: User) -> Article:
published_at=None,
slug="some-draft-article-slug",
content="## some draft article markdown\n\n[a draft article link](https://article.com)",
draft_key=uuid.uuid4(),
)

View file

@ -41,6 +41,10 @@ def test_access_article_by_slug(client: Client, published_article: Article):
def _test_access_article_by_slug(client: Client, item: Article):
res = client.get(reverse("article-detail", kwargs={"slug": item.slug}))
_assert_article_is_rendered(item, res)
def _assert_article_is_rendered(item: Article, res):
assert res.status_code == 200
content = res.content.decode("utf-8")
assert item.title in content
@ -57,6 +61,17 @@ def test_anonymous_cant_access_draft_detail(
assert res.status_code == 404
@pytest.mark.django_db
def test_anonymous_can_access_draft_detail_with_key(
client: Client, unpublished_article: Article
):
res = client.get(
reverse("article-detail", kwargs={"slug": unpublished_article.slug})
+ f"?draft_key={unpublished_article.draft_key}"
)
_assert_article_is_rendered(unpublished_article, res)
@pytest.mark.django_db
def test_user_can_access_draft_detail(
client: Client, author: User, unpublished_article: Article

View file

@ -48,10 +48,14 @@ class ArticleDetailView(generic.DetailView):
template_name = "articles/article_detail.html"
def get_queryset(self):
key = self.request.GET.get("draft_key")
if key:
return Article.objects.filter(draft_key=key)
queryset = super().get_queryset()
if self.request.user.is_authenticated:
return queryset
return queryset.filter(status=Article.PUBLISHED)
if not self.request.user.is_authenticated:
queryset = queryset.filter(status=Article.PUBLISHED)
return queryset
def get_object(self, queryset=None) -> Article:
obj = super().get_object(queryset) # type: Article