add deletion + tests and processing status
Run Tests / Run Tests (push) Successful in 36s Details

This commit is contained in:
James Ravenscroft 2024-12-09 14:21:51 +00:00
parent 7176e8ce09
commit db7794dece
8 changed files with 171 additions and 22 deletions

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.16 on 2024-12-09 14:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('webui', '0003_imagememo_image_mimetype_alter_imagememo_image'),
]
operations = [
migrations.AddField(
model_name='imagememo',
name='status',
field=models.CharField(choices=[('pending', 'Pending'), ('processing', 'Processing'), ('done', 'Done'), ('error', 'Error')], default='pending', max_length=10),
),
]

View File

@ -38,6 +38,12 @@ class UserManager(BaseUserManager):
return self._create_user(email, password, **extra_fields) return self._create_user(email, password, **extra_fields)
class MemoStatus(models.TextChoices):
Pending = "pending"
Processing = "processing"
Done = "done"
Error = "error"
class ImageMemo(models.Model): class ImageMemo(models.Model):
"""Model definition for ImageMemo.""" """Model definition for ImageMemo."""
@ -54,6 +60,9 @@ class ImageMemo(models.Model):
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
status = models.CharField(max_length=10, choices=MemoStatus.choices, default=MemoStatus.Pending)
class Meta: class Meta:
ordering = ["-created_at"] ordering = ["-created_at"]

View File

@ -13,11 +13,27 @@
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{% for document in documents %} {% for document in documents %}
<div class="bg-white p-6 rounded-lg shadow border-2 border-dotted border-gray-300"> <div
<img src="{% url 'document_thumbnail' pk=document.id %}" alt="{{ document.title }} thumbnail" class="w-full h-48 object-cover mb-4 rounded"> class="bg-white p-6 rounded-lg shadow border-2 border-dotted border-gray-300"
<h3 class="text-xl font-semibold mb-2">{{ document.title }}</h3> >
<img
src="{% url 'document_thumbnail' pk=document.id %}"
alt="{{ document.title }} thumbnail"
class="w-full h-48 object-cover mb-4 rounded"
/>
<div class="flex justify-between items-center mb-2">
<h3 class="text-xl font-semibold">{{ document.title }}</h3>
<span
class="px-2 py-1 text-xs font-semibold rounded-full {% if document.status == 'pending' %} bg-gray-200 text-gray-800 {% elif document.status == 'processing' %} bg-blue-200 text-blue-800 {% elif document.status == 'done' %} bg-green-200 text-green-800 {% elif document.status == 'error' %} bg-red-200 text-red-800 {% endif %}"
>
{{ document.status|title }}
</span>
</div>
<p class="text-gray-600 mb-4"> <p class="text-gray-600 mb-4">
Analyzed on: {{ document.analysis_date }} Created: {{ document.created_at }}
</p>
<p class="text-gray-600 mb-4">
Last Updated: {{ document.updated_at }}
</p> </p>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<a <a
@ -27,15 +43,22 @@
> >
<a <a
href="{% url 'download_document' document.id %}" href="{% url 'download_document' document.id %}"
class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 transition duration-300" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-green-600 transition duration-300"
>Download</a >Export</a
> >
<form action="{% url 'delete_document' document.id %}" method="post" onsubmit="return confirm('Are you sure you want to delete this document?');"> <form
{% csrf_token %} action="{% url 'delete_document' document.id %}"
<button type="submit" class="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 transition duration-300"> method="post"
onsubmit="return confirm('Are you sure you want to delete this document?');"
>
{% csrf_token %}
<button
type="submit"
class="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 transition duration-300"
>
Delete Delete
</button> </button>
</form> </form>
</div> </div>
</div> </div>
{% empty %} {% empty %}
@ -47,15 +70,31 @@
</section> </section>
<section class="text-center"> <section class="text-center">
<h2 class="text-3xl font-bold text-gray-800 mb-6">Upload a New Document</h2> <h2 class="text-3xl font-bold text-gray-800 mb-6">Upload a New Document</h2>
<form action="{% url 'upload_document' %}" method="post" enctype="multipart/form-data"> <form
action="{% url 'upload_document' %}"
method="post"
enctype="multipart/form-data"
>
{% csrf_token %} {% csrf_token %}
<div class="mb-4"> <div class="mb-4">
<input type="file" name="document" id="document" class="hidden" accept=".png,.jpg,.jpeg"> <input
<label for="document" class="bg-blue-500 text-white px-6 py-3 rounded-lg hover:bg-blue-600 transition duration-300 cursor-pointer inline-block"> type="file"
name="document"
id="document"
class="hidden"
accept=".png,.jpg,.jpeg"
/>
<label
for="document"
class="bg-blue-500 text-white px-6 py-3 rounded-lg hover:bg-blue-600 transition duration-300 cursor-pointer inline-block"
>
Choose File Choose File
</label> </label>
</div> </div>
<button type="submit" class="bg-blue-500 text-white px-6 py-3 rounded-lg hover:bg-blue-600 transition duration-300"> <button
type="submit"
class="bg-blue-500 text-white px-6 py-3 rounded-lg hover:bg-blue-600 transition duration-300"
>
Upload Document Upload Document
</button> </button>
</form> </form>

