diff --git a/todo/templates/todo/task_detail.html b/todo/templates/todo/task_detail.html
index 1a3fd84..8eca361 100644
--- a/todo/templates/todo/task_detail.html
+++ b/todo/templates/todo/task_detail.html
@@ -131,6 +131,7 @@
Uploaded |
By |
Type |
+ Remove |
@@ -140,6 +141,12 @@
{{ attachment.timestamp }} |
{{ attachment.added_by.get_full_name }} |
{{ attachment.extension.lower }} |
+
+
+ |
{% endfor %}
diff --git a/todo/urls.py b/todo/urls.py
index 0573def..a539616 100644
--- a/todo/urls.py
+++ b/todo/urls.py
@@ -56,6 +56,11 @@ urlpatterns = [
'task//',
views.task_detail,
name='task_detail'),
+
+ path(
+ 'attachment/remove//',
+ views.remove_attachment,
+ name='remove_attachment'),
]
if HAS_TASK_MERGE:
diff --git a/todo/utils.py b/todo/utils.py
index 0016a53..23c0e01 100644
--- a/todo/utils.py
+++ b/todo/utils.py
@@ -1,13 +1,14 @@
import email.utils
-import time
import logging
+import os
+import time
from django.conf import settings
from django.contrib.sites.models import Site
from django.core import mail
from django.template.loader import render_to_string
-from todo.models import Comment, Task
+from todo.models import Attachment, Comment, Task
log = logging.getLogger(__name__)
@@ -153,3 +154,19 @@ def toggle_task_completed(task_id: int) -> bool:
except Task.DoesNotExist:
log.info(f"Task {task_id} not found.")
return False
+
+
+def remove_attachment_file(attachment_id: int) -> bool:
+ """Delete an Attachment object and its corresponding file from the filesystem."""
+ try:
+ attachment = Attachment.objects.get(id=attachment_id)
+ if attachment.file:
+ if os.path.isfile(attachment.file.path):
+ os.remove(attachment.file.path)
+
+ attachment.delete()
+ return True
+
+ except Attachment.DoesNotExist:
+ log.info(f"Attachment {attachment_id} not found.")
+ return False
diff --git a/todo/views/__init__.py b/todo/views/__init__.py
index 4c5777d..fb6c891 100644
--- a/todo/views/__init__.py
+++ b/todo/views/__init__.py
@@ -2,10 +2,11 @@ from todo.views.add_list import add_list # noqa: F401
from todo.views.del_list import del_list # noqa: F401
from todo.views.delete_task import delete_task # noqa: F401
from todo.views.external_add import external_add # noqa: F401
+from todo.views.import_csv import import_csv # noqa: F401
from todo.views.list_detail import list_detail # noqa: F401
from todo.views.list_lists import list_lists # noqa: F401
+from todo.views.remove_attachment import remove_attachment # noqa: F401
from todo.views.reorder_tasks import reorder_tasks # noqa: F401
from todo.views.search import search # noqa: F401
from todo.views.task_detail import task_detail # noqa: F401
from todo.views.toggle_done import toggle_done # noqa: F401
-from todo.views.import_csv import import_csv # noqa: F401
diff --git a/todo/views/remove_attachment.py b/todo/views/remove_attachment.py
new file mode 100644
index 0000000..75b235f
--- /dev/null
+++ b/todo/views/remove_attachment.py
@@ -0,0 +1,41 @@
+from django.contrib import messages
+from django.contrib.auth.decorators import login_required
+from django.core.exceptions import PermissionDenied
+from django.http import HttpResponse
+from django.shortcuts import get_object_or_404, redirect
+from django.urls import reverse
+
+from todo.models import Attachment
+from todo.utils import remove_attachment_file
+
+
+@login_required
+def remove_attachment(request, attachment_id: int) -> HttpResponse:
+ """Delete a previously posted attachment object and its corresponding file
+ from the filesystem, permissions allowing.
+ """
+
+ if request.method == "POST":
+ attachment = get_object_or_404(Attachment, pk=attachment_id)
+
+ redir_url = reverse(
+ "todo:task_detail",
+ kwargs={"task_id": attachment.task.id},
+ )
+
+ # Permissions
+ if not (
+ attachment.task.task_list.group in request.user.groups.all()
+ or request.user.is_superuser
+ ):
+ raise PermissionDenied
+
+ if remove_attachment_file(attachment.id):
+ messages.success(request, f"Attachment {attachment.id} removed.")
+ else:
+ messages.error(request, f"Sorry, there was a problem deleting attachment {attachment.id}.")
+
+ return redirect(redir_url)
+
+ else:
+ raise PermissionDenied