Repository: medtagger/MedTagger

Issue:
Cannot delete Scan or Slices from SQL DB with Repositories methods
## Current Behavior

Methods related to deleting Scans or Slices does not work. Reproduction can be found below.

## Steps to Reproduce the Problem

```
In [20]: from medtagger.repositories.scans import delete_scan_by_id

In [21]: delete_scan_by_id('dc4c5ce4-7004-4e61-80bd-e4e010c98ac6')
---------------------------------------------------------------------------
IntegrityError                            Traceback (most recent call last)
~/Documents/MedTagger/backend/venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py in _execute_context(self, dialect, constructor, statement, parameters, *args)
   1192                         parameters,
-> 1193                         context)
   1194         except BaseException as e:

~/Documents/MedTagger/backend/venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py in do_execute(self, cursor, statement, parameters, context)
    508     def do_execute(self, cursor, statement, parameters, context=None):
--> 509         cursor.execute(statement, parameters)
    510

IntegrityError: update or delete on table "Scans" violates foreign key constraint "fk_Slices_scan_id_Scans" on table "Slices"
DETAIL:  Key (id)=(dc4c5ce4-7004-4e61-80bd-e4e010c98ac6) is still referenced from table "Slices".


The above exception was the direct cause of the following exception:

IntegrityError                            Traceback (most recent call last)
<ipython-input-21-c7f27834d7f6> in <module>()
----> 1 delete_scan_by_id('dc4c5ce4-7004-4e61-80bd-e4e010c98ac6')

~/Documents/MedTagger/backend/medtagger/repositories/scans.py in delete_scan_by_id(scan_id)
     43     """Remove Scan from SQL database."""
     44     with db_session() as session:
---> 45         session.query(Scan).filter(Scan.id == scan_id).delete()
     46
     47

~/Documents/MedTagger/backend/venv/lib/python3.6/site-packages/sqlalchemy/orm/query.py in delete(self, synchronize_session)
   3344         delete_op = persistence.BulkDelete.factory(
   3345             self, synchronize_session)
-> 3346         delete_op.exec_()
   3347         return delete_op.rowcount
   3348

~/Documents/MedTagger/backend/venv/lib/python3.6/site-packages/sqlalchemy/orm/persistence.py in exec_(self)
   1324         self._do_pre()
   1325         self._do_pre_synchronize()
-> 1326         self._do_exec()
   1327         self._do_post_synchronize()
   1328         self._do_post()

~/Documents/MedTagger/backend/venv/lib/python3.6/site-packages/sqlalchemy/orm/persistence.py in _do_exec(self)
   1516                                  self.context.whereclause)
   1517
-> 1518         self._execute_stmt(delete_stmt)
   1519
   1520     def _do_post(self):

~/Documents/MedTagger/backend/venv/lib/python3.6/site-packages/sqlalchemy/orm/persistence.py in _execute_stmt(self, stmt)
   1331         self.result = self.query.session.execute(
   1332             stmt, params=self.query._params,
-> 1333             mapper=self.mapper)
   1334         self.rowcount = self.result.rowcount
   1335

~/Documents/MedTagger/backend/venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py in execute(self, clause, params, mapper, bind, **kw)
   1174
   1175         return self._connection_for_bind(
-> 1176             bind, close_with_result=True).execute(clause, params or {})
   1177
   1178     def scalar(self, clause, params=None, mapper=None, bind=None, **kw):

~/Documents/MedTagger/backend/venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py in execute(self, object, *multiparams, **params)
    946             raise exc.ObjectNotExecutableError(object)
    947         else:
--> 948             return meth(self, multiparams, params)
    949
    950     def _execute_function(self, func, multiparams, params):

~/Documents/MedTagger/backend/venv/lib/python3.6/site-packages/sqlalchemy/sql/elements.py in _execute_on_connection(self, connection, multiparams, params)
    267     def _execute_on_connection(self, connection, multiparams, params):
    268         if self.supports_execution:
--> 269             return connection._execute_clauseelement(self, multiparams, params)
    270         else:
    271             raise exc.ObjectNotExecutableError(self)

~/Documents/MedTagger/backend/venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py in _execute_clauseelement(self, elem, multiparams, params)
   1058             compiled_sql,
   1059             distilled_params,
-> 1060             compiled_sql, distilled_params
   1061         )
   1062         if self._has_events or self.engine._has_events:

~/Documents/MedTagger/backend/venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py in _execute_context(self, dialect, constructor, statement, parameters, *args)
   1198                 parameters,
   1199                 cursor,
-> 1200                 context)
   1201
   1202         if self._has_events or self.engine._has_events:

~/Documents/MedTagger/backend/venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py in _handle_dbapi_exception(self, e, statement, parameters, cursor, context)
   1411                 util.raise_from_cause(
   1412                     sqlalchemy_exception,
-> 1413                     exc_info
   1414                 )
   1415             else:

~/Documents/MedTagger/backend/venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py in raise_from_cause(exception, exc_info)
    263     exc_type, exc_value, exc_tb = exc_info
    264     cause = exc_value if exc_value is not exception else None
--> 265     reraise(type(exception), exception, tb=exc_tb, cause=cause)
    266
    267 if py3k:

~/Documents/MedTagger/backend/venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py in reraise(tp, value, tb, cause)
    246             value.__cause__ = cause
    247         if value.__traceback__ is not tb:
--> 248             raise value.with_traceback(tb)
    249         raise value
    250

~/Documents/MedTagger/backend/venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py in _execute_context(self, dialect, constructor, statement, parameters, *args)
   1191                         statement,
   1192                         parameters,
-> 1193                         context)
   1194         except BaseException as e:
   1195             self._handle_dbapi_exception(

~/Documents/MedTagger/backend/venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py in do_execute(self, cursor, statement, parameters, context)
    507
    508     def do_execute(self, cursor, statement, parameters, context=None):
--> 509         cursor.execute(statement, parameters)
    510
    511     def do_execute_no_params(self, cursor, statement, context=None):

IntegrityError: (psycopg2.IntegrityError) update or delete on table "Scans" violates foreign key constraint "fk_Slices_scan_id_Scans" on table "Slices"
DETAIL:  Key (id)=(dc4c5ce4-7004-4e61-80bd-e4e010c98ac6) is still referenced from table "Slices".
 [SQL: 'DELETE FROM "Scans" WHERE "Scans".id = %(id_1)s'] [parameters: {'id_1': 'dc4c5ce4-7004-4e61-80bd-e4e010c98ac6'}] (Background on this error at: http://sqlalche.me/e/gkpj)
```


