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.

221 lines
6.6 KiB

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