Delete log files on instance deletion

Delete log files from the file system when the corresponding model
instance is deleted. Add a corresponding test case.

Issue: HIC-271
Change-Id: I44709c78839091272ccc9b931e7cbe7b3947dff9
diff --git a/crashreports/models.py b/crashreports/models.py
index c54f7f1..25aa452 100644
--- a/crashreports/models.py
+++ b/crashreports/models.py
@@ -6,6 +6,7 @@
 
 from django.db import models, transaction
 from django.contrib.auth.models import User
+from django.dispatch import receiver
 from taggit.managers import TaggableManager
 
 
@@ -163,6 +164,16 @@
         )
 
 
+@receiver(models.signals.post_delete, sender=LogFile)
+def auto_delete_file_on_delete(sender, instance, **kwargs):
+    """Delete the file from the filesystem on deletion of the db instance."""
+    # pylint: disable=unused-argument
+
+    if instance.logfile:
+        if os.path.isfile(instance.logfile.path):
+            instance.logfile.delete(save=False)
+
+
 class HeartBeat(models.Model):
     """A heartbeat that was sent by a device."""
 
diff --git a/crashreports/tests/test_rest_api_logfiles.py b/crashreports/tests/test_rest_api_logfiles.py
index afe8abb..9b8b6b2 100644
--- a/crashreports/tests/test_rest_api_logfiles.py
+++ b/crashreports/tests/test_rest_api_logfiles.py
@@ -12,7 +12,12 @@
 
 from rest_framework import status
 
-from crashreports.models import crashreport_file_name, Device
+from crashreports.models import (
+    crashreport_file_name,
+    Device,
+    Crashreport,
+    LogFile,
+)
 from crashreports.tests.utils import HiccupCrashreportsAPITestCase, Dummy
 
 
@@ -22,6 +27,7 @@
 
     LIST_CREATE_URL = "api_v1_crashreports"
     PUT_LOGFILE_URL = "api_v1_putlogfile_for_device_id"
+    POST_LOGFILE_URL = "api_v1_logfiles_by_id"
 
     def setUp(self):
         """Call the super setup method and register a device."""
@@ -102,6 +108,30 @@
         """Test upload of logfiles as Fairphone staff user."""
         self._test_logfile_upload(self.fp_staff_client, self.device_uuid)
 
+    def test_logfile_deletion(self):
+        """Test deletion of logfile instances."""
+        # Create a user, device and crashreport with logfile
+        device = Dummy.create_dummy_device(Dummy.create_dummy_user())
+        crashreport = Dummy.create_dummy_report(Crashreport, device)
+        logfile, logfile_path = Dummy.create_dummy_log_file_with_actual_file(
+            crashreport
+        )
+
+        # Assert that the crashreport and logfile have been created
+        self.assertEqual(Crashreport.objects.count(), 1)
+        self.assertEqual(LogFile.objects.count(), 1)
+        self.assertTrue(os.path.isfile(logfile_path))
+
+        # Delete the logfile
+        response = self.fp_staff_client.delete(
+            reverse(self.POST_LOGFILE_URL, args=[logfile.id])
+        )
+        self.assertEqual(status.HTTP_204_NO_CONTENT, response.status_code)
+
+        # Assert that the logfile has been deleted
+        self.assertEqual(LogFile.objects.count(), 0)
+        self.assertFalse(os.path.isfile(logfile_path))
+
     def tearDown(self):
         """Remove the file and directories that were created for the test."""
         shutil.rmtree(settings.MEDIA_ROOT)
diff --git a/crashreports/tests/utils.py b/crashreports/tests/utils.py
index 9ee316a..fcaa2a3 100644
--- a/crashreports/tests/utils.py
+++ b/crashreports/tests/utils.py
@@ -1,17 +1,25 @@
 """Utility functions shared by all crashreports tests."""
 
 import os
+import shutil
 import zipfile
 from datetime import date, datetime
 from typing import Optional
 
 import pytz
+from django.conf import settings
 from django.contrib.auth.models import User, Group
 from django.urls import reverse
 from rest_framework import status
 from rest_framework.test import APITestCase, APIClient
 
-from crashreports.models import Crashreport, Device, HeartBeat, LogFile
+from crashreports.models import (
+    Crashreport,
+    Device,
+    HeartBeat,
+    LogFile,
+    crashreport_file_name,
+)
 from hiccup.allauth_adapters import FP_STAFF_GROUP_NAME
 
 
@@ -102,11 +110,6 @@
         }
     )
 
-    DEFAULT_DUMMY_LOG_FILE_VALUES = {
-        "logfile_type": "last_kmsg",
-        "logfile": "test_logfile.zip",
-    }
-
     DEFAULT_DUMMY_LOG_FILE_NAME = "dmesg.log"
 
     CRASH_TYPE_TO_BOOT_REASON_MAP = {
@@ -115,12 +118,18 @@
         "other": "whatever",
     }
 
+    DEFAULT_DUMMY_LOG_FILE_FILENAME = "test_logfile.zip"
     DEFAULT_DUMMY_LOG_FILE_DIRECTORY = os.path.join("resources", "test")
 
     DEFAULT_DUMMY_LOG_FILE_PATH = os.path.join(
-        DEFAULT_DUMMY_LOG_FILE_DIRECTORY, "test_logfile.zip"
+        DEFAULT_DUMMY_LOG_FILE_DIRECTORY, DEFAULT_DUMMY_LOG_FILE_FILENAME
     )
 
+    DEFAULT_DUMMY_LOG_FILE_VALUES = {
+        "logfile_type": "last_kmsg",
+        "logfile": DEFAULT_DUMMY_LOG_FILE_FILENAME,
+    }
+
     @staticmethod
     def _update_copy(original, update):
         """Merge fields of update into a copy of original."""
@@ -272,6 +281,38 @@
         return entity
 
     @staticmethod
+    def create_dummy_log_file_with_actual_file(crashreport, **kwargs):
+        """Create a dummy log file instance along with a file.
+
+        The dummy instance is created and saved to the database. The log file
+        is copied to the respective location in the media directory.
+
+        Args:
+            crashreport: The crashreport that the log file belongs to.
+            **kwargs: Optional arguments to extend/overwrite the default values.
+
+        Returns: The created log file instance and the path to the copied file.
+
+        """
+        logfile = Dummy.create_dummy_log_file(crashreport, **kwargs)
+        logfile_filename = os.path.basename(logfile.logfile.path)
+        test_logfile_path = os.path.join(
+            settings.MEDIA_ROOT,
+            crashreport_file_name(logfile, logfile_filename),
+        )
+        logfile.logfile = test_logfile_path
+        logfile.save()
+
+        os.makedirs(os.path.dirname(test_logfile_path))
+        shutil.copy(
+            os.path.join(
+                Dummy.DEFAULT_DUMMY_LOG_FILE_DIRECTORY, logfile_filename
+            ),
+            test_logfile_path,
+        )
+        return logfile, test_logfile_path
+
+    @staticmethod
     def read_logfile_contents(path_to_zipfile, logfile_name):
         """Read bytes of a zipped logfile."""
         archive = zipfile.ZipFile(path_to_zipfile, "r")