refactor upload code and better handle missing thumbnail
Run Tests / Run Tests (push) Successful in 34s Details

This commit is contained in:
James Ravenscroft 2024-12-09 09:53:44 +00:00
parent bb8c29ea91
commit a27298cff0
7 changed files with 135 additions and 43 deletions

View File

@ -1,17 +1,13 @@
import os import os
from django.conf import settings from django.conf import settings
import pytest import pytest
import requests
from django.urls import reverse from django.urls import reverse
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse
import requests
from ..models import ImageMemo, User from ..models import ImageMemo, User
import logging
thumbnail_dir = getattr(settings, "THUMBNAIL_DIR", "thumbnails") thumbnail_dir = getattr(settings, "THUMBNAIL_DIR", "thumbnails")
@pytest.fixture @pytest.fixture
def cleanup_uploaded_files(): def cleanup_uploaded_files():
files_to_cleanup = [] files_to_cleanup = []
@ -29,11 +25,9 @@ def cleanup_uploaded_files():
image_memo.delete() image_memo.delete()
def get_test_filepath(basename): def get_test_filepath(basename):
return os.path.join(os.path.dirname(__file__), "data", basename) return os.path.join(os.path.dirname(__file__), "data", basename)
@pytest.mark.django_db @pytest.mark.django_db
def test_document_thumbnail_incorrect_user(client, cleanup_uploaded_files): def test_document_thumbnail_incorrect_user(client, cleanup_uploaded_files):
user1 = User.objects.create_user(email="user1@test.com", password="password1") user1 = User.objects.create_user(email="user1@test.com", password="password1")
@ -72,7 +66,6 @@ def test_document_thumbnail_incorrect_user(client, cleanup_uploaded_files):
assert response.status_code == 200 assert response.status_code == 200
assert response["Content-Type"] == "image/jpeg" assert response["Content-Type"] == "image/jpeg"
@pytest.mark.django_db @pytest.mark.django_db
def test_document_thumbnail_golden_path_png(client, cleanup_uploaded_files): def test_document_thumbnail_golden_path_png(client, cleanup_uploaded_files):
user = User.objects.create_user(email="user@test.com", password="password") user = User.objects.create_user(email="user@test.com", password="password")
@ -101,7 +94,6 @@ def test_document_thumbnail_golden_path_png(client, cleanup_uploaded_files):
assert response.status_code == 200 assert response.status_code == 200
assert response["Content-Type"] == "image/jpeg" assert response["Content-Type"] == "image/jpeg"
@pytest.mark.django_db @pytest.mark.django_db
def test_document_thumbnail_golden_path_gif(client, cleanup_uploaded_files): def test_document_thumbnail_golden_path_gif(client, cleanup_uploaded_files):
user = User.objects.create_user(email="user@test.com", password="password") user = User.objects.create_user(email="user@test.com", password="password")
@ -129,3 +121,28 @@ def test_document_thumbnail_golden_path_gif(client, cleanup_uploaded_files):
assert response.status_code == 200 assert response.status_code == 200
assert response["Content-Type"] == "image/jpeg" assert response["Content-Type"] == "image/jpeg"
@pytest.mark.django_db
def test_document_thumbnail_file_missing(client, cleanup_uploaded_files, caplog):
user = User.objects.create_user(email="user@test.com", password="password")
image_memo = ImageMemo.objects.create(
author=user,
image="non_existent_file.jpg",
)
cleanup_uploaded_files.append((image_memo, ""))
assert client.login(
username=user.email, password="password"
), "Failed to log in test client as user"
url = reverse("document_thumbnail", kwargs={"pk": str(image_memo.id)})
with caplog.at_level(logging.WARNING):
response = client.get(url)
assert response.status_code == 404
assert response.content == b"Document not found"
assert "The file associated with this document does not exist" in caplog.text

View File

