add deletion + tests and processing status
Run Tests / Run Tests (push) Successful in 36s
Details
Run Tests / Run Tests (push) Successful in 36s
Details
This commit is contained in:
parent
7176e8ce09
commit
db7794dece
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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"]
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
# ...
|
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in New Issue