Repository: openfun/marsha

Issue:
Impossible to delete a classroom with documents
## Bug Report

**Problematic Behavior**

If the permissions allow me to delete a classroom and this classroom contains at least one document, I can't delete it because there is a database constraint.

**Expected behavior/code**

When I want to delete a classroom and the permission allows me to do it, the deletion should happen.

**Steps to Reproduce**
1. Create a classroom
2. Upload a document
3. Wait it is ready
4. Delete the classroom

**Additional context/Screenshots**

```
("Cannot delete some instances of model 'Classroom' because they are referenced through protected foreign keys: ClassroomDocument.", {{'deleted': None, 'deleted_by_cascade': False, 'id': UUID('xxx'), 'created_on': datetime.datetime(2022, 11, 24, 14, 40 , 46, 860422, tzinfo=datetime.timezone.utc), 'updated_on': datetime.datetime(2022, 11, 24, 14, 41, 30, 448922, tzinfo=datetime.timezone.utc), 'uploaded_on': datetime.datetime(2022, 11, 24, 14, 40, 47, tzinfo=datetime.timezone.utc), 'upload_state': 'ready', 'classroom': UUID('xxx'), 'filename': 'foo.pdf', 'is_default': False}, {'deleted': None, 'deleted_by_cascade': False, 'id': UUID('xxx'), 'created_on': datetime.datetime(2022, 10, 4, 10, 7, 18, 731135, tzinfo=datetime.timezone.utc), 'updated_on': datetime.datetime(2022, 10, 4, 10, 7, 23, 766429, tzinfo=datetime.timezone.utc), 'uploaded_on': datetime.datetime(2022, 10, 4, 10, 7, 18, tzinfo=datetime.timezone.utc), 'upload_state':'ready', 'classroom': UUID('fxxx'), 'filename': 'bar.pdf', 'is_default': True}})
```



Discussion:

Resolving patch:
diff --git a/src/backend/marsha/bbb/migrations/0014_alter_classroomrecording_classroom.py b/src/backend/marsha/bbb/migrations/0014_alter_classroomrecording_classroom.py
new file mode 100644
--- /dev/null
+++ b/src/backend/marsha/bbb/migrations/0014_alter_classroomrecording_classroom.py
@@ -0,0 +1,35 @@
+# Generated by Django 4.1.9 on 2023-05-10 14:08
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("bbb", "0013_classroom_tools_parameters"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="classroomrecording",
+            name="classroom",
+            field=models.ForeignKey(
+                help_text="classroom to which this recording belongs",
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="recordings",
+                to="bbb.classroom",
+                verbose_name="classroom recording",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="classroomdocument",
+            name="classroom",
+            field=models.ForeignKey(
+                help_text="clasroom to which this document belongs",
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="classroom_documents",
+                to="bbb.classroom",
+                verbose_name="classroom document",
+            ),
+        ),
+    ]
diff --git a/src/backend/marsha/bbb/models.py b/src/backend/marsha/bbb/models.py
--- a/src/backend/marsha/bbb/models.py
+++ b/src/backend/marsha/bbb/models.py
@@ -184,8 +184,8 @@ class ClassroomDocument(UploadableFileMixin, BaseModel):
         related_name="classroom_documents",
         verbose_name=_("classroom document"),
         help_text=_("clasroom to which this document belongs"),
-        # don't allow hard deleting a classroom if it still contains a document
-        on_delete=models.PROTECT,
+        # Delete all documents belonging to this classroom
+        on_delete=models.CASCADE,
     )
 
     filename = models.CharField(
@@ -251,8 +251,8 @@ class ClassroomRecording(BaseModel):
         related_name="recordings",
         verbose_name=_("classroom recording"),
         help_text=_("classroom to which this recording belongs"),
-        # don't allow hard deleting a classroom if it still contains a recording
-        on_delete=models.PROTECT,
+        # Delete all records belonging to this classroom
+        on_delete=models.CASCADE,
     )
 
     record_id = models.CharField(


Test patch:
diff --git a/src/backend/marsha/bbb/tests/api/classroom/test_delete.py b/src/backend/marsha/bbb/tests/api/classroom/test_delete.py
--- a/src/backend/marsha/bbb/tests/api/classroom/test_delete.py
+++ b/src/backend/marsha/bbb/tests/api/classroom/test_delete.py
@@ -3,8 +3,12 @@
 
 from django.test import TestCase, override_settings
 
-from marsha.bbb.factories import ClassroomFactory
-from marsha.bbb.models import Classroom
+from marsha.bbb.factories import (
+    ClassroomDocumentFactory,
+    ClassroomFactory,
+    ClassroomRecordingFactory,
+)
+from marsha.bbb.models import Classroom, ClassroomDocument, ClassroomRecording
 from marsha.core.factories import (
     OrganizationAccessFactory,
     PlaylistAccessFactory,
@@ -146,3 +150,52 @@ def test_api_classroom_delete_user_access_token_playlist_admin(self):
 
         self.assertEqual(response.status_code, 204)
         self.assertEqual(Classroom.objects.count(), 0)
+
+    def test_api_classroom_delete_with_records(self):
+        """An organization administrator should be able to delete a classroom
+        having video records."""
+        organization_access = OrganizationAccessFactory(role=ADMINISTRATOR)
+        playlist = PlaylistFactory(organization=organization_access.organization)
+        classroom = ClassroomFactory(playlist=playlist)
+        ClassroomRecordingFactory(classroom=classroom)
+
+        jwt_token = UserAccessTokenFactory(user=organization_access.user)
+
+        self.assertEqual(Classroom.objects.count(), 1)
+        self.assertEqual(ClassroomRecording.objects.count(), 1)
+
+        response = self.client.delete(
+            f"/api/classrooms/{classroom.id}/",
+            {
+                "playlist": str(playlist.id),
+            },
+            HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
+            content_type="application/json",
+        )
+        self.assertEqual(response.status_code, 204)
+        self.assertEqual(Classroom.objects.count(), 0)
+        self.assertEqual(ClassroomRecording.objects.count(), 0)
+
+    def test_api_classroom_delete_with_document(self):
+        """An organization administrator should be able to delete a classroom having documents."""
+        organization_access = OrganizationAccessFactory(role=ADMINISTRATOR)
+        playlist = PlaylistFactory(organization=organization_access.organization)
+        classroom = ClassroomFactory(playlist=playlist)
+        ClassroomDocumentFactory(classroom=classroom)
+
+        jwt_token = UserAccessTokenFactory(user=organization_access.user)
+
+        self.assertEqual(Classroom.objects.count(), 1)
+        self.assertEqual(ClassroomDocument.objects.count(), 1)
+
+        response = self.client.delete(
+            f"/api/classrooms/{classroom.id}/",
+            {
+                "playlist": str(playlist.id),
+            },
+            HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
+            content_type="application/json",
+        )
+        self.assertEqual(response.status_code, 204)
+        self.assertEqual(Classroom.objects.count(), 0)
+        self.assertEqual(ClassroomDocument.objects.count(), 0)