We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Add ability to clone a project
Feature description
---------
Add a button on the Projects page that allows the user to clone an existing project and specify which sections to duplicate (Guidelines, Labels, Data, etc) into a new project.

Use Case
----------
Often, I want to do a test run of annotations to check the efficacy of my class labels and instructions before annotating a larger dataset. 
There are times when I need to perform multiple test runs after changing instructions, class labels, or both. This is a very manual workflow of creating multiple, near duplicate projects. Having the ability to duplicate or clone an existing project, and then make edits manually would be an improvement. Currently, the only option in Doccano is to create a new project from scratch and manually copy/paste the Guidelines and re-upload a labels file (or manually enter in the labels a second time).


Env: MacOS

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `backend/projects/models.py`
Content:
```
1 import abc
2 
3 from django.conf import settings
4 from django.contrib.auth.models import User
5 from django.core.exceptions import ValidationError
6 from django.db import models
7 from django.db.models import Manager
8 from polymorphic.models import PolymorphicModel
9 
10 from roles.models import Role
11 
12 DOCUMENT_CLASSIFICATION = "DocumentClassification"
13 SEQUENCE_LABELING = "SequenceLabeling"
14 SEQ2SEQ = "Seq2seq"
15 SPEECH2TEXT = "Speech2text"
16 IMAGE_CLASSIFICATION = "ImageClassification"
17 BOUNDING_BOX = "BoundingBox"
18 SEGMENTATION = "Segmentation"
19 IMAGE_CAPTIONING = "ImageCaptioning"
20 INTENT_DETECTION_AND_SLOT_FILLING = "IntentDetectionAndSlotFilling"
21 PROJECT_CHOICES = (
22     (DOCUMENT_CLASSIFICATION, "document classification"),
23     (SEQUENCE_LABELING, "sequence labeling"),
24     (SEQ2SEQ, "sequence to sequence"),
25     (INTENT_DETECTION_AND_SLOT_FILLING, "intent detection and slot filling"),
26     (SPEECH2TEXT, "speech to text"),
27     (IMAGE_CLASSIFICATION, "image classification"),
28     (BOUNDING_BOX, "bounding box"),
29     (SEGMENTATION, "segmentation"),
30     (IMAGE_CAPTIONING, "image captioning"),
31 )
32 
33 
34 class Project(PolymorphicModel):
35     name = models.CharField(max_length=100)
36     description = models.TextField(default="")
37     guideline = models.TextField(default="", blank=True)
38     created_at = models.DateTimeField(auto_now_add=True)
39     updated_at = models.DateTimeField(auto_now=True)
40     created_by = models.ForeignKey(
41         User,
42         on_delete=models.SET_NULL,
43         null=True,
44     )
45     project_type = models.CharField(max_length=30, choices=PROJECT_CHOICES)
46     random_order = models.BooleanField(default=False)
47     collaborative_annotation = models.BooleanField(default=False)
48     single_class_classification = models.BooleanField(default=False)
49 
50     def add_admin(self):
51         admin_role = Role.objects.get(name=settings.ROLE_PROJECT_ADMIN)
52         Member.objects.create(
53             project=self,
54             user=self.created_by,
55             role=admin_role,
56         )
57 
58     @property
59     @abc.abstractmethod
60     def is_text_project(self) -> bool:
61         return False
62 
63     def __str__(self):
64         return self.name
65 
66 
67 class TextClassificationProject(Project):
68     @property
69     def is_text_project(self) -> bool:
70         return True
71 
72 
73 class SequenceLabelingProject(Project):
74     allow_overlapping = models.BooleanField(default=False)
75     grapheme_mode = models.BooleanField(default=False)
76     use_relation = models.BooleanField(default=False)
77 
78     @property
79     def is_text_project(self) -> bool:
80         return True
81 
82 
83 class Seq2seqProject(Project):
84     @property
85     def is_text_project(self) -> bool:
86         return True
87 
88 
89 class IntentDetectionAndSlotFillingProject(Project):
90     @property
91     def is_text_project(self) -> bool:
92         return True
93 
94 
95 class Speech2textProject(Project):
96     @property
97     def is_text_project(self) -> bool:
98         return False
99 
100 
101 class ImageClassificationProject(Project):
102     @property
103     def is_text_project(self) -> bool:
104         return False
105 
106 
107 class BoundingBoxProject(Project):
108     @property
109     def is_text_project(self) -> bool:
110         return False
111 
112 
113 class SegmentationProject(Project):
114     @property
115     def is_text_project(self) -> bool:
116         return False
117 
118 
119 class ImageCaptioningProject(Project):
120     @property
121     def is_text_project(self) -> bool:
122         return False
123 
124 
125 class Tag(models.Model):
126     text = models.TextField()
127     project = models.ForeignKey(to=Project, on_delete=models.CASCADE, related_name="tags")
128 
129     def __str__(self):
130         return self.text
131 
132 
133 class MemberManager(Manager):
134     def can_update(self, project: int, member_id: int, new_role: str) -> bool:
135         """The project needs at least 1 admin.
136 
137         Args:
138             project: The project id.
139             member_id: The member id.
140             new_role: The new role name.
141 
142         Returns:
143             Whether the mapping can be updated or not.
144         """
145         queryset = self.filter(project=project, role__name=settings.ROLE_PROJECT_ADMIN)
146         if queryset.count() > 1:
147             return True
148         else:
149             admin = queryset.first()
150             # we can change the role except for the only admin.
151             return admin.id != member_id or new_role == settings.ROLE_PROJECT_ADMIN
152 
153     def has_role(self, project_id: int, user: User, role_name: str):
154         return self.filter(project=project_id, user=user, role__name=role_name).exists()
155 
156 
157 class Member(models.Model):
158     user = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name="role_mappings")
159     project = models.ForeignKey(to=Project, on_delete=models.CASCADE, related_name="role_mappings")
160     role = models.ForeignKey(to=Role, on_delete=models.CASCADE)
161     created_at = models.DateTimeField(auto_now_add=True)
162     updated_at = models.DateTimeField(auto_now=True)
163     objects = MemberManager()
164 
165     def clean(self):
166         members = self.__class__.objects.exclude(id=self.id)
167         if members.filter(user=self.user, project=self.project).exists():
168             message = "This user is already assigned to a role in this project."
169             raise ValidationError(message)
170 
171     @property
172     def username(self):
173         return self.user.username
174 
175     class Meta:
176         unique_together = ("user", "project")
177
```