Discussion:
Required for #412 

Resolving patch:
diff --git a/backend/alembic/versions/9f2eafdf821e_add_cascade_delete_to_scans.py b/backend/alembic/versions/9f2eafdf821e_add_cascade_delete_to_scans.py
new file mode 100644
--- /dev/null
+++ b/backend/alembic/versions/9f2eafdf821e_add_cascade_delete_to_scans.py
@@ -0,0 +1,99 @@
+"""Add cascade delete
+
+Added Cascade delete to:
+- Scan,
+- Slices,
+- Labels,
+- LabelElements,
+- every type of LabelElements for given tool.
+
+Fixed few naming bugs.
+
+Revision ID: 9f2eafdf821e
+Revises: 0707294d0a96
+Create Date: 2018-08-24 20:05:43.171568
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+revision = '9f2eafdf821e'
+down_revision = '0707294d0a96'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.drop_constraint('fk_Slices_scan_id_Scans', 'Slices')
+    op.create_foreign_key('fk_Slices_scan_id_Scans', 'Slices', 'Scans', ['scan_id'], ['id'], ondelete='CASCADE')
+
+    op.drop_constraint('fk_Labels_scan_id_Scans', 'Labels')
+    op.create_foreign_key('fk_Labels_scan_id_Scans', 'Labels', 'Scans', ['scan_id'], ['id'], ondelete='CASCADE')
+
+    # Droping LabelSelections foreign key (instead of LabelElements) due to naming bug
+    op.drop_constraint('fk_LabelSelections_label_id_Labels', 'LabelElements')
+    op.create_foreign_key('fk_LabelElements_label_id_Labels', 'LabelElements', 'Labels', ['label_id'], ['id'],
+                          ondelete='CASCADE')
+
+    op.drop_constraint('fk_RectangularLabelElements_id_LabelElements', 'RectangularLabelElements')
+    op.create_foreign_key(
+        'fk_RectangularLabelElements_id_LabelElements', 'RectangularLabelElements', 'LabelElements', ['id'], ['id'],
+        ondelete='CASCADE')
+
+    # Altering primary key name from LabelSelections to LabelElements
+    op.execute('ALTER INDEX "pk_LabelSelections" RENAME TO "pk_LabelElements"')
+
+    # Dropping PointLabelElement (without 's') due to naming bug
+    op.drop_constraint('fk_PointLabelElement_id_LabelElements', 'PointLabelElements')
+    op.create_foreign_key(
+        'fk_PointLabelElements_id_LabelElements', 'PointLabelElements', 'LabelElements', ['id'], ['id'],
+        ondelete='CASCADE')
+
+    op.drop_constraint('fk_ChainLabelElements_id_LabelElements', 'ChainLabelElements')
+    op.create_foreign_key(
+        'fk_ChainLabelElements_id_LabelElements', 'ChainLabelElements', 'LabelElements', ['id'], ['id'],
+        ondelete='CASCADE')
+
+    op.drop_constraint('fk_ChainLabelElementPoints_label_element_id_LabelElements', 'ChainLabelElementPoints')
+    op.create_foreign_key(
+        'fk_ChainLabelElementPoints_label_element_id_LabelElements', 'ChainLabelElementPoints', 'LabelElements',
+        ['label_element_id'], ['id'], ondelete='CASCADE')
+
+    op.drop_constraint('fk_BrushLabelElements_id_LabelElements', 'BrushLabelElements')
+    op.create_foreign_key(
+        'fk_BrushLabelElements_id_LabelElements', 'BrushLabelElements', 'LabelElements', ['id'], ['id'],
+        ondelete='CASCADE')
+
+
+def downgrade():
+    op.drop_constraint('fk_Slices_scan_id_Scans', 'Slices')
+    op.create_foreign_key('fk_Slices_scan_id_Scans', 'Slices', 'Scans', ['scan_id'], ['id'])
+
+    op.drop_constraint('fk_Labels_scan_id_Scans', 'Labels')
+    op.create_foreign_key('fk_Labels_scan_id_Scans', 'Labels', 'Scans', ['scan_id'], ['id'])
+
+    op.drop_constraint('fk_LabelElements_label_id_Labels', 'LabelElements')
+    op.create_foreign_key('fk_LabelSelections_label_id_Labels', 'LabelElements', 'Labels', ['label_id'], ['id'])
+
+    op.execute('ALTER INDEX "pk_LabelElements" RENAME TO "pk_LabelSelections"')
+
+    op.drop_constraint('fk_RectangularLabelElements_id_LabelElements', 'RectangularLabelElements')
+    op.create_foreign_key(
+        'fk_RectangularLabelElements_id_LabelElements', 'RectangularLabelElements', 'LabelElements', ['id'], ['id'])
+
+    op.drop_constraint('fk_PointLabelElements_id_LabelElements', 'PointLabelElements')
+    op.create_foreign_key(
+        'fk_PointLabelElement_id_LabelElements', 'PointLabelElements', 'LabelElements', ['id'], ['id'])
+
+    op.drop_constraint('fk_ChainLabelElements_id_LabelElements', 'ChainLabelElements')
+    op.create_foreign_key(
+        'fk_ChainLabelElements_id_LabelElements', 'ChainLabelElements', 'LabelElements', ['id'], ['id'])
+
+    op.drop_constraint('fk_ChainLabelElementPoints_label_element_id_LabelElements', 'ChainLabelElementPoints')
+    op.create_foreign_key(
+        'fk_ChainLabelElementPoints_label_element_id_LabelElements', 'ChainLabelElementPoints', 'LabelElements',
+        ['label_element_id'], ['id'])
+
+    op.drop_constraint('fk_BrushLabelElements_id_LabelElements', 'BrushLabelElements')
+    op.create_foreign_key(
+        'fk_BrushLabelElements_id_LabelElements', 'BrushLabelElements', 'LabelElements', ['id'], ['id'])
diff --git a/backend/medtagger/database/models.py b/backend/medtagger/database/models.py
--- a/backend/medtagger/database/models.py
+++ b/backend/medtagger/database/models.py
@@ -3,14 +3,17 @@
 import uuid
 from typing import List, Dict, cast, Optional, Any
 
-from sqlalchemy import Column, Integer, Float, String, ForeignKey, Boolean, Enum, Table, and_
+from sqlalchemy import Column, Integer, Float, String, ForeignKey, Boolean, Enum, Table, and_, event
 from sqlalchemy.dialects.postgresql import JSONB
+from sqlalchemy.engine import Connection
 from sqlalchemy.orm import relationship
+from sqlalchemy.orm.mapper import Mapper
 
 from medtagger.database.utils import ArrayOfEnum
 from medtagger.database import Base, db_session
 from medtagger.definitions import ScanStatus, SliceStatus, SliceOrientation, LabelVerificationStatus, \
     LabelElementStatus, LabelTool
+from medtagger.storage.models import BrushLabelElement as StorageBrushLabelElement, OriginalSlice, ProcessedSlice
 from medtagger.types import ScanID, SliceID, LabelID, LabelElementID, SliceLocation, SlicePosition, \
     LabelPosition, LabelShape, LabelingTime, LabelTagID, ActionID, SurveyID, SurveyElementID, SurveyElementKey, \
     ActionResponseID, SurveyResponseID, PointID, TaskID
@@ -177,8 +180,9 @@ class Scan(Base):
     owner_id: Optional[int] = Column(Integer, ForeignKey('Users.id'))
     owner: Optional[User] = relationship('User', back_populates='scans')
 
-    slices: List['Slice'] = relationship('Slice', back_populates='scan', order_by=lambda: Slice.location)
-    labels: List['Label'] = relationship('Label', back_populates='scan')
+    slices: List['Slice'] = relationship('Slice', back_populates='scan', cascade='delete',
+                                         order_by=lambda: Slice.location)
+    labels: List['Label'] = relationship('Label', back_populates='scan', cascade='delete')
 
     def __init__(self, dataset: Dataset, declared_number_of_slices: int, user: Optional[User]) -> None:
         """Initialize Scan.
