Add cache to attachment urls
This commit is contained in:
parent
871d28f864
commit
d7a7e65805
9 changed files with 174 additions and 6 deletions
59
src/articles/migrations/0034_replace_attachment_urls.py
Normal file
59
src/articles/migrations/0034_replace_attachment_urls.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
# Generated by Django 4.1.1 on 2022-10-01 06:55
|
||||||
|
from django.apps.registry import Apps
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
|
def replace_with_wrapper_url(
|
||||||
|
apps: Apps, schema_editor: BaseDatabaseSchemaEditor
|
||||||
|
) -> None:
|
||||||
|
Attachment = apps.get_model("attachments", "Attachment")
|
||||||
|
Article = apps.get_model("articles", "Article")
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
attachments = Attachment.objects.using(db_alias).all()
|
||||||
|
modified = []
|
||||||
|
for article in Article.objects.using(db_alias).all():
|
||||||
|
for attachment in attachments:
|
||||||
|
article.content = article.content.replace(
|
||||||
|
attachment.original_file.url,
|
||||||
|
reverse("attachments:original", kwargs={"pk": attachment.pk}),
|
||||||
|
)
|
||||||
|
if attachment.processed_file:
|
||||||
|
article.content = article.content.replace(
|
||||||
|
attachment.processed_file.url,
|
||||||
|
reverse("attachments:processed", kwargs={"pk": attachment.pk}),
|
||||||
|
)
|
||||||
|
modified.append(article)
|
||||||
|
Article.objects.using(db_alias).bulk_update(modified, ["content"])
|
||||||
|
|
||||||
|
|
||||||
|
def replace_with_file_url(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None:
|
||||||
|
Attachment = apps.get_model("attachments", "Attachment")
|
||||||
|
Article = apps.get_model("articles", "Article")
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
attachments = Attachment.objects.using(db_alias).all()
|
||||||
|
modified = []
|
||||||
|
for article in Article.objects.using(db_alias).all():
|
||||||
|
for attachment in attachments:
|
||||||
|
article.content = article.content.replace(
|
||||||
|
reverse("attachments:original", kwargs={"pk": attachment.pk}),
|
||||||
|
attachment.original_file.url,
|
||||||
|
)
|
||||||
|
if attachment.processed_file:
|
||||||
|
article.content = article.content.replace(
|
||||||
|
reverse("attachments:processed", kwargs={"pk": attachment.pk}),
|
||||||
|
attachment.processed_file.url,
|
||||||
|
)
|
||||||
|
modified.append(article)
|
||||||
|
Article.objects.using(db_alias).bulk_update(modified, ["content"])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("articles", "0033_alter_article_options"),
|
||||||
|
("attachments", "0008_attachment_updated_at"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [migrations.RunPython(replace_with_wrapper_url, replace_with_file_url)]
|
|
@ -43,18 +43,16 @@ class AttachmentAdmin(admin.ModelAdmin):
|
||||||
def processed_file_url(self, instance: Attachment) -> str:
|
def processed_file_url(self, instance: Attachment) -> str:
|
||||||
if instance.processed_file:
|
if instance.processed_file:
|
||||||
return format_html(
|
return format_html(
|
||||||
'{0} <a class="copy-button" data-to-copy="{1}" href="#">📋</a>',
|
'{0} <a class="copy-button" data-to-copy="{0}" href="#">📋</a>',
|
||||||
instance.processed_file.url,
|
instance.processed_file_url,
|
||||||
instance.processed_file.url,
|
|
||||||
)
|
)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def original_file_url(self, instance: Attachment) -> str:
|
def original_file_url(self, instance: Attachment) -> str:
|
||||||
if instance.original_file:
|
if instance.original_file:
|
||||||
return format_html(
|
return format_html(
|
||||||
'{0} <a class="copy-button" data-to-copy="{1}" href="#">📋</a>',
|
'{0} <a class="copy-button" data-to-copy="{0}" href="#">📋</a>',
|
||||||
instance.original_file.url,
|
instance.original_file_url,
|
||||||
instance.original_file.url,
|
|
||||||
)
|
)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
18
src/attachments/migrations/0008_attachment_updated_at.py
Normal file
18
src/attachments/migrations/0008_attachment_updated_at.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 4.1.1 on 2022-10-01 06:13
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("attachments", "0007_auto_20201201_1917"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="attachment",
|
||||||
|
name="updated_at",
|
||||||
|
field=models.DateTimeField(auto_now=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -11,6 +11,7 @@ from django.core.files import File
|
||||||
from django.core.handlers.wsgi import WSGIRequest
|
from django.core.handlers.wsgi import WSGIRequest
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.fields.files import FieldFile
|
from django.db.models.fields.files import FieldFile
|
||||||
|
from django.urls import reverse
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from articles.utils import build_full_absolute_url
|
from articles.utils import build_full_absolute_url
|
||||||
|
@ -35,6 +36,7 @@ class Attachment(models.Model):
|
||||||
original_file = AbsoluteUrlFileField()
|
original_file = AbsoluteUrlFileField()
|
||||||
processed_file = AbsoluteUrlFileField(blank=True, null=True)
|
processed_file = AbsoluteUrlFileField(blank=True, null=True)
|
||||||
open_graph_image = models.BooleanField(blank=True, default=False)
|
open_graph_image = models.BooleanField(blank=True, default=False)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
objects = AttachmentManager()
|
objects = AttachmentManager()
|
||||||
|
|
||||||
|
@ -48,6 +50,16 @@ class Attachment(models.Model):
|
||||||
self.processed_file = None # type: ignore
|
self.processed_file = None # type: ignore
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def original_file_url(self) -> str:
|
||||||
|
return reverse("attachments:original", kwargs={"pk": self.pk})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def processed_file_url(self) -> str | None:
|
||||||
|
if self.processed_file:
|
||||||
|
return reverse("attachments:processed", kwargs={"pk": self.pk})
|
||||||
|
return None
|
||||||
|
|
||||||
def save(self, *args: Any, **kwargs: Any) -> None:
|
def save(self, *args: Any, **kwargs: Any) -> None:
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
BIN
src/attachments/tests/resources/docker-logo.png
Normal file
BIN
src/attachments/tests/resources/docker-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
45
src/attachments/tests/test_views.py
Normal file
45
src/attachments/tests/test_views.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.core.files import File
|
||||||
|
from django.test.client import Client
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from attachments.models import Attachment
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.block_network()
|
||||||
|
@pytest.fixture()
|
||||||
|
def attachment(db: None) -> Attachment:
|
||||||
|
# This path manipulation is required to make the test run from this directory
|
||||||
|
# or from upper in the hierarchy (e.g.: settings.BASE_DIR)
|
||||||
|
original_path = Path(__file__).parent / "resources" / "image.png"
|
||||||
|
original_path = original_path.relative_to(Path.cwd())
|
||||||
|
processed_path = Path(__file__).parent / "resources" / "docker-logo.png"
|
||||||
|
processed_path = processed_path.relative_to(Path.cwd())
|
||||||
|
with original_path.open("rb") as of, processed_path.open("rb") as pf:
|
||||||
|
original_file = File(of)
|
||||||
|
processed_file = File(pf)
|
||||||
|
attachment = Attachment.objects.create(
|
||||||
|
description="Docker logo",
|
||||||
|
original_file=original_file,
|
||||||
|
processed_file=processed_file,
|
||||||
|
)
|
||||||
|
attachment.save()
|
||||||
|
return attachment
|
||||||
|
|
||||||
|
|
||||||
|
def test_view_original(attachment: Attachment, client: Client) -> None:
|
||||||
|
url = reverse("attachments:original", kwargs={"pk": attachment.pk})
|
||||||
|
res = client.get(url)
|
||||||
|
assert res.status_code == 302
|
||||||
|
assert res.url == attachment.original_file.url
|
||||||
|
assert "Last-Modified" in res.headers
|
||||||
|
|
||||||
|
|
||||||
|
def test_view_processed(attachment: Attachment, client: Client) -> None:
|
||||||
|
url = reverse("attachments:processed", kwargs={"pk": attachment.pk})
|
||||||
|
res = client.get(url)
|
||||||
|
assert res.status_code == 302
|
||||||
|
assert res.url == attachment.processed_file.url
|
||||||
|
assert "Last-Modified" in res.headers
|
10
src/attachments/urls.py
Normal file
10
src/attachments/urls.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from attachments import views
|
||||||
|
|
||||||
|
app_name = "attachments"
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("<int:pk>/original/", views.get_original, name="original"),
|
||||||
|
path("<int:pk>/processed/", views.get_processed, name="processed"),
|
||||||
|
]
|
25
src/attachments/views.py
Normal file
25
src/attachments/views.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from django.core.handlers.wsgi import WSGIRequest
|
||||||
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.views.decorators.http import last_modified
|
||||||
|
|
||||||
|
from attachments.models import Attachment
|
||||||
|
|
||||||
|
|
||||||
|
def get_updated_at(request: WSGIRequest, pk: int) -> datetime.datetime:
|
||||||
|
attachment = get_object_or_404(Attachment, pk=pk)
|
||||||
|
return attachment.updated_at
|
||||||
|
|
||||||
|
|
||||||
|
@last_modified(get_updated_at)
|
||||||
|
def get_original(request: WSGIRequest, pk: int) -> HttpResponse:
|
||||||
|
attachment = get_object_or_404(Attachment, pk=pk)
|
||||||
|
return HttpResponseRedirect(attachment.original_file.url)
|
||||||
|
|
||||||
|
|
||||||
|
@last_modified(get_updated_at)
|
||||||
|
def get_processed(request: WSGIRequest, pk: int) -> HttpResponse:
|
||||||
|
attachment = get_object_or_404(Attachment, pk=pk)
|
||||||
|
return HttpResponseRedirect(attachment.processed_file.url)
|
|
@ -32,6 +32,7 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path("", include("articles.urls")),
|
path("", include("articles.urls")),
|
||||||
|
path("attachments/", include("attachments.urls")),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
|
Reference in a new issue