View File

@ -0,0 +1,74 @@
import pytest
import uuid
from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse
from django.core.files.storage import default_storage
from ..models import ImageMemo, User
@pytest.mark.django_db
class TestDeleteDocument:
@pytest.fixture
def setup_users(self):
self.user1 = User.objects.create_user(
email="user1@test.com", password="password1"
)
self.user2 = User.objects.create_user(
email="user2@test.com", password="password2"
)
@pytest.fixture
def setup_document(self, setup_users):
img_content = b"fake image content"
test_image = SimpleUploadedFile(
name="test_image.jpg", content=img_content, content_type="image/jpeg"
)
self.image_memo = ImageMemo.objects.create(
author=self.user1,
image=test_image,
)
def test_delete_document_success(self, client, setup_document):
client.force_login(self.user1)
url = reverse("delete_document", kwargs={"pk": str(self.image_memo.id)})
response = client.post(url)
assert response.status_code == 302
assert response.url == reverse("dashboard")
assert not ImageMemo.objects.filter(id=self.image_memo.id).exists()
assert not default_storage.exists(self.image_memo.image.name)
def test_delete_nonexistent_document(self, client, setup_users):
client.force_login(self.user1)
url = reverse(
"delete_document", kwargs={"pk": "facade00-0000-4000-a000-000000000000"}
)
response = client.post(url)
assert response.status_code == 404
assert response.content == b"Document not found"
def test_delete_other_users_document(self, client, setup_document):
client.force_login(self.user2)
url = reverse("delete_document", kwargs={"pk": str(self.image_memo.id)})
response = client.post(url)
assert response.status_code == 404
assert response.content == b"Document not found"
assert ImageMemo.objects.filter(id=self.image_memo.id).exists()
def test_delete_document_missing_file(self, client, setup_document):
# Remove the file manually
default_storage.delete(self.image_memo.image.name)
client.force_login(self.user1)
url = reverse("delete_document", kwargs={"pk": str(self.image_memo.id)})
response = client.post(url)
assert response.status_code == 302
assert response.url == reverse("dashboard")
assert not ImageMemo.objects.filter(id=self.image_memo.id).exists()
# ...

View File

@ -9,10 +9,13 @@ urlpatterns = [
path("documents/upload", views.upload_document, name="upload_document"), path("documents/upload", views.upload_document, name="upload_document"),
path("documents/<str:pk>", views.view_document, name="view_document"), path("documents/<str:pk>", views.view_document, name="view_document"),
path( path(
"documents/<str:pk>/thumbnail", views.document_thumbnail, name="document_thumbnail" "documents/<str:pk>/thumbnail",
views.document_thumbnail,
name="document_thumbnail",
), ),
path( path(
"documents/<str:pk>/download", views.download_document, name="download_document" "documents/<str:pk>/download", views.download_document, name="download_document"
), ),
path("documents/<str:pk>/delete", views.delete_document, name="delete_document"),
path("auth/register", views.register, name="register"), path("auth/register", views.register, name="register"),
] ]

View File

@ -16,6 +16,7 @@ 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 from .upload import upload_document
from .delete import delete_document
def index(request): def index(request):
@ -32,6 +33,7 @@ __all__ = [
"view_document", "view_document",
"download_document", "download_document",
"upload_document", "upload_document",
"delete_document",
] ]

View File

@ -1,5 +1,4 @@
import logging from loguru import logger
import os
from django.contrib import messages from django.contrib import messages
from django.shortcuts import redirect from django.shortcuts import redirect
@ -12,9 +11,6 @@ from django.http import HttpRequest, HttpResponse
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
logger = logging.getLogger(__name__)
@login_required @login_required
def delete_document(request: HttpRequest, pk: str): def delete_document(request: HttpRequest, pk: str):
@ -26,6 +22,14 @@ def delete_document(request: HttpRequest, pk: str):
return HttpResponse(content="Document not found", status=404) return HttpResponse(content="Document not found", status=404)
# delete file from storage # delete file from storage
default_storage.delete(document.image.name) if default_storage.exists(document.image.name):
default_storage.delete(document.image.name)
else:
logger.warning(
"File {document.image.name} associated with doc {document.id} did not exist when we tried to delete it"
)
# delete document from database
document.delete()
return redirect("dashboard") return redirect("dashboard")