@@ -252,7 +256,7 @@ class Slice(Base):
     width: int = Column(Integer, nullable=True)
     height: int = Column(Integer, nullable=True)
 
-    scan_id: ScanID = Column(String, ForeignKey('Scans.id'))
+    scan_id: ScanID = Column(String, ForeignKey('Scans.id', ondelete='cascade'))
     scan: Scan = relationship('Scan', back_populates='slices')
 
     def __init__(self, orientation: SliceOrientation, location: SliceLocation = None,
@@ -319,14 +323,14 @@ class Label(Base):
 
     __tablename__ = 'Labels'
     id: LabelID = Column(String, primary_key=True)
-    scan_id: ScanID = Column(String, ForeignKey('Scans.id'))
+    scan_id: ScanID = Column(String, ForeignKey('Scans.id', ondelete='cascade'))
     scan: Scan = relationship('Scan', back_populates='labels')
     task_id: TaskID = Column(Integer, ForeignKey('Tasks.id'), nullable=False)
     task: Task = relationship('Task')
 
     labeling_time: LabelingTime = Column(Float, nullable=True)
 
-    elements: List['LabelElement'] = relationship('LabelElement', back_populates='label')
+    elements: List['LabelElement'] = relationship('LabelElement', back_populates='label', cascade='delete')
 
     owner_id: int = Column(Integer, ForeignKey('Users.id'))
     owner: User = relationship('User', back_populates='labels')
@@ -404,7 +408,7 @@ class LabelElement(Base):
 
     slice_index: int = Column(Integer, nullable=False)
 
-    label_id: LabelID = Column(String, ForeignKey('Labels.id'))
+    label_id: LabelID = Column(String, ForeignKey('Labels.id', ondelete='cascade'))
     label: Label = relationship('Label', back_populates='elements')
 
     tag_id: LabelTagID = Column(Integer, ForeignKey('LabelTags.id'))
@@ -448,7 +452,7 @@ class RectangularLabelElement(LabelElement):
     """Definition of a Label Element made with Rectangle Tool."""
 
     __tablename__ = 'RectangularLabelElements'
-    id: LabelElementID = Column(String, ForeignKey('LabelElements.id'), primary_key=True)
+    id: LabelElementID = Column(String, ForeignKey('LabelElements.id', ondelete='cascade'), primary_key=True)
 
     x: float = Column(Float, nullable=False)
     y: float = Column(Float, nullable=False)
@@ -482,7 +486,7 @@ class BrushLabelElement(LabelElement):
     """Definition of a Label Element made with Brush Tool."""
 
     __tablename__ = 'BrushLabelElements'
-    id: LabelElementID = Column(String, ForeignKey('LabelElements.id'), primary_key=True)
+    id: LabelElementID = Column(String, ForeignKey('LabelElements.id', ondelete='cascade'), primary_key=True)
 
     width: int = Column(Integer, nullable=False)
     height: int = Column(Integer, nullable=False)
@@ -513,7 +517,7 @@ class PointLabelElement(LabelElement):
     """Definition of a Label Element made with Point Tool."""
 
     __tablename__ = 'PointLabelElements'
-    id: LabelElementID = Column(String, ForeignKey('LabelElements.id'), primary_key=True)
+    id: LabelElementID = Column(String, ForeignKey('LabelElements.id', ondelete='cascade'), primary_key=True)
 
     x: float = Column(Float, nullable=False)
     y: float = Column(Float, nullable=False)
@@ -542,11 +546,11 @@ class ChainLabelElement(LabelElement):
     """Definition of a Label Element made with Chain Tool."""
 
     __tablename__ = 'ChainLabelElements'
-    id: LabelElementID = Column(String, ForeignKey('LabelElements.id'), primary_key=True)
+    id: LabelElementID = Column(String, ForeignKey('LabelElements.id', ondelete='cascade'), primary_key=True)
 
     points: List['ChainLabelElementPoint'] = relationship('ChainLabelElementPoint',
                                                           order_by='ChainLabelElementPoint.order',
-                                                          back_populates='label_element')
+                                                          back_populates='label_element', cascade='delete')
     loop: bool = Column(Boolean, nullable=False)
 
     __mapper_args__ = {
@@ -577,7 +581,8 @@ class ChainLabelElementPoint(Base):
 
     x: float = Column(Float, nullable=False)
     y: float = Column(Float, nullable=False)
-    label_element_id: LabelElementID = Column(String, ForeignKey('LabelElements.id'), nullable=False)
+    label_element_id: LabelElementID = Column(String, ForeignKey('LabelElements.id', ondelete='cascade'),
+                                              nullable=False)
     label_element: ChainLabelElement = relationship('ChainLabelElement', back_populates='points')
     order: int = Column(Integer, nullable=False)
 
@@ -798,3 +803,21 @@ def __init__(self, survey_id: SurveyID, data: Dict[SurveyElementKey, Any]) -> No
     def get_details(self) -> Dict:
         """Return dictionary details about this Survey Response."""
         return self.data
+
+
+# pylint: disable=unused-argument
+@event.listens_for(BrushLabelElement, 'before_delete')
+def delete_brush_element_from_storage(mapper: Mapper, connection: Connection, target: Slice) -> None:
+    """Delete BrushLabelElement from storage."""
+    brush_label_element = StorageBrushLabelElement.get(id=target.id)
+    brush_label_element.delete()
+
+
+# pylint: disable=unused-argument
+@event.listens_for(Slice, 'before_delete')
+def delete_original_and_processed_slice_from_storage(mapper: Mapper, connection: Connection, target: Slice) -> None:
+    """Delete original and processed Slices from storage."""
+    original_slice = OriginalSlice.get(id=target.id)
+    original_slice.delete()
+    processed_slice = ProcessedSlice.get(id=target.id)
+    processed_slice.delete()
diff --git a/backend/medtagger/repositories/scans.py b/backend/medtagger/repositories/scans.py
--- a/backend/medtagger/repositories/scans.py
+++ b/backend/medtagger/repositories/scans.py
@@ -42,7 +42,8 @@ def get_random_scan(task: Task = None, user: User = None) -> Scan:
 def delete_scan_by_id(scan_id: ScanID) -> None:
     """Remove Scan from SQL database."""
     with db_session() as session:
-        session.query(Scan).filter(Scan.id == scan_id).delete()
+        scan = session.query(Scan).filter(Scan.id == scan_id).one()
+        session.delete(scan)
 
 
 def add_new_scan(dataset: Dataset, number_of_slices: int, user: Optional[User]) -> Scan:


Test patch:
diff --git a/backend/tests/functional_tests/test_deleting_scan.py b/backend/tests/functional_tests/test_deleting_scan.py
new file mode 100644
--- /dev/null
+++ b/backend/tests/functional_tests/test_deleting_scan.py
@@ -0,0 +1,268 @@
+"""Tests for deleting scans and its elements from database."""
+import json
+from typing import Any
+
+from cassandra.cqlengine.query import DoesNotExist
+from sqlalchemy.orm.exc import NoResultFound
+import pytest
+
+from medtagger.definitions import LabelTool
+from medtagger.repositories import (
+    datasets as DatasetsRepository,
+    label_tags as LabelTagsRepository,
+    tasks as TasksRepository,
+    scans as ScansRepository,
+    slices as SliceRepository,
+)
+from medtagger.types import ScanID, SliceID
+from medtagger.storage.models import BrushLabelElement, OriginalSlice, ProcessedSlice
+from tests.functional_tests import get_api_client, get_web_socket_client, get_headers
+from tests.functional_tests.conftest import get_token_for_logged_in_user
+
+
+def test_delete_scan_without_slices(prepare_environment: Any, synchronous_celery: Any) -> None:
+    """Test deleting scan without any slices."""
+    api_client = get_api_client()
+    user_token = get_token_for_logged_in_user('admin')
+
+    # Step 1. Prepare a structure for the test
+    DatasetsRepository.add_new_dataset('KIDNEYS', 'Kidneys')
+    task = TasksRepository.add_task('MARK_KIDNEYS', 'Mark Kidneys', 'path/to/image', ['KIDNEYS'], [])
+    LabelTagsRepository.add_new_tag('EXAMPLE_TAG', 'Example Tag', [LabelTool.RECTANGLE], task.id)
+
+    # Step 2. Add Scan to the system
+    payload = {'dataset': 'KIDNEYS', 'number_of_slices': 0}
+    response = api_client.post('/api/v1/scans/', data=json.dumps(payload),
+                               headers=get_headers(token=user_token, json=True))
+    assert response.status_code == 201
+    json_response = json.loads(response.data)
+    assert isinstance(json_response, dict)
+    scan_id: ScanID = json_response['scan_id']
+    assert isinstance(scan_id, str)
+    assert len(scan_id) >= 1
+
+    # Step 3. Get scan
+    response = api_client.get('/api/v1/scans/{}'.format(scan_id),
+                              headers=get_headers(token=user_token))
+    assert response.status_code == 200
+    json_response = json.loads(response.data)
+    assert isinstance(json_response, dict)
+    assert json_response['scan_id'] == scan_id
+    assert json_response['number_of_slices'] == 0
+
+    # Step 4. Delete scan from the system
+    ScansRepository.delete_scan_by_id(scan_id)
+
+    # Step 5. Check that scan has been deleted
+    response = api_client.get('/api/v1/scans/{}'.format(scan_id),
+                              headers=get_headers(token=user_token))
+    assert response.status_code == 404
+
+
+def test_delete_scan_with_slices(prepare_environment: Any, synchronous_celery: Any) -> None:
+    """Test deleting scan with at least 1 slice."""
+    api_client = get_api_client()
+    user_token = get_token_for_logged_in_user('admin')
+
+    # Step 1. Prepare a structure for the test
+    DatasetsRepository.add_new_dataset('KIDNEYS', 'Kidneys')
+    task = TasksRepository.add_task('MARK_KIDNEYS', 'Mark Kidneys', 'path/to/image', ['KIDNEYS'], [])
+    LabelTagsRepository.add_new_tag('EXAMPLE_TAG', 'Example Tag', [LabelTool.RECTANGLE], task.id)
+
+    # Step 2. Add Scan to the system
+    payload = {'dataset': 'KIDNEYS', 'number_of_slices': 1}
+    response = api_client.post('/api/v1/scans/', data=json.dumps(payload),
+                               headers=get_headers(token=user_token, json=True))
+    assert response.status_code == 201
+    json_response = json.loads(response.data)
+    assert isinstance(json_response, dict)
+    scan_id: ScanID = json_response['scan_id']
+    assert isinstance(scan_id, str)
+    assert len(scan_id) >= 1
+
+    # Step 3. Send slices
+    with open('tests/assets/example_scan/slice_1.dcm', 'rb') as image:
+        response = api_client.post('/api/v1/scans/{}/slices'.format(scan_id), data={
+            'image': (image, 'slice_1.dcm'),
+        }, content_type='multipart/form-data', headers=get_headers(token=user_token))
+        assert response.status_code == 201
+    json_response = json.loads(response.data)
+    assert isinstance(json_response, dict)
+    slice_id: SliceID = json_response['slice_id']
+    assert isinstance(slice_id, str)
+    assert len(slice_id) >= 1
+
+    # Step 4. Get scan
+    response = api_client.get('/api/v1/scans/{}'.format(scan_id),
+                              headers=get_headers(token=user_token))
+    assert response.status_code == 200
+    json_response = json.loads(response.data)
+    assert isinstance(json_response, dict)
+    assert json_response['scan_id'] == scan_id
+    assert json_response['number_of_slices'] == 1
+    assert json_response['width'] == 512
+    assert json_response['height'] == 512
+
+    # Step 5. Delete scan from the system
+    ScansRepository.delete_scan_by_id(scan_id)
+
+    # Step 6. Check that scan has been deleted
+    response = api_client.get('/api/v1/scans/{}'.format(scan_id),
+                              headers=get_headers(token=user_token))
+    assert response.status_code == 404
+
+    # Step 7. Check that slices has been deleted
+    with pytest.raises(NoResultFound):
+        SliceRepository.get_slice_by_id(slice_id)
+
+
+# pylint: disable=too-many-locals
+def test_delete_scan_with_labels(prepare_environment: Any, synchronous_celery: Any) -> None:
+    """Test deleting scan with at least 1 slice and with labels made with all tools."""
+    api_client = get_api_client()
+    web_socket_client = get_web_socket_client(namespace='/slices')
+    user_token = get_token_for_logged_in_user('admin')
+
+    # Step 1. Prepare a structure for the test
+    DatasetsRepository.add_new_dataset('KIDNEYS', 'Kidneys')
+    task = TasksRepository.add_task('MARK_KIDNEYS', 'Mark Kidneys', 'path/to/image', ['KIDNEYS'], [])
+    LabelTagsRepository.add_new_tag('EXAMPLE_TAG', 'Example Tag',
+                                    [LabelTool.RECTANGLE, LabelTool.CHAIN, LabelTool.POINT, LabelTool.BRUSH],
+                                    task.id)
+
+    # Step 2. Add Scan to the system
+    payload = {'dataset': 'KIDNEYS', 'number_of_slices': 1}
+    response = api_client.post('/api/v1/scans/', data=json.dumps(payload),
+                               headers=get_headers(token=user_token, json=True))
+    assert response.status_code == 201
+    json_response = json.loads(response.data)
+    assert isinstance(json_response, dict)
+    scan_id: ScanID = json_response['scan_id']
+    assert isinstance(scan_id, str)
+    assert len(scan_id) >= 1
+
+    # Step 3. Send slices
+    with open('tests/assets/example_scan/slice_1.dcm', 'rb') as image:
+        response = api_client.post('/api/v1/scans/{}/slices'.format(scan_id), data={
+            'image': (image, 'slice_1.dcm'),
+        }, content_type='multipart/form-data', headers=get_headers(token=user_token))
+        assert response.status_code == 201
+    json_response = json.loads(response.data)
+    assert isinstance(json_response, dict)
+    slice_id: SliceID = json_response['slice_id']
+    assert isinstance(slice_id, str)
+    assert len(slice_id) >= 1
+
+    # Step 4. Get scan
+    response = api_client.get('/api/v1/scans/{}'.format(scan_id),
+                              headers=get_headers(token=user_token))
+    assert response.status_code == 200
+    json_response = json.loads(response.data)
+    assert isinstance(json_response, dict)
+    assert json_response['scan_id'] == scan_id
+    assert json_response['number_of_slices'] == 1
+    assert json_response['width'] == 512
+    assert json_response['height'] == 512
+
+    # Step 5. Get slices from the server
+    web_socket_client.emit('request_slices', {'scan_id': scan_id, 'begin': 0, 'count': 1}, namespace='/slices')
+    responses = web_socket_client.get_received(namespace='/slices')
+    assert len(responses) == 1
+    response = responses[0]
+    assert response['name'] == 'slice'
+    assert response['args'][0]['scan_id'] == scan_id
+    assert response['args'][0]['index'] == 0
+    assert isinstance(response['args'][0]['image'], bytes)
+
+    # Step 6. Label it with all available tools
+    payload = {
+        'elements': [{
+            'x': 0.5,
+            'y': 0.5,
+            'slice_index': 0,
+            'width': 0.1,
+            'height': 0.1,
+            'tag': 'EXAMPLE_TAG',
+            'tool': LabelTool.RECTANGLE.value,
+        }, {
+            'slice_index': 0,
+            'width': 128,
+            'height': 128,
+            'image_key': 'SLICE_1',
+            'tag': 'EXAMPLE_TAG',
+            'tool': LabelTool.BRUSH.value,
+        }, {
+            'slice_index': 0,
+            'x': 0.25,
+            'y': 0.5,
+            'tag': 'EXAMPLE_TAG',
+            'tool': LabelTool.POINT.value,
+        }, {
+            'slice_index': 0,
+            'points': [
+                {
+                    'x': 0.2,
+                    'y': 0.3,
+                },
+                {
+                    'x': 0.5,
+                    'y': 0.8,
+                },
+            ],
+            'tag': 'EXAMPLE_TAG',
+            'tool': LabelTool.CHAIN.value,
+            'loop': False,
+        }],
+        'labeling_time': 12.34,
+    }
+    with open('tests/assets/example_labels/binary_mask.png', 'rb') as image:
+        data = {
+            'label': json.dumps(payload),
+            'SLICE_1': (image, 'slice_1'),
+        }
+        response = api_client.post('/api/v1/scans/{}/MARK_KIDNEYS/label'.format(scan_id), data=data,
+                                   headers=get_headers(token=user_token, multipart=True))
+    assert response.status_code == 201
+    json_response = json.loads(response.data)
+    assert isinstance(json_response, dict)
+    label_id = json_response['label_id']
+    assert isinstance(label_id, str)
+    assert len(label_id) >= 1
+
+    # Step 7. Fetch details about above Label and check image storage
+    response = api_client.get('/api/v1/labels/' + label_id, headers=get_headers(token=user_token))
+    assert response.status_code == 200
+    json_response = json.loads(response.data)
+    assert isinstance(json_response, dict)
+    label_element_id = json_response['elements'][1]['label_element_id']
+    brush_label_element = BrushLabelElement.get(id=label_element_id)
+    assert brush_label_element.image
+
+    # Step 7. Delete scan from the system
+    ScansRepository.delete_scan_by_id(scan_id)
+
+    # Step 8. Check that scan has been deleted
+    response = api_client.get('/api/v1/scans/{}'.format(scan_id),
+                              headers=get_headers(token=user_token))
+    assert response.status_code == 404
+
+    # Step 9. Check that slices has been deleted
+    with pytest.raises(NoResultFound):
+        SliceRepository.get_slice_by_id(slice_id)
+
+    # Step 10. Check that slices original image has been deleted from storage
+    with pytest.raises(DoesNotExist):
+        OriginalSlice.get(id=slice_id)
+
+    # Step 11. Check that slices processed image has been deleted from storage
+    with pytest.raises(DoesNotExist):
+        ProcessedSlice.get(id=slice_id)
+
+    # Step 12. Check that labels has been deleted
+    response = api_client.get('/api/v1/labels/{}'.format(label_id),
+                              headers=get_headers(token=user_token))
+    assert response.status_code == 404
+
+    # Step 13. Check that Brush Label was deleted from storage
+    with pytest.raises(DoesNotExist):
+        BrushLabelElement.get(id=label_element_id)