@ -11,11 +11,11 @@ from ..models import ImageMemo
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from .thumbnail import document_thumbnail from .thumbnail import document_thumbnail
from .register import register from .register import register
from .upload import upload_document
def index(request): def index(request):
@ -23,7 +23,16 @@ def index(request):
return render(request, "index.html") return render(request, "index.html")
__all__ = ["index", "document_thumbnail", "register", "dashboard", "settings", "view_document", "download_document", "upload_document"] __all__ = [
"index",
"document_thumbnail",
"register",
"dashboard",
"settings",
"view_document",
"download_document",
"upload_document",
]
@login_required @login_required
@ -50,33 +59,3 @@ def view_document(request):
@login_required @login_required
def download_document(request): def download_document(request):
return render(request, "document.html") return render(request, "document.html")
@login_required
def upload_document(request):
if request.method == "POST" and request.FILES.get("document"):
uploaded_file = request.FILES["document"]
# Check if the file is an image
if not uploaded_file.content_type.startswith("image/"):
messages.error(request, "Please upload an image file.")
return redirect("dashboard")
# Save the image
file_name = default_storage.save(
f"uploads/{uploaded_file.name}", ContentFile(uploaded_file.read())
)
# Create an ImageMemo instance
image_memo = ImageMemo(
image=file_name,
content="", # You can add initial content here if needed
author=request.user, # Assuming the user is authenticated
)
image_memo.save()
messages.success(request, "Image uploaded successfully!")
return redirect("dashboard") # Redirect to dashboard or appropriate page

View File

@ -1,5 +1,7 @@
import os import os
from loguru import logger
from PIL import Image from PIL import Image
from io import BytesIO from io import BytesIO
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
@ -10,6 +12,7 @@ from django.contrib.auth.decorators import login_required
from ..models import ImageMemo from ..models import ImageMemo
@login_required @login_required
def document_thumbnail(request: HttpRequest, pk: str): def document_thumbnail(request: HttpRequest, pk: str):
"""Given a document uuid, look it up, ensure that it belongs to the current user and respond with a thumbnail""" """Given a document uuid, look it up, ensure that it belongs to the current user and respond with a thumbnail"""
@ -18,6 +21,7 @@ def document_thumbnail(request: HttpRequest, pk: str):
document = ImageMemo.objects.filter(id=pk, author__id=request.user.id).first() document = ImageMemo.objects.filter(id=pk, author__id=request.user.id).first()
if not document: if not document:
logger.debug(f"No memo found for user={request.user.id} and memo_id={pk}")
return HttpResponse(content="Document not found", status=404) return HttpResponse(content="Document not found", status=404)
# look up the file on disk # look up the file on disk
@ -34,6 +38,10 @@ def document_thumbnail(request: HttpRequest, pk: str):
if not os.path.exists(thumbnail_dir): if not os.path.exists(thumbnail_dir):
os.makedirs(thumbnail_dir) os.makedirs(thumbnail_dir)
if not os.path.exists(document.image.name):
logger.warning("The file associated with this document does not exist")
return HttpResponse(content="Document not found", status=404)
# Open the image using PIL # Open the image using PIL
image = Image.open(default_storage.open(document.image.name)) image = Image.open(default_storage.open(document.image.name))

View File

@ -0,0 +1,48 @@
import logging
import os
from django.contrib import messages
from django.shortcuts import redirect
from django.core.files.storage import default_storage
from django.core.files.base import ContentFile
from ..models import ImageMemo
from django.http import HttpRequest
from uuid import uuid4
from django.contrib.auth.decorators import login_required
logger = logging.getLogger(__name__)
@login_required
def upload_document(request: HttpRequest):
if request.method == "POST" and request.FILES.get("document"):
uploaded_file = request.FILES["document"]
# Check if the file is an image
if not uploaded_file.content_type.startswith("image/"):
messages.error(request, "Please upload an image file.")
return redirect("dashboard")
name,ext = os.path.splitext(uploaded_file.name)
new_name = f"{uuid4().hex}-{ext}"
# Save the image
file_name = default_storage.save(
f"uploads/{new_name}", ContentFile(uploaded_file.read())
)
# Create an ImageMemo instance
image_memo = ImageMemo(
image=file_name,
content="", # You can add initial content here if needed
author=request.user, # Assuming the user is authenticated
)
image_memo.save()
messages.success(request, "Image uploaded successfully!")
return redirect("dashboard") # Redirect to dashboard or appropriate page

View File

@ -7,8 +7,10 @@ requires-python = ">=3.9"
dependencies = [ dependencies = [
"celery>=5.4.0", "celery>=5.4.0",
"django>=4.2.16", "django>=4.2.16",
"loguru>=0.7.3",
"pillow>=11.0.0", "pillow>=11.0.0",
"pytest-django>=4.9.0", "pytest-django>=4.9.0",
"pytest-loguru>=0.4.0",
"pytest>=8.3.4", "pytest>=8.3.4",
"requests>=2.32.3", "requests>=2.32.3",
] ]

38
uv.lock
View File

@ -261,6 +261,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/87/ec/7811a3cf9fdfee3ee88e54d08fcbc3fabe7c1b6e4059826c59d7b795651c/kombu-5.4.2-py3-none-any.whl", hash = "sha256:14212f5ccf022fc0a70453bb025a1dcc32782a588c49ea866884047d66e14763", size = 201349 }, { url = "https://files.pythonhosted.org/packages/87/ec/7811a3cf9fdfee3ee88e54d08fcbc3fabe7c1b6e4059826c59d7b795651c/kombu-5.4.2-py3-none-any.whl", hash = "sha256:14212f5ccf022fc0a70453bb025a1dcc32782a588c49ea866884047d66e14763", size = 201349 },
] ]
[[package]]
name = "loguru"
version = "0.7.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "win32-setctime", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 },
]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "24.2" version = "24.2"
@ -277,9 +290,11 @@ source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "celery" }, { name = "celery" },
{ name = "django" }, { name = "django" },
{ name = "loguru" },
{ name = "pillow" }, { name = "pillow" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-django" }, { name = "pytest-django" },
{ name = "pytest-loguru" },
{ name = "requests" }, { name = "requests" },
] ]
@ -287,9 +302,11 @@ dependencies = [
requires-dist = [ requires-dist = [
{ name = "celery", specifier = ">=5.4.0" }, { name = "celery", specifier = ">=5.4.0" },
{ name = "django", specifier = ">=4.2.16" }, { name = "django", specifier = ">=4.2.16" },
{ name = "loguru", specifier = ">=0.7.3" },
{ name = "pillow", specifier = ">=11.0.0" }, { name = "pillow", specifier = ">=11.0.0" },
{ name = "pytest", specifier = ">=8.3.4" }, { name = "pytest", specifier = ">=8.3.4" },
{ name = "pytest-django", specifier = ">=4.9.0" }, { name = "pytest-django", specifier = ">=4.9.0" },
{ name = "pytest-loguru", specifier = ">=0.4.0" },
{ name = "requests", specifier = ">=2.32.3" }, { name = "requests", specifier = ">=2.32.3" },
] ]
@ -425,6 +442,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/47/fe/54f387ee1b41c9ad59e48fb8368a361fad0600fe404315e31a12bacaea7d/pytest_django-4.9.0-py3-none-any.whl", hash = "sha256:1d83692cb39188682dbb419ff0393867e9904094a549a7d38a3154d5731b2b99", size = 23723 }, { url = "https://files.pythonhosted.org/packages/47/fe/54f387ee1b41c9ad59e48fb8368a361fad0600fe404315e31a12bacaea7d/pytest_django-4.9.0-py3-none-any.whl", hash = "sha256:1d83692cb39188682dbb419ff0393867e9904094a549a7d38a3154d5731b2b99", size = 23723 },
] ]
[[package]]
name = "pytest-loguru"
version = "0.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "loguru" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/f2/8ca6c8780e714fbfd35d7dcc772af99310272a01457b0887c90c75f2ec52/pytest_loguru-0.4.0.tar.gz", hash = "sha256:0d9e4e72ae9bfd92f774c666e7353766af11b0b78edd59c290e89be116050f03", size = 6696 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/33/ef/b0c2e96e3508bca8d1874e39789d541cd7f4731b38bcf9c7098f0b882001/pytest_loguru-0.4.0-py3-none-any.whl", hash = "sha256:3cc7b9c6b22cb158209ccbabf0d678dacd3f3c7497d6f46f1c338c13bee1ac77", size = 3886 },
]
[[package]] [[package]]
name = "python-dateutil" name = "python-dateutil"
version = "2.9.0.post0" version = "2.9.0.post0"
@ -553,3 +582,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 },
] ]
[[package]]
name = "win32-setctime"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083 },
]