Repository: doccano/doccano

Issue:
Broken: Importing and Exporting SequenceLabeling projects with relations
How to reproduce the behaviour
---------
<!-- Before submitting an issue, make sure to check the docs and closed issues and FAQ to see if any of the solutions work for you. https://github.com/doccano/doccano/wiki/Frequently-Asked-Questions -->

<!-- Include a code example or the steps that led to the problem. Please try to be as specific as possible. -->

Your Environment
---------
<!-- Include details of your environment.-->
*   Operating System: Dockeer
*   Python Version Used: 3.8
*   When you install doccano: 11/1/22
*   How did you install doccano (Heroku button etc): docker-compose

I observed issues with the UI and interacting with relation labels. I am able create a relation label between two span labels in the UI, however the relation array get exported empty when going through the Export Dataset -> JSONL(relation) path. Furthermore, issues occur when trying to import relations as well. The import dataset flow only takes one "Column Label" field. When that is set to label, all of the span label and relation label info are uploaded as metadata. 

![image](https://user-images.githubusercontent.com/81196676/199328801-c98d3693-ff8f-45b3-80a3-7d916075ec56.png)

If the "Column Label" field is set to "entities" the span labels are imported and only the relation label data is uploaded as metadata. 

![image](https://user-images.githubusercontent.com/81196676/199329062-26b1ea53-8b03-464d-932e-15e0b0e83061.png)

The first goal would be that the export process, exports in the format displayed when you select the JSONL(relation) option from Export Dataset.

ie.

```
{
    "text": "Google was founded on September 4, 1998, by Larry Page and Sergey Brin.",
    "entities": [
        {
            "id": 0,
            "start_offset": 0,
            "end_offset": 6,
            "label": "ORG"
        },
        {
            "id": 1,
            "start_offset": 22,
            "end_offset": 39,
            "label": "DATE"
        },
        {
            "id": 2,
            "start_offset": 44,
            "end_offset": 54,
            "label": "PERSON"
        },
        {
            "id": 3,
            "start_offset": 59,
            "end_offset": 70,
            "label": "PERSON"
        }
    ],
    "relations": [
        {
            "id": 0,
            "from_id": 0,
            "to_id": 1,
            "type": "foundedAt"
        },
        {
            "id": 1,
            "from_id": 0,
            "to_id": 2,
            "type": "foundedBy"
        },
        {
            "id": 2,
            "from_id": 0,
            "to_id": 3,
            "type": "foundedBy"
        }
    ]
}
```

The second goal would be the ability to upload span labels and relation labels. Basically, Import Dataset should work with the Export Dataset -> JSONL(relation) results. I'll include a JSONL testing file for imports.

[relation_import_sample.zip](https://github.com/doccano/doccano/files/9913661/relation_import_sample.zip)


Discussion:
Related to #2022

Resolving patch:
diff --git a/backend/data_import/pipeline/label.py b/backend/data_import/pipeline/label.py
--- a/backend/data_import/pipeline/label.py
+++ b/backend/data_import/pipeline/label.py
@@ -142,6 +142,6 @@ def create(self, user, example: Example, types: LabelTypes, **kwargs):
             user=user,
             example=example,
             type=types[self.type],
-            from_id=kwargs["id_to_span"][self.from_id],
-            to_id=kwargs["id_to_span"][self.to_id],
+            from_id=kwargs["id_to_span"][(self.from_id, str(self.example_uuid))],
+            to_id=kwargs["id_to_span"][(self.to_id, str(self.example_uuid))],
         )
diff --git a/backend/data_import/pipeline/labels.py b/backend/data_import/pipeline/labels.py
--- a/backend/data_import/pipeline/labels.py
+++ b/backend/data_import/pipeline/labels.py
@@ -1,6 +1,6 @@
 import abc
 from itertools import groupby
-from typing import Dict, List
+from typing import Dict, List, Tuple
 
 from .examples import Examples
 from .label import Label
@@ -70,11 +70,11 @@ def clean(self, project: Project):
         self.labels = spans
 
     @property
-    def id_to_span(self) -> Dict[int, SpanModel]:
-        span_uuids = [str(label.uuid) for label in self.labels]
-        spans = SpanModel.objects.filter(uuid__in=span_uuids)
+    def id_to_span(self) -> Dict[Tuple[int, str], SpanModel]:
+        uuids = [str(span.uuid) for span in self.labels]
+        spans = SpanModel.objects.filter(uuid__in=uuids)
         uuid_to_span = {span.uuid: span for span in spans}
-        return {span.id: uuid_to_span[span.uuid] for span in self.labels}
+        return {(span.id, str(span.example_uuid)): uuid_to_span[span.uuid] for span in self.labels}
 
 
 class Texts(Labels):


Test patch:
diff --git a/backend/data_import/tests/test_label.py b/backend/data_import/tests/test_label.py
--- a/backend/data_import/tests/test_label.py
+++ b/backend/data_import/tests/test_label.py
@@ -25,7 +25,7 @@ class TestLabel(TestCase):
     def setUp(self):
         self.project = prepare_project(self.task)
         self.user = self.project.admin
-        self.example = mommy.make("Example", project=self.project.item)
+        self.example = mommy.make("Example", project=self.project.item, text="hello world")
 
 
 class TestCategoryLabel(TestLabel):
@@ -166,12 +166,12 @@ def test_create_type(self):
         self.assertEqual(relation_type.text, "A")
 
     def test_create(self):
-        relation = RelationLabel(type="A", from_id=0, to_id=1, example_uuid=uuid.uuid4())
+        relation = RelationLabel(type="A", from_id=0, to_id=1, example_uuid=self.example.uuid)
         types = MagicMock()
         types.__getitem__.return_value = mommy.make(RelationType, project=self.project.item)
         id_to_span = {
-            0: mommy.make(SpanModel, start_offset=0, end_offset=1),
-            1: mommy.make(SpanModel, start_offset=2, end_offset=3),
+            (0, str(self.example.uuid)): mommy.make(SpanModel, start_offset=0, end_offset=1, example=self.example),
+            (1, str(self.example.uuid)): mommy.make(SpanModel, start_offset=2, end_offset=3, example=self.example),
         }
         relation_model = relation.create(self.user, self.example, types, id_to_span=id_to_span)
         self.assertIsInstance(relation_model, RelationModel)
diff --git a/backend/data_import/tests/test_labels.py b/backend/data_import/tests/test_labels.py
--- a/backend/data_import/tests/test_labels.py
+++ b/backend/data_import/tests/test_labels.py
@@ -146,7 +146,7 @@ def setUp(self):
         self.project = prepare_project(SEQUENCE_LABELING, use_relation=True)
         self.user = self.project.admin
         example_uuid = uuid.uuid4()
-        example = mommy.make("Example", project=self.project.item, uuid=example_uuid)
+        example = mommy.make("Example", project=self.project.item, uuid=example_uuid, text="hello world")
         from_span = mommy.make("Span", example=example, start_offset=0, end_offset=1)
         to_span = mommy.make("Span", example=example, start_offset=2, end_offset=3)
         labels = [
@@ -154,7 +154,7 @@ def setUp(self):
         ]
         self.relations = Relations(labels, self.types)
         self.spans = MagicMock()
-        self.spans.id_to_span = {from_span.id: from_span, to_span.id: to_span}
+        self.spans.id_to_span = {(from_span.id, str(example_uuid)): from_span, (to_span.id, str(example_uuid)): to_span}
         self.examples = MagicMock()
         self.examples.__getitem__.return_value = example
         self.examples.__contains__.return_value = True