Repository: doccano/doccano

Issue:
Python 3.7 compatability
This PR fixes `doccano init` to fail on Python 3.7 with

```
Traceback (most recent call last):
  File "/usr/lib/python3.7/code.py", line 90, in runcode
    exec(code, self.locals)
  File "<input>", line 1, in <module>
ImportError: cannot import name 'Literal' from 'typing' (/usr/lib/python3.7/typing.py)
```
see https://stackoverflow.com/questions/61206437/importerror-cannot-import-name-literal-from-typing?noredirect=1&lq=1





Discussion:

Resolving patch:
diff --git a/backend/api/models.py b/backend/api/models.py
--- a/backend/api/models.py
+++ b/backend/api/models.py
@@ -1,7 +1,7 @@
+import abc
 import random
 import string
 import uuid
-from typing import Literal
 
 from auto_labeling_pipeline.models import RequestModelFactory
 from django.contrib.auth.models import User
@@ -39,8 +39,30 @@ class Project(PolymorphicModel):
     collaborative_annotation = models.BooleanField(default=False)
     single_class_classification = models.BooleanField(default=False)
 
-    def is_task_of(self, task: Literal['text', 'image', 'speech']):
-        raise NotImplementedError()
+    @property
+    @abc.abstractmethod
+    def is_text_project(self) -> bool:
+        return False
+
+    @property
+    def can_define_label(self) -> bool:
+        """Whether or not the project can define label(ignoring the type of label)"""
+        return False
+
+    @property
+    def can_define_relation(self) -> bool:
+        """Whether or not the project can define relation."""
+        return False
+
+    @property
+    def can_define_category(self) -> bool:
+        """Whether or not the project can define category."""
+        return False
+
+    @property
+    def can_define_span(self) -> bool:
+        """Whether or not the project can define span."""
+        return False
 
     def __str__(self):
         return self.name
@@ -48,40 +70,82 @@ def __str__(self):
 
 class TextClassificationProject(Project):
 
-    def is_task_of(self, task: Literal['text', 'image', 'speech']):
-        return task == 'text'
+    @property
+    def is_text_project(self) -> bool:
+        return True
+
+    @property
+    def can_define_label(self) -> bool:
+        return True
+
+    @property
+    def can_define_category(self) -> bool:
+        return True
 
 
 class SequenceLabelingProject(Project):
     allow_overlapping = models.BooleanField(default=False)
     grapheme_mode = models.BooleanField(default=False)
 
-    def is_task_of(self, task: Literal['text', 'image', 'speech']):
-        return task == 'text'
+    @property
+    def is_text_project(self) -> bool:
+        return True
+
+    @property
+    def can_define_label(self) -> bool:
+        return True
+
+    @property
+    def can_define_span(self) -> bool:
+        return True
 
 
 class Seq2seqProject(Project):
 
-    def is_task_of(self, task: Literal['text', 'image', 'speech']):
-        return task == 'text'
+    @property
+    def is_text_project(self) -> bool:
+        return True
 
 
 class IntentDetectionAndSlotFillingProject(Project):
 
-    def is_task_of(self, task: Literal['text', 'image', 'speech']):
-        return task == 'text'
+    @property
+    def is_text_project(self) -> bool:
+        return True
+
+    @property
+    def can_define_label(self) -> bool:
+        return True
+
+    @property
+    def can_define_category(self) -> bool:
+        return True
+
+    @property
+    def can_define_span(self) -> bool:
+        return True
 
 
 class Speech2textProject(Project):
 
-    def is_task_of(self, task: Literal['text', 'image', 'speech']):
-        return task == 'speech'
+    @property
+    def is_text_project(self) -> bool:
+        return False
 
 
 class ImageClassificationProject(Project):
 
-    def is_task_of(self, task: Literal['text', 'image', 'speech']):
-        return task == 'image'
+    @property
+    def is_text_project(self) -> bool:
+        return False
+
+    @property
+    def can_define_label(self) -> bool:
+        return True
+
+    @property
+    def can_define_category(self) -> bool:
+        return True
 
 
 def generate_random_hex_color():
diff --git a/backend/api/serializers.py b/backend/api/serializers.py
--- a/backend/api/serializers.py
+++ b/backend/api/serializers.py
@@ -173,11 +173,21 @@ class Meta:
             'random_order',
             'collaborative_annotation',
             'single_class_classification',
+            'is_text_project',
+            'can_define_label',
+            'can_define_relation',
+            'can_define_category',
+            'can_define_span',
             'tags'
         )
         read_only_fields = (
             'updated_at',
             'users',
+            'is_text_project',
+            'can_define_label',
+            'can_define_relation',
+            'can_define_category',
+            'can_define_span',
             'tags'
         )
 
diff --git a/backend/api/views/auto_labeling.py b/backend/api/views/auto_labeling.py
--- a/backend/api/views/auto_labeling.py
+++ b/backend/api/views/auto_labeling.py
@@ -142,7 +142,7 @@ def send_request(self, model, example):
 
     def prepare_example(self):
         text = self.request.data['text']
-        if self.project.is_task_of('text'):
+        if self.project.is_text_project:
             return text
         else:
             tu = TemporaryUpload.objects.get(upload_id=text)
@@ -221,7 +221,7 @@ def perform_create(self, serializer):
 
     def get_example(self, project):
         example = get_object_or_404(Example, pk=self.kwargs['example_id'])
-        if project.is_task_of('text'):
+        if project.is_text_project:
             return example.text
         else:
             return str(example.filename)


Test patch: