refactor upload code and better handle missing thumbnail
Run Tests / Run Tests (push) Successful in 34s
Details
Run Tests / Run Tests (push) Successful in 34s
Details
This commit is contained in:
parent
bb8c29ea91
commit
a27298cff0
|
@ -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
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
@ -51,7 +59,7 @@ def document_thumbnail(request: HttpRequest, pk: str):
|
||||||
image.thumbnail((thumb_max_width, thumb_height)) # Adjust size as needed
|
image.thumbnail((thumb_max_width, thumb_height)) # Adjust size as needed
|
||||||
|
|
||||||
# if the image format is not jpeg, convert as needed
|
# if the image format is not jpeg, convert as needed
|
||||||
if image.format!= "JPEG":
|
if image.format != "JPEG":
|
||||||
image = image.convert("RGB")
|
image = image.convert("RGB")
|
||||||
|
|
||||||
# Save the thumbnail to a BytesIO object
|
# Save the thumbnail to a BytesIO object
|
||||||
|
|
|
@ -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
|
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
|
@ -2,4 +2,4 @@
|
||||||
[pytest]
|
[pytest]
|
||||||
DJANGO_SETTINGS_MODULE = penparse.test_settings
|
DJANGO_SETTINGS_MODULE = penparse.test_settings
|
||||||
# -- recommended but optional:
|
# -- recommended but optional:
|
||||||
python_files = tests.py test/test_*.py *_tests.py
|
python_files = tests.py test/test_*.py *_tests.py
|
||||||
|
|
38
uv.lock
38
uv.lock
|
@ -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 },
|
||||||
|
]
|
||||||
|
|
Loading…
Reference in New Issue