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 0000000..6eecb6b Binary files /dev/null and b/src/attachments/tests/resources/docker-logo.png differ 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: