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
from django.conf import settings
import pytest
import requests
from django.urls import reverse
from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse
import requests
from ..models import ImageMemo, User
import logging
thumbnail_dir = getattr(settings, "THUMBNAIL_DIR", "thumbnails")
@pytest.fixture
def cleanup_uploaded_files():
files_to_cleanup = []
@ -29,11 +25,9 @@ def cleanup_uploaded_files():
image_memo.delete()
def get_test_filepath(basename):
return os.path.join(os.path.dirname(__file__), "data", basename)
@pytest.mark.django_db
def test_document_thumbnail_incorrect_user(client, cleanup_uploaded_files):
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["Content-Type"] == "image/jpeg"
@pytest.mark.django_db
def test_document_thumbnail_golden_path_png(client, cleanup_uploaded_files):
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["Content-Type"] == "image/jpeg"
@pytest.mark.django_db
def test_document_thumbnail_golden_path_gif(client, cleanup_uploaded_files):
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["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
logger = logging.getLogger(__name__)
from .thumbnail import document_thumbnail
from .register import register
from .upload import upload_document
def index(request):
@ -23,7 +23,16 @@ def index(request):
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
@ -50,33 +59,3 @@ def view_document(request):
@login_required
def download_document(request):
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
from loguru import logger
from PIL import Image
from io import BytesIO
from django.core.files.storage import default_storage
@ -10,6 +12,7 @@ from django.contrib.auth.decorators import login_required
from ..models import ImageMemo
@login_required
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"""
@ -18,6 +21,7 @@ def document_thumbnail(request: HttpRequest, pk: str):
document = ImageMemo.objects.filter(id=pk, author__id=request.user.id).first()
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)
# look up the file on disk
@ -34,6 +38,10 @@ def document_thumbnail(request: HttpRequest, pk: str):
if not os.path.exists(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
image = Image.open(default_storage.open(document.image.name))
@ -51,7 +59,7 @@ def document_thumbnail(request: HttpRequest, pk: str):
image.thumbnail((thumb_max_width, thumb_height)) # Adjust size as needed
# if the image format is not jpeg, convert as needed
if image.format!= "JPEG":
if image.format != "JPEG":
image = image.convert("RGB")
# Save the thumbnail to a BytesIO object

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 = [
"celery>=5.4.0",
"django>=4.2.16",
"loguru>=0.7.3",
"pillow>=11.0.0",
"pytest-django>=4.9.0",
"pytest-loguru>=0.4.0",
"pytest>=8.3.4",
"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 },
]
[[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]]
name = "packaging"
version = "24.2"
@ -277,9 +290,11 @@ source = { virtual = "." }
dependencies = [
{ name = "celery" },
{ name = "django" },
{ name = "loguru" },
{ name = "pillow" },
{ name = "pytest" },
{ name = "pytest-django" },
{ name = "pytest-loguru" },
{ name = "requests" },
]
@ -287,9 +302,11 @@ dependencies = [
requires-dist = [
{ name = "celery", specifier = ">=5.4.0" },
{ name = "django", specifier = ">=4.2.16" },
{ name = "loguru", specifier = ">=0.7.3" },
{ name = "pillow", specifier = ">=11.0.0" },
{ name = "pytest", specifier = ">=8.3.4" },
{ name = "pytest-django", specifier = ">=4.9.0" },
{ name = "pytest-loguru", specifier = ">=0.4.0" },
{ 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 },
]
[[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]]
name = "python-dateutil"
version = "2.9.0.post0"
@ -553,3 +582,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc
wheels = [
{ 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 },
]