diff --git a/penparse/webui/test/test_thumbnail_view.py b/penparse/webui/test/test_thumbnail_view.py index 832d1a6..c51efb8 100644 --- a/penparse/webui/test/test_thumbnail_view.py +++ b/penparse/webui/test/test_thumbnail_view.py @@ -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 \ No newline at end of file diff --git a/penparse/webui/views/__init__.py b/penparse/webui/views/__init__.py index 2f2572d..2facf45 100644 --- a/penparse/webui/views/__init__.py +++ b/penparse/webui/views/__init__.py @@ -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 - - diff --git a/penparse/webui/views/thumbnail.py b/penparse/webui/views/thumbnail.py index f2f3277..16b317f 100644 --- a/penparse/webui/views/thumbnail.py +++ b/penparse/webui/views/thumbnail.py @@ -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 diff --git a/penparse/webui/views/upload.py b/penparse/webui/views/upload.py new file mode 100644 index 0000000..7c69fe9 --- /dev/null +++ b/penparse/webui/views/upload.py @@ -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 diff --git a/pyproject.toml b/pyproject.toml index e3b700f..32c5ea6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", ] diff --git a/pytest.ini b/pytest.ini index 7f3edd1..ed4898d 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,4 +2,4 @@ [pytest] DJANGO_SETTINGS_MODULE = penparse.test_settings # -- recommended but optional: -python_files = tests.py test/test_*.py *_tests.py \ No newline at end of file +python_files = tests.py test/test_*.py *_tests.py diff --git a/uv.lock b/uv.lock index c51dfb8..e265582 100644 --- a/uv.lock +++ b/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 }, ] +[[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 }, +]