Path: `backend/projects/urls.py`
Content:
```
1 from django.urls import path
2 
3 from .views.member import MemberDetail, MemberList, MyRole
4 from .views.project import ProjectDetail, ProjectList
5 from .views.tag import TagDetail, TagList
6 
7 urlpatterns = [
8     path(route="projects", view=ProjectList.as_view(), name="project_list"),
9     path(route="projects/<int:project_id>", view=ProjectDetail.as_view(), name="project_detail"),
10     path(route="projects/<int:project_id>/my-role", view=MyRole.as_view(), name="my_role"),
11     path(route="projects/<int:project_id>/tags", view=TagList.as_view(), name="tag_list"),
12     path(route="projects/<int:project_id>/tags/<int:tag_id>", view=TagDetail.as_view(), name="tag_detail"),
13     path(route="projects/<int:project_id>/members", view=MemberList.as_view(), name="member_list"),
14     path(route="projects/<int:project_id>/members/<int:member_id>", view=MemberDetail.as_view(), name="member_detail"),
15 ]
16
```

Path: `backend/projects/views/project.py`
Content:
```
1 from django.conf import settings
2 from django_filters.rest_framework import DjangoFilterBackend
3 from rest_framework import filters, generics, status
4 from rest_framework.permissions import IsAdminUser, IsAuthenticated
5 from rest_framework.response import Response
6 
7 from projects.models import Project
8 from projects.permissions import IsProjectAdmin, IsProjectStaffAndReadOnly
9 from projects.serializers import ProjectPolymorphicSerializer
10 
11 
12 class ProjectList(generics.ListCreateAPIView):
13     serializer_class = ProjectPolymorphicSerializer
14     filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter)
15     search_fields = ("name", "description")
16     ordering_fields = ["name", "created_at", "created_by", "project_type"]
17     ordering = ["-created_at"]
18 
19     def get_permissions(self):
20         if self.request.method == "GET":
21             self.permission_classes = [
22                 IsAuthenticated,
23             ]
24         else:
25             self.permission_classes = [IsAuthenticated & IsAdminUser]
26         return super().get_permissions()
27 
28     def get_queryset(self):
29         return Project.objects.filter(role_mappings__user=self.request.user)
30 
31     def perform_create(self, serializer):
32         project = serializer.save(created_by=self.request.user)
33         project.add_admin()
34 
35     def delete(self, request, *args, **kwargs):
36         delete_ids = request.data["ids"]
37         projects = Project.objects.filter(
38             role_mappings__user=self.request.user,
39             role_mappings__role__name=settings.ROLE_PROJECT_ADMIN,
40             pk__in=delete_ids,
41         )
42         # Todo: I want to use bulk delete.
43         # But it causes the constraint error.
44         # See https://github.com/django-polymorphic/django-polymorphic/issues/229
45         for project in projects:
46             project.delete()
47         return Response(status=status.HTTP_204_NO_CONTENT)
48 
49 
50 class ProjectDetail(generics.RetrieveUpdateDestroyAPIView):
51     queryset = Project.objects.all()
52     serializer_class = ProjectPolymorphicSerializer
53     lookup_url_kwarg = "project_id"
54     permission_classes = [IsAuthenticated & (IsProjectAdmin | IsProjectStaffAndReadOnly)]
55
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/backend/projects/models.py b/backend/projects/models.py
--- a/backend/projects/models.py
+++ b/backend/projects/models.py
@@ -1,4 +1,5 @@
 import abc
+import uuid
 
 from django.conf import settings
 from django.contrib.auth.models import User
@@ -60,6 +61,54 @@
     def is_text_project(self) -> bool:
         return False
 
+    def clone(self) -> "Project":
+        """Clone the project.
+        See https://docs.djangoproject.com/en/4.2/topics/db/queries/#copying-model-instances
+
+        Returns:
+            The cloned project.
+        """
+        project = Project.objects.get(pk=self.pk)
+        project.pk = None
+        project.id = None
+        project._state.adding = True
+        project.save()
+
+        def bulk_clone(queryset: models.QuerySet, field_initializers: dict = None):
+            """Clone the queryset.
+
+            Args:
+                queryset: The queryset to clone.
+                field_initializers: The field initializers.
+            """
+            if field_initializers is None:
+                field_initializers = {}
+            items = []
+            for item in queryset:
+                item.id = None
+                item.pk = None
+                for field, value_or_callable in field_initializers.items():
+                    if callable(value_or_callable):
+                        value_or_callable = value_or_callable()
+                    setattr(item, field, value_or_callable)
+                item.project = project
+                item._state.adding = True
+                items.append(item)
+            queryset.model.objects.bulk_create(items)
+
+        bulk_clone(self.role_mappings.all())
+        bulk_clone(self.tags.all())
+
+        # clone examples
+        bulk_clone(self.examples.all(), field_initializers={"uuid": uuid.uuid4})
+
+        # clone label types
+        bulk_clone(self.categorytype_set.all())
+        bulk_clone(self.spantype_set.all())
+        bulk_clone(self.relationtype_set.all())
+
+        return project
+
     def __str__(self):
         return self.name
 
diff --git a/backend/projects/urls.py b/backend/projects/urls.py
--- a/backend/projects/urls.py
+++ b/backend/projects/urls.py
@@ -1,7 +1,7 @@
 from django.urls import path
 
 from .views.member import MemberDetail, MemberList, MyRole
-from .views.project import ProjectDetail, ProjectList
+from .views.project import CloneProject, ProjectDetail, ProjectList
 from .views.tag import TagDetail, TagList
 
 urlpatterns = [
@@ -11,5 +11,6 @@
     path(route="projects/<int:project_id>/tags", view=TagList.as_view(), name="tag_list"),
     path(route="projects/<int:project_id>/tags/<int:tag_id>", view=TagDetail.as_view(), name="tag_detail"),
     path(route="projects/<int:project_id>/members", view=MemberList.as_view(), name="member_list"),
+    path(route="projects/<int:project_id>/clone", view=CloneProject.as_view(), name="clone_project"),
     path(route="projects/<int:project_id>/members/<int:member_id>", view=MemberDetail.as_view(), name="member_detail"),
 ]
diff --git a/backend/projects/views/project.py b/backend/projects/views/project.py
--- a/backend/projects/views/project.py
+++ b/backend/projects/views/project.py
@@ -1,6 +1,8 @@
 from django.conf import settings
+from django.db import transaction
+from django.shortcuts import get_object_or_404
 from django_filters.rest_framework import DjangoFilterBackend
-from rest_framework import filters, generics, status
+from rest_framework import filters, generics, status, views
 from rest_framework.permissions import IsAdminUser, IsAuthenticated
 from rest_framework.response import Response
 
@@ -52,3 +54,14 @@
     serializer_class = ProjectPolymorphicSerializer
     lookup_url_kwarg = "project_id"
     permission_classes = [IsAuthenticated & (IsProjectAdmin | IsProjectStaffAndReadOnly)]
+
+
+class CloneProject(views.APIView):
+    permission_classes = [IsAuthenticated & IsProjectAdmin]
+
+    @transaction.atomic
+    def post(self, request, *args, **kwargs):
+        project = get_object_or_404(Project, pk=self.kwargs["project_id"])
+        cloned_project = project.clone()
+        serializer = ProjectPolymorphicSerializer(cloned_project)
+        return Response(serializer.data, status=status.HTTP_201_CREATED)

{"golden_diff": "diff --git a/backend/projects/models.py b/backend/projects/models.py\n--- a/backend/projects/models.py\n+++ b/backend/projects/models.py\n@@ -1,4 +1,5 @@\n import abc\n+import uuid\n \n from django.conf import settings\n from django.contrib.auth.models import User\n@@ -60,6 +61,54 @@\n     def is_text_project(self) -> bool:\n         return False\n \n+    def clone(self) -> \"Project\":\n+        \"\"\"Clone the project.\n+        See https://docs.djangoproject.com/en/4.2/topics/db/queries/#copying-model-instances\n+\n+        Returns:\n+            The cloned project.\n+        \"\"\"\n+        project = Project.objects.get(pk=self.pk)\n+        project.pk = None\n+        project.id = None\n+        project._state.adding = True\n+        project.save()\n+\n+        def bulk_clone(queryset: models.QuerySet, field_initializers: dict = None):\n+            \"\"\"Clone the queryset.\n+\n+            Args:\n+                queryset: The queryset to clone.\n+                field_initializers: The field initializers.\n+            \"\"\"\n+            if field_initializers is None:\n+                field_initializers = {}\n+            items = []\n+            for item in queryset:\n+                item.id = None\n+                item.pk = None\n+                for field, value_or_callable in field_initializers.items():\n+                    if callable(value_or_callable):\n+                        value_or_callable = value_or_callable()\n+                    setattr(item, field, value_or_callable)\n+                item.project = project\n+                item._state.adding = True\n+                items.append(item)\n+            queryset.model.objects.bulk_create(items)\n+\n+        bulk_clone(self.role_mappings.all())\n+        bulk_clone(self.tags.all())\n+\n+        # clone examples\n+        bulk_clone(self.examples.all(), field_initializers={\"uuid\": uuid.uuid4})\n+\n+        # clone label types\n+        bulk_clone(self.categorytype_set.all())\n+        bulk_clone(self.spantype_set.all())\n+        bulk_clone(self.relationtype_set.all())\n+\n+        return project\n+\n     def __str__(self):\n         return self.name\n \ndiff --git a/backend/projects/urls.py b/backend/projects/urls.py\n--- a/backend/projects/urls.py\n+++ b/backend/projects/urls.py\n@@ -1,7 +1,7 @@\n from django.urls import path\n \n from .views.member import MemberDetail, MemberList, MyRole\n-from .views.project import ProjectDetail, ProjectList\n+from .views.project import CloneProject, ProjectDetail, ProjectList\n from .views.tag import TagDetail, TagList\n \n urlpatterns = [\n@@ -11,5 +11,6 @@\n     path(route=\"projects/<int:project_id>/tags\", view=TagList.as_view(), name=\"tag_list\"),\n     path(route=\"projects/<int:project_id>/tags/<int:tag_id>\", view=TagDetail.as_view(), name=\"tag_detail\"),\n     path(route=\"projects/<int:project_id>/members\", view=MemberList.as_view(), name=\"member_list\"),\n+    path(route=\"projects/<int:project_id>/clone\", view=CloneProject.as_view(), name=\"clone_project\"),\n     path(route=\"projects/<int:project_id>/members/<int:member_id>\", view=MemberDetail.as_view(), name=\"member_detail\"),\n ]\ndiff --git a/backend/projects/views/project.py b/backend/projects/views/project.py\n--- a/backend/projects/views/project.py\n+++ b/backend/projects/views/project.py\n@@ -1,6 +1,8 @@\n from django.conf import settings\n+from django.db import transaction\n+from django.shortcuts import get_object_or_404\n from django_filters.rest_framework import DjangoFilterBackend\n-from rest_framework import filters, generics, status\n+from rest_framework import filters, generics, status, views\n from rest_framework.permissions import IsAdminUser, IsAuthenticated\n from rest_framework.response import Response\n \n@@ -52,3 +54,14 @@\n     serializer_class = ProjectPolymorphicSerializer\n     lookup_url_kwarg = \"project_id\"\n     permission_classes = [IsAuthenticated & (IsProjectAdmin | IsProjectStaffAndReadOnly)]\n+\n+\n+class CloneProject(views.APIView):\n+    permission_classes = [IsAuthenticated & IsProjectAdmin]\n+\n+    @transaction.atomic\n+    def post(self, request, *args, **kwargs):\n+        project = get_object_or_404(Project, pk=self.kwargs[\"project_id\"])\n+        cloned_project = project.clone()\n+        serializer = ProjectPolymorphicSerializer(cloned_project)\n+        return Response(serializer.data, status=status.HTTP_201_CREATED)\n", "issue": "Add ability to clone a project\nFeature description\r\n---------\r\nAdd a button on the Projects page that allows the user to clone an existing project and specify which sections to duplicate (Guidelines, Labels, Data, etc) into a new project.\r\n\r\nUse Case\r\n----------\r\nOften, I want to do a test run of annotations to check the efficacy of my class labels and instructions before annotating a larger dataset. \r\nThere are times when I need to perform multiple test runs after changing instructions, class labels, or both. This is a very manual workflow of creating multiple, near duplicate projects. Having the ability to duplicate or clone an existing project, and then make edits manually would be an improvement. Currently, the only option in Doccano is to create a new project from scratch and manually copy/paste the Guidelines and re-upload a labels file (or manually enter in the labels a second time).\r\n\r\n\r\nEnv: MacOS\n", "before_files": [{"content": "import abc\n\nfrom django.conf import settings\nfrom django.contrib.auth.models import User\nfrom django.core.exceptions import ValidationError\nfrom django.db import models\nfrom django.db.models import Manager\nfrom polymorphic.models import PolymorphicModel\n\nfrom roles.models import Role\n\nDOCUMENT_CLASSIFICATION = \"DocumentClassification\"\nSEQUENCE_LABELING = \"SequenceLabeling\"\nSEQ2SEQ = \"Seq2seq\"\nSPEECH2TEXT = \"Speech2text\"\nIMAGE_CLASSIFICATION = \"ImageClassification\"\nBOUNDING_BOX = \"BoundingBox\"\nSEGMENTATION = \"Segmentation\"\nIMAGE_CAPTIONING = \"ImageCaptioning\"\nINTENT_DETECTION_AND_SLOT_FILLING = \"IntentDetectionAndSlotFilling\"\nPROJECT_CHOICES = (\n    (DOCUMENT_CLASSIFICATION, \"document classification\"),\n    (SEQUENCE_LABELING, \"sequence labeling\"),\n    (SEQ2SEQ, \"sequence to sequence\"),\n    (INTENT_DETECTION_AND_SLOT_FILLING, \"intent detection and slot filling\"),\n    (SPEECH2TEXT, \"speech to text\"),\n    (IMAGE_CLASSIFICATION, \"image classification\"),\n    (BOUNDING_BOX, \"bounding box\"),\n    (SEGMENTATION, \"segmentation\"),\n    (IMAGE_CAPTIONING, \"image captioning\"),\n)\n\n\nclass Project(PolymorphicModel):\n    name = models.CharField(max_length=100)\n    description = models.TextField(default=\"\")\n    guideline = models.TextField(default=\"\", blank=True)\n    created_at = models.DateTimeField(auto_now_add=True)\n    updated_at = models.DateTimeField(auto_now=True)\n    created_by = models.ForeignKey(\n        User,\n        on_delete=models.SET_NULL,\n        null=True,\n    )\n    project_type = models.CharField(max_length=30, choices=PROJECT_CHOICES)\n    random_order = models.BooleanField(default=False)\n    collaborative_annotation = models.BooleanField(default=False)\n    single_class_classification = models.BooleanField(default=False)\n\n    def add_admin(self):\n        admin_role = Role.objects.get(name=settings.ROLE_PROJECT_ADMIN)\n        Member.objects.create(\n            project=self,\n            user=self.created_by,\n            role=admin_role,\n        )\n\n    @property\n    @abc.abstractmethod\n    def is_text_project(self) -> bool:\n        return False\n\n    def __str__(self):\n        return self.name\n\n\nclass TextClassificationProject(Project):\n    @property\n    def is_text_project(self) -> bool:\n        return True\n\n\nclass SequenceLabelingProject(Project):\n    allow_overlapping = models.BooleanField(default=False)\n    grapheme_mode = models.BooleanField(default=False)\n    use_relation = models.BooleanField(default=False)\n\n    @property\n    def is_text_project(self) -> bool:\n        return True\n\n\nclass Seq2seqProject(Project):\n    @property\n    def is_text_project(self) -> bool:\n        return True\n\n\nclass IntentDetectionAndSlotFillingProject(Project):\n    @property\n    def is_text_project(self) -> bool:\n        return True\n\n\nclass Speech2textProject(Project):\n    @property\n    def is_text_project(self) -> bool:\n        return False\n\n\nclass ImageClassificationProject(Project):\n    @property\n    def is_text_project(self) -> bool:\n        return False\n\n\nclass BoundingBoxProject(Project):\n    @property\n    def is_text_project(self) -> bool:\n        return False\n\n\nclass SegmentationProject(Project):\n    @property\n    def is_text_project(self) -> bool:\n        return False\n\n\nclass ImageCaptioningProject(Project):\n    @property\n    def is_text_project(self) -> bool:\n        return False\n\n\nclass Tag(models.Model):\n    text = models.TextField()\n    project = models.ForeignKey(to=Project, on_delete=models.CASCADE, related_name=\"tags\")\n\n    def __str__(self):\n        return self.text\n\n\nclass MemberManager(Manager):\n    def can_update(self, project: int, member_id: int, new_role: str) -> bool:\n        \"\"\"The project needs at least 1 admin.\n\n        Args:\n            project: The project id.\n            member_id: The member id.\n            new_role: The new role name.\n\n        Returns:\n            Whether the mapping can be updated or not.\n        \"\"\"\n        queryset = self.filter(project=project, role__name=settings.ROLE_PROJECT_ADMIN)\n        if queryset.count() > 1:\n            return True\n        else:\n            admin = queryset.first()\n            # we can change the role except for the only admin.\n            return admin.id != member_id or new_role == settings.ROLE_PROJECT_ADMIN\n\n    def has_role(self, project_id: int, user: User, role_name: str):\n        return self.filter(project=project_id, user=user, role__name=role_name).exists()\n\n\nclass Member(models.Model):\n    user = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name=\"role_mappings\")\n    project = models.ForeignKey(to=Project, on_delete=models.CASCADE, related_name=\"role_mappings\")\n    role = models.ForeignKey(to=Role, on_delete=models.CASCADE)\n    created_at = models.DateTimeField(auto_now_add=True)\n    updated_at = models.DateTimeField(auto_now=True)\n    objects = MemberManager()\n\n    def clean(self):\n        members = self.__class__.objects.exclude(id=self.id)\n        if members.filter(user=self.user, project=self.project).exists():\n            message = \"This user is already assigned to a role in this project.\"\n            raise ValidationError(message)\n\n    @property\n    def username(self):\n        return self.user.username\n\n    class Meta:\n        unique_together = (\"user\", \"project\")\n", "path": "backend/projects/models.py"}, {"content": "from django.urls import path\n\nfrom .views.member import MemberDetail, MemberList, MyRole\nfrom .views.project import ProjectDetail, ProjectList\nfrom .views.tag import TagDetail, TagList\n\nurlpatterns = [\n    path(route=\"projects\", view=ProjectList.as_view(), name=\"project_list\"),\n    path(route=\"projects/<int:project_id>\", view=ProjectDetail.as_view(), name=\"project_detail\"),\n    path(route=\"projects/<int:project_id>/my-role\", view=MyRole.as_view(), name=\"my_role\"),\n    path(route=\"projects/<int:project_id>/tags\", view=TagList.as_view(), name=\"tag_list\"),\n    path(route=\"projects/<int:project_id>/tags/<int:tag_id>\", view=TagDetail.as_view(), name=\"tag_detail\"),\n    path(route=\"projects/<int:project_id>/members\", view=MemberList.as_view(), name=\"member_list\"),\n    path(route=\"projects/<int:project_id>/members/<int:member_id>\", view=MemberDetail.as_view(), name=\"member_detail\"),\n]\n", "path": "backend/projects/urls.py"}, {"content": "from django.conf import settings\nfrom django_filters.rest_framework import DjangoFilterBackend\nfrom rest_framework import filters, generics, status\nfrom rest_framework.permissions import IsAdminUser, IsAuthenticated\nfrom rest_framework.response import Response\n\nfrom projects.models import Project\nfrom projects.permissions import IsProjectAdmin, IsProjectStaffAndReadOnly\nfrom projects.serializers import ProjectPolymorphicSerializer\n\n\nclass ProjectList(generics.ListCreateAPIView):\n    serializer_class = ProjectPolymorphicSerializer\n    filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter)\n    search_fields = (\"name\", \"description\")\n    ordering_fields = [\"name\", \"created_at\", \"created_by\", \"project_type\"]\n    ordering = [\"-created_at\"]\n\n    def get_permissions(self):\n        if self.request.method == \"GET\":\n            self.permission_classes = [\n                IsAuthenticated,\n            ]\n        else:\n            self.permission_classes = [IsAuthenticated & IsAdminUser]\n        return super().get_permissions()\n\n    def get_queryset(self):\n        return Project.objects.filter(role_mappings__user=self.request.user)\n\n    def perform_create(self, serializer):\n        project = serializer.save(created_by=self.request.user)\n        project.add_admin()\n\n    def delete(self, request, *args, **kwargs):\n        delete_ids = request.data[\"ids\"]\n        projects = Project.objects.filter(\n            role_mappings__user=self.request.user,\n            role_mappings__role__name=settings.ROLE_PROJECT_ADMIN,\n            pk__in=delete_ids,\n        )\n        # Todo: I want to use bulk delete.\n        # But it causes the constraint error.\n        # See https://github.com/django-polymorphic/django-polymorphic/issues/229\n        for project in projects:\n            project.delete()\n        return Response(status=status.HTTP_204_NO_CONTENT)\n\n\nclass ProjectDetail(generics.RetrieveUpdateDestroyAPIView):\n    queryset = Project.objects.all()\n    serializer_class = ProjectPolymorphicSerializer\n    lookup_url_kwarg = \"project_id\"\n    permission_classes = [IsAuthenticated & (IsProjectAdmin | IsProjectStaffAndReadOnly)]\n", "path": "backend/projects/views/project.py"}], "after_files": [{"content": "import abc\nimport uuid\n\nfrom django.conf import settings\nfrom django.contrib.auth.models import User\nfrom django.core.exceptions import ValidationError\nfrom django.db import models\nfrom django.db.models import Manager\nfrom polymorphic.models import PolymorphicModel\n\nfrom roles.models import Role\n\nDOCUMENT_CLASSIFICATION = \"DocumentClassification\"\nSEQUENCE_LABELING = \"SequenceLabeling\"\nSEQ2SEQ = \"Seq2seq\"\nSPEECH2TEXT = \"Speech2text\"\nIMAGE_CLASSIFICATION = \"ImageClassification\"\nBOUNDING_BOX = \"BoundingBox\"\nSEGMENTATION = \"Segmentation\"\nIMAGE_CAPTIONING = \"ImageCaptioning\"\nINTENT_DETECTION_AND_SLOT_FILLING = \"IntentDetectionAndSlotFilling\"\nPROJECT_CHOICES = (\n    (DOCUMENT_CLASSIFICATION, \"document classification\"),\n    (SEQUENCE_LABELING, \"sequence labeling\"),\n    (SEQ2SEQ, \"sequence to sequence\"),\n    (INTENT_DETECTION_AND_SLOT_FILLING, \"intent detection and slot filling\"),\n    (SPEECH2TEXT, \"speech to text\"),\n    (IMAGE_CLASSIFICATION, \"image classification\"),\n    (BOUNDING_BOX, \"bounding box\"),\n    (SEGMENTATION, \"segmentation\"),\n    (IMAGE_CAPTIONING, \"image captioning\"),\n)\n\n\nclass Project(PolymorphicModel):\n    name = models.CharField(max_length=100)\n    description = models.TextField(default=\"\")\n    guideline = models.TextField(default=\"\", blank=True)\n    created_at = models.DateTimeField(auto_now_add=True)\n    updated_at = models.DateTimeField(auto_now=True)\n    created_by = models.ForeignKey(\n        User,\n        on_delete=models.SET_NULL,\n        null=True,\n    )\n    project_type = models.CharField(max_length=30, choices=PROJECT_CHOICES)\n    random_order = models.BooleanField(default=False)\n    collaborative_annotation = models.BooleanField(default=False)\n    single_class_classification = models.BooleanField(default=False)\n\n    def add_admin(self):\n        admin_role = Role.objects.get(name=settings.ROLE_PROJECT_ADMIN)\n        Member.objects.create(\n            project=self,\n            user=self.created_by,\n            role=admin_role,\n        )\n\n    @property\n    @abc.abstractmethod\n    def is_text_project(self) -> bool:\n        return False\n\n    def clone(self) -> \"Project\":\n        \"\"\"Clone the project.\n        See https://docs.djangoproject.com/en/4.2/topics/db/queries/#copying-model-instances\n\n        Returns:\n            The cloned project.\n        \"\"\"\n        project = Project.objects.get(pk=self.pk)\n        project.pk = None\n        project.id = None\n        project._state.adding = True\n        project.save()\n\n        def bulk_clone(queryset: models.QuerySet, field_initializers: dict = None):\n            \"\"\"Clone the queryset.\n\n            Args:\n                queryset: The queryset to clone.\n                field_initializers: The field initializers.\n            \"\"\"\n            if field_initializers is None:\n                field_initializers = {}\n            items = []\n            for item in queryset:\n                item.id = None\n                item.pk = None\n                for field, value_or_callable in field_initializers.items():\n                    if callable(value_or_callable):\n                        value_or_callable = value_or_callable()\n                    setattr(item, field, value_or_callable)\n                item.project = project\n                item._state.adding = True\n                items.append(item)\n            queryset.model.objects.bulk_create(items)\n\n        bulk_clone(self.role_mappings.all())\n        bulk_clone(self.tags.all())\n\n        # clone examples\n        bulk_clone(self.examples.all(), field_initializers={\"uuid\": uuid.uuid4})\n\n        # clone label types\n        bulk_clone(self.categorytype_set.all())\n        bulk_clone(self.spantype_set.all())\n        bulk_clone(self.relationtype_set.all())\n\n        return project\n\n    def __str__(self):\n        return self.name\n\n\nclass TextClassificationProject(Project):\n    @property\n    def is_text_project(self) -> bool:\n        return True\n\n\nclass SequenceLabelingProject(Project):\n    allow_overlapping = models.BooleanField(default=False)\n    grapheme_mode = models.BooleanField(default=False)\n    use_relation = models.BooleanField(default=False)\n\n    @property\n    def is_text_project(self) -> bool:\n        return True\n\n\nclass Seq2seqProject(Project):\n    @property\n    def is_text_project(self) -> bool:\n        return True\n\n\nclass IntentDetectionAndSlotFillingProject(Project):\n    @property\n    def is_text_project(self) -> bool:\n        return True\n\n\nclass Speech2textProject(Project):\n    @property\n    def is_text_project(self) -> bool:\n        return False\n\n\nclass ImageClassificationProject(Project):\n    @property\n    def is_text_project(self) -> bool:\n        return False\n\n\nclass BoundingBoxProject(Project):\n    @property\n    def is_text_project(self) -> bool:\n        return False\n\n\nclass SegmentationProject(Project):\n    @property\n    def is_text_project(self) -> bool:\n        return False\n\n\nclass ImageCaptioningProject(Project):\n    @property\n    def is_text_project(self) -> bool:\n        return False\n\n\nclass Tag(models.Model):\n    text = models.TextField()\n    project = models.ForeignKey(to=Project, on_delete=models.CASCADE, related_name=\"tags\")\n\n    def __str__(self):\n        return self.text\n\n\nclass MemberManager(Manager):\n    def can_update(self, project: int, member_id: int, new_role: str) -> bool:\n        \"\"\"The project needs at least 1 admin.\n\n        Args:\n            project: The project id.\n            member_id: The member id.\n            new_role: The new role name.\n\n        Returns:\n            Whether the mapping can be updated or not.\n        \"\"\"\n        queryset = self.filter(project=project, role__name=settings.ROLE_PROJECT_ADMIN)\n        if queryset.count() > 1:\n            return True\n        else:\n            admin = queryset.first()\n            # we can change the role except for the only admin.\n            return admin.id != member_id or new_role == settings.ROLE_PROJECT_ADMIN\n\n    def has_role(self, project_id: int, user: User, role_name: str):\n        return self.filter(project=project_id, user=user, role__name=role_name).exists()\n\n\nclass Member(models.Model):\n    user = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name=\"role_mappings\")\n    project = models.ForeignKey(to=Project, on_delete=models.CASCADE, related_name=\"role_mappings\")\n    role = models.ForeignKey(to=Role, on_delete=models.CASCADE)\n    created_at = models.DateTimeField(auto_now_add=True)\n    updated_at = models.DateTimeField(auto_now=True)\n    objects = MemberManager()\n\n    def clean(self):\n        members = self.__class__.objects.exclude(id=self.id)\n        if members.filter(user=self.user, project=self.project).exists():\n            message = \"This user is already assigned to a role in this project.\"\n            raise ValidationError(message)\n\n    @property\n    def username(self):\n        return self.user.username\n\n    class Meta:\n        unique_together = (\"user\", \"project\")\n", "path": "backend/projects/models.py"}, {"content": "from django.urls import path\n\nfrom .views.member import MemberDetail, MemberList, MyRole\nfrom .views.project import CloneProject, ProjectDetail, ProjectList\nfrom .views.tag import TagDetail, TagList\n\nurlpatterns = [\n    path(route=\"projects\", view=ProjectList.as_view(), name=\"project_list\"),\n    path(route=\"projects/<int:project_id>\", view=ProjectDetail.as_view(), name=\"project_detail\"),\n    path(route=\"projects/<int:project_id>/my-role\", view=MyRole.as_view(), name=\"my_role\"),\n    path(route=\"projects/<int:project_id>/tags\", view=TagList.as_view(), name=\"tag_list\"),\n    path(route=\"projects/<int:project_id>/tags/<int:tag_id>\", view=TagDetail.as_view(), name=\"tag_detail\"),\n    path(route=\"projects/<int:project_id>/members\", view=MemberList.as_view(), name=\"member_list\"),\n    path(route=\"projects/<int:project_id>/clone\", view=CloneProject.as_view(), name=\"clone_project\"),\n    path(route=\"projects/<int:project_id>/members/<int:member_id>\", view=MemberDetail.as_view(), name=\"member_detail\"),\n]\n", "path": "backend/projects/urls.py"}, {"content": "from django.conf import settings\nfrom django.db import transaction\nfrom django.shortcuts import get_object_or_404\nfrom django_filters.rest_framework import DjangoFilterBackend\nfrom rest_framework import filters, generics, status, views\nfrom rest_framework.permissions import IsAdminUser, IsAuthenticated\nfrom rest_framework.response import Response\n\nfrom projects.models import Project\nfrom projects.permissions import IsProjectAdmin, IsProjectStaffAndReadOnly\nfrom projects.serializers import ProjectPolymorphicSerializer\n\n\nclass ProjectList(generics.ListCreateAPIView):\n    serializer_class = ProjectPolymorphicSerializer\n    filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter)\n    search_fields = (\"name\", \"description\")\n    ordering_fields = [\"name\", \"created_at\", \"created_by\", \"project_type\"]\n    ordering = [\"-created_at\"]\n\n    def get_permissions(self):\n        if self.request.method == \"GET\":\n            self.permission_classes = [\n                IsAuthenticated,\n            ]\n        else:\n            self.permission_classes = [IsAuthenticated & IsAdminUser]\n        return super().get_permissions()\n\n    def get_queryset(self):\n        return Project.objects.filter(role_mappings__user=self.request.user)\n\n    def perform_create(self, serializer):\n        project = serializer.save(created_by=self.request.user)\n        project.add_admin()\n\n    def delete(self, request, *args, **kwargs):\n        delete_ids = request.data[\"ids\"]\n        projects = Project.objects.filter(\n            role_mappings__user=self.request.user,\n            role_mappings__role__name=settings.ROLE_PROJECT_ADMIN,\n            pk__in=delete_ids,\n        )\n        # Todo: I want to use bulk delete.\n        # But it causes the constraint error.\n        # See https://github.com/django-polymorphic/django-polymorphic/issues/229\n        for project in projects:\n            project.delete()\n        return Response(status=status.HTTP_204_NO_CONTENT)\n\n\nclass ProjectDetail(generics.RetrieveUpdateDestroyAPIView):\n    queryset = Project.objects.all()\n    serializer_class = ProjectPolymorphicSerializer\n    lookup_url_kwarg = \"project_id\"\n    permission_classes = [IsAuthenticated & (IsProjectAdmin | IsProjectStaffAndReadOnly)]\n\n\nclass CloneProject(views.APIView):\n    permission_classes = [IsAuthenticated & IsProjectAdmin]\n\n    @transaction.atomic\n    def post(self, request, *args, **kwargs):\n        project = get_object_or_404(Project, pk=self.kwargs[\"project_id\"])\n        cloned_project = project.clone()\n        serializer = ProjectPolymorphicSerializer(cloned_project)\n        return Response(serializer.data, status=status.HTTP_201_CREATED)\n", "path": "backend/projects/views/project.py"}]}