From d7a7e65805ef4032643327ba7abf6f02701e5879 Mon Sep 17 00:00:00 2001 From: Gabriel Augendre Date: Sat, 1 Oct 2022 09:16:26 +0200 Subject: [PATCH] Add cache to attachment urls --- .../0034_replace_attachment_urls.py | 59 ++++++++++++++++++ src/attachments/admin.py | 10 ++- .../migrations/0008_attachment_updated_at.py | 18 ++++++ src/attachments/models.py | 12 ++++ .../tests/resources/docker-logo.png | Bin 0 -> 24928 bytes src/attachments/tests/test_views.py | 45 +++++++++++++ src/attachments/urls.py | 10 +++ src/attachments/views.py | 25 ++++++++ src/blog/urls.py | 1 + 9 files changed, 174 insertions(+), 6 deletions(-) create mode 100644 src/articles/migrations/0034_replace_attachment_urls.py create mode 100644 src/attachments/migrations/0008_attachment_updated_at.py create mode 100644 src/attachments/tests/resources/docker-logo.png create mode 100644 src/attachments/tests/test_views.py create mode 100644 src/attachments/urls.py create mode 100644 src/attachments/views.py diff --git a/src/articles/migrations/0034_replace_attachment_urls.py b/src/articles/migrations/0034_replace_attachment_urls.py new file mode 100644 index 0000000..7328375 --- /dev/null +++ b/src/articles/migrations/0034_replace_attachment_urls.py @@ -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)] diff --git a/src/attachments/admin.py b/src/attachments/admin.py index 14672a7..04d2f2a 100644 --- a/src/attachments/admin.py +++ b/src/attachments/admin.py @@ -43,18 +43,16 @@ class AttachmentAdmin(admin.ModelAdmin): def processed_file_url(self, instance: Attachment) -> str: if instance.processed_file: return format_html( - '{0} 📋', - instance.processed_file.url, - instance.processed_file.url, + '{0} 📋', + instance.processed_file_url, ) return "" def original_file_url(self, instance: Attachment) -> str: if instance.original_file: return format_html( - '{0} 📋', - instance.original_file.url, - instance.original_file.url, + '{0} 📋', + instance.original_file_url, ) return "" diff --git a/src/attachments/migrations/0008_attachment_updated_at.py b/src/attachments/migrations/0008_attachment_updated_at.py new file mode 100644 index 0000000..d5590a8 --- /dev/null +++ b/src/attachments/migrations/0008_attachment_updated_at.py @@ -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), + ), + ] diff --git a/src/attachments/models.py b/src/attachments/models.py index 24a8ff5..5505af0 100644 --- a/src/attachments/models.py +++ b/src/attachments/models.py @@ -11,6 +11,7 @@ from django.core.files import File from django.core.handlers.wsgi import WSGIRequest from django.db import models from django.db.models.fields.files import FieldFile +from django.urls import reverse from PIL import Image from articles.utils import build_full_absolute_url @@ -35,6 +36,7 @@ class Attachment(models.Model): original_file = AbsoluteUrlFileField() processed_file = AbsoluteUrlFileField(blank=True, null=True) open_graph_image = models.BooleanField(blank=True, default=False) + updated_at = models.DateTimeField(auto_now=True) objects = AttachmentManager() @@ -48,6 +50,16 @@ class Attachment(models.Model): self.processed_file = None # type: ignore 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: super().save(*args, **kwargs) diff --git a/src/attachments/tests/resources/docker-logo.png b/src/attachments/tests/resources/docker-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6eecb6bde89696b5c08b9ce30d56deeb6ff9300f GIT binary patch literal 24928 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 UP?Gn&!S5zDWvz!r|60BHAHQ9q%m4rY literal 0 HcmV?d00001 diff --git a/src/attachments/tests/test_views.py b/src/attachments/tests/test_views.py new file mode 100644 index 0000000..91fdde6 --- /dev/null +++ b/src/attachments/tests/test_views.py @@ -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 diff --git a/src/attachments/urls.py b/src/attachments/urls.py new file mode 100644 index 0000000..160b979 --- /dev/null +++ b/src/attachments/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from attachments import views + +app_name = "attachments" + +urlpatterns = [ + path("/original/", views.get_original, name="original"), + path("/processed/", views.get_processed, name="processed"), +] diff --git a/src/attachments/views.py b/src/attachments/views.py new file mode 100644 index 0000000..5d761f9 --- /dev/null +++ b/src/attachments/views.py @@ -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) diff --git a/src/blog/urls.py b/src/blog/urls.py index 72c2f90..3d6a2bc 100644 --- a/src/blog/urls.py +++ b/src/blog/urls.py @@ -32,6 +32,7 @@ urlpatterns = [ ), path("admin/", admin.site.urls), path("", include("articles.urls")), + path("attachments/", include("attachments.urls")), ] if settings.DEBUG: