Allow sharing drafts publicly with a draft key
This commit is contained in:
parent
6b52f6b2b2
commit
d1eced4f1f
6 changed files with 70 additions and 4 deletions
|
@ -36,6 +36,7 @@ class ArticleAdmin(admin.ModelAdmin):
|
||||||
("created_at", "updated_at"),
|
("created_at", "updated_at"),
|
||||||
"views_count",
|
"views_count",
|
||||||
"has_code",
|
"has_code",
|
||||||
|
"draft_public_url",
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -57,6 +58,7 @@ class ArticleAdmin(admin.ModelAdmin):
|
||||||
"views_count",
|
"views_count",
|
||||||
"status",
|
"status",
|
||||||
"published_at",
|
"published_at",
|
||||||
|
"draft_public_url",
|
||||||
]
|
]
|
||||||
prepopulated_fields = {"slug": ("title",)}
|
prepopulated_fields = {"slug": ("title",)}
|
||||||
change_form_template = "articles/article_change_form.html"
|
change_form_template = "articles/article_change_form.html"
|
||||||
|
@ -80,7 +82,17 @@ class ArticleAdmin(admin.ModelAdmin):
|
||||||
messages.success(request, f"{len(queryset)} articles unpublished.")
|
messages.success(request, f"{len(queryset)} articles unpublished.")
|
||||||
|
|
||||||
unpublish.short_description = "Unpublish selected articles"
|
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:
|
class Media:
|
||||||
css = {"all": ("admin_articles.css",)}
|
css = {"all": ("admin_articles.css",)}
|
||||||
|
|
20
articles/migrations/0026_article_draft_key.py
Normal file
20
articles/migrations/0026_article_draft_key.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,4 +1,5 @@
|
||||||
import re
|
import re
|
||||||
|
import uuid
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
import html2text
|
import html2text
|
||||||
|
@ -12,6 +13,7 @@ from django.utils import timezone
|
||||||
from markdown.extensions.codehilite import CodeHiliteExtension
|
from markdown.extensions.codehilite import CodeHiliteExtension
|
||||||
|
|
||||||
from articles.markdown import LazyLoadingImageExtension
|
from articles.markdown import LazyLoadingImageExtension
|
||||||
|
from articles.utils import build_full_absolute_url
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractUser):
|
class User(AbstractUser):
|
||||||
|
@ -47,6 +49,7 @@ class Article(AdminUrlMixin, models.Model):
|
||||||
has_code = models.BooleanField(default=False, blank=True)
|
has_code = models.BooleanField(default=False, blank=True)
|
||||||
is_home = models.BooleanField(default=False, blank=True)
|
is_home = models.BooleanField(default=False, blank=True)
|
||||||
custom_css = models.TextField(blank=True)
|
custom_css = models.TextField(blank=True)
|
||||||
|
draft_key = models.UUIDField(default=uuid.uuid4)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["-published_at"]
|
ordering = ["-published_at"]
|
||||||
|
@ -108,3 +111,12 @@ class Article(AdminUrlMixin, models.Model):
|
||||||
if not self.slug:
|
if not self.slug:
|
||||||
self.slug = slugify(self.title)
|
self.slug = slugify(self.title)
|
||||||
return super().save(*args, **kwargs)
|
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()
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import uuid
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -40,6 +42,7 @@ def unpublished_article(author: User) -> Article:
|
||||||
published_at=None,
|
published_at=None,
|
||||||
slug="some-draft-article-slug",
|
slug="some-draft-article-slug",
|
||||||
content="## some draft article markdown\n\n[a draft article link](https://article.com)",
|
content="## some draft article markdown\n\n[a draft article link](https://article.com)",
|
||||||
|
draft_key=uuid.uuid4(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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):
|
def _test_access_article_by_slug(client: Client, item: Article):
|
||||||
res = client.get(reverse("article-detail", kwargs={"slug": item.slug}))
|
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
|
assert res.status_code == 200
|
||||||
content = res.content.decode("utf-8")
|
content = res.content.decode("utf-8")
|
||||||
assert item.title in content
|
assert item.title in content
|
||||||
|
@ -57,6 +61,17 @@ def test_anonymous_cant_access_draft_detail(
|
||||||
assert res.status_code == 404
|
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
|
@pytest.mark.django_db
|
||||||
def test_user_can_access_draft_detail(
|
def test_user_can_access_draft_detail(
|
||||||
client: Client, author: User, unpublished_article: Article
|
client: Client, author: User, unpublished_article: Article
|
||||||
|
|
|
@ -48,10 +48,14 @@ class ArticleDetailView(generic.DetailView):
|
||||||
template_name = "articles/article_detail.html"
|
template_name = "articles/article_detail.html"
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
key = self.request.GET.get("draft_key")
|
||||||
|
if key:
|
||||||
|
return Article.objects.filter(draft_key=key)
|
||||||
|
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
if self.request.user.is_authenticated:
|
if not self.request.user.is_authenticated:
|
||||||
return queryset
|
queryset = queryset.filter(status=Article.PUBLISHED)
|
||||||
return queryset.filter(status=Article.PUBLISHED)
|
return queryset
|
||||||
|
|
||||||
def get_object(self, queryset=None) -> Article:
|
def get_object(self, queryset=None) -> Article:
|
||||||
obj = super().get_object(queryset) # type: Article
|
obj = super().get_object(queryset) # type: Article
|
||||||
|
|
Reference in a new issue