You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

217 lines
6.5 KiB

3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
  1. import abc
  2. import uuid
  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. from roles.models import Role
  10. class ProjectType(models.TextChoices):
  11. DOCUMENT_CLASSIFICATION = "DocumentClassification"
  12. SEQUENCE_LABELING = "SequenceLabeling"
  13. SEQ2SEQ = "Seq2seq"
  14. INTENT_DETECTION_AND_SLOT_FILLING = "IntentDetectionAndSlotFilling"
  15. SPEECH2TEXT = "Speech2text"
  16. IMAGE_CLASSIFICATION = "ImageClassification"
  17. BOUNDING_BOX = "BoundingBox"
  18. SEGMENTATION = "Segmentation"
  19. IMAGE_CAPTIONING = "ImageCaptioning"
  20. class Project(PolymorphicModel):
  21. name = models.CharField(max_length=100)
  22. description = models.TextField(default="")
  23. guideline = models.TextField(default="", blank=True)
  24. created_at = models.DateTimeField(auto_now_add=True)
  25. updated_at = models.DateTimeField(auto_now=True)
  26. created_by = models.ForeignKey(
  27. User,
  28. on_delete=models.SET_NULL,
  29. null=True,
  30. )
  31. project_type = models.CharField(max_length=30, choices=ProjectType.choices)
  32. random_order = models.BooleanField(default=False)
  33. collaborative_annotation = models.BooleanField(default=False)
  34. single_class_classification = models.BooleanField(default=False)
  35. allow_member_to_create_label_type = models.BooleanField(default=False)
  36. def add_admin(self):
  37. admin_role = Role.objects.get(name=settings.ROLE_PROJECT_ADMIN)
  38. Member.objects.create(
  39. project=self,
  40. user=self.created_by,
  41. role=admin_role,
  42. )
  43. @property
  44. @abc.abstractmethod
  45. def is_text_project(self) -> bool:
  46. return False
  47. def clone(self) -> "Project":
  48. """Clone the project.
  49. See https://docs.djangoproject.com/en/4.2/topics/db/queries/#copying-model-instances
  50. Returns:
  51. The cloned project.
  52. """
  53. project = Project.objects.get(pk=self.pk)
  54. project.pk = None
  55. project.id = None
  56. project._state.adding = True
  57. project.save()
  58. def bulk_clone(queryset: models.QuerySet, field_initializers: dict = None):
  59. """Clone the queryset.
  60. Args:
  61. queryset: The queryset to clone.
  62. field_initializers: The field initializers.
  63. """
  64. if field_initializers is None:
  65. field_initializers = {}
  66. items = []
  67. for item in queryset:
  68. item.id = None
  69. item.pk = None
  70. for field, value_or_callable in field_initializers.items():
  71. if callable(value_or_callable):
  72. value_or_callable = value_or_callable()
  73. setattr(item, field, value_or_callable)
  74. item.project = project
  75. item._state.adding = True
  76. items.append(item)
  77. queryset.model.objects.bulk_create(items)
  78. bulk_clone(self.role_mappings.all())
  79. bulk_clone(self.tags.all())
  80. # clone examples
  81. bulk_clone(self.examples.all(), field_initializers={"uuid": uuid.uuid4})
  82. # clone label types
  83. bulk_clone(self.categorytype_set.all())
  84. bulk_clone(self.spantype_set.all())
  85. bulk_clone(self.relationtype_set.all())
  86. return project
  87. def __str__(self):
  88. return self.name
  89. class TextClassificationProject(Project):
  90. @property
  91. def is_text_project(self) -> bool:
  92. return True
  93. class SequenceLabelingProject(Project):
  94. allow_overlapping = models.BooleanField(default=False)
  95. grapheme_mode = models.BooleanField(default=False)
  96. use_relation = models.BooleanField(default=False)
  97. @property
  98. def is_text_project(self) -> bool:
  99. return True
  100. class Seq2seqProject(Project):
  101. @property
  102. def is_text_project(self) -> bool:
  103. return True
  104. class IntentDetectionAndSlotFillingProject(Project):
  105. @property
  106. def is_text_project(self) -> bool:
  107. return True
  108. class Speech2textProject(Project):
  109. @property
  110. def is_text_project(self) -> bool:
  111. return False
  112. class ImageClassificationProject(Project):
  113. @property
  114. def is_text_project(self) -> bool:
  115. return False
  116. class BoundingBoxProject(Project):
  117. @property
  118. def is_text_project(self) -> bool:
  119. return False
  120. class SegmentationProject(Project):
  121. @property
  122. def is_text_project(self) -> bool:
  123. return False
  124. class ImageCaptioningProject(Project):
  125. @property
  126. def is_text_project(self) -> bool:
  127. return False
  128. class Tag(models.Model):
  129. text = models.TextField()
  130. project = models.ForeignKey(to=Project, on_delete=models.CASCADE, related_name="tags")
  131. def __str__(self):
  132. return self.text
  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. Args:
  137. project: The project id.
  138. member_id: The member id.
  139. new_role: The new role name.
  140. Returns:
  141. Whether the mapping can be updated or not.
  142. """
  143. queryset = self.filter(project=project, role__name=settings.ROLE_PROJECT_ADMIN)
  144. if queryset.count() > 1:
  145. return True
  146. else:
  147. admin = queryset.first()
  148. # we can change the role except for the only admin.
  149. return admin.id != member_id or new_role == settings.ROLE_PROJECT_ADMIN
  150. def has_role(self, project_id: int, user: User, role_name: str):
  151. return self.filter(project=project_id, user=user, role__name=role_name).exists()
  152. class Member(models.Model):
  153. user = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name="role_mappings")
  154. project = models.ForeignKey(to=Project, on_delete=models.CASCADE, related_name="role_mappings")
  155. role = models.ForeignKey(to=Role, on_delete=models.CASCADE)
  156. created_at = models.DateTimeField(auto_now_add=True)
  157. updated_at = models.DateTimeField(auto_now=True)
  158. objects = MemberManager()
  159. def clean(self):
  160. members = self.__class__.objects.exclude(id=self.id)
  161. if members.filter(user=self.user, project=self.project).exists():
  162. message = "This user is already assigned to a role in this project."
  163. raise ValidationError(message)
  164. @property
  165. def username(self):
  166. return self.user.username
  167. class Meta:
  168. unique_together = ("user", "project")