models.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import datetime
  2. import uuid
  3. from django.contrib.auth.models import User
  4. from django.contrib.contenttypes.fields import GenericForeignKey, \
  5. GenericRelation
  6. from django.contrib.contenttypes.models import ContentType
  7. from django.core import serializers
  8. from django.db import models, connection
  9. from django.db.models.aggregates import Sum
  10. from martor.models import MartorField
  11. from notifications.signals import notify
  12. class Member(models.Model):
  13. user = models.OneToOneField(User, on_delete=models.CASCADE)
  14. trigram = models.CharField(max_length=5)
  15. def __str__(self):
  16. return self.trigram
  17. class BaseModel(models.Model):
  18. created = models.DateTimeField(auto_now_add=True)
  19. updated = models.DateTimeField(auto_now=True)
  20. objects = models.Manager()
  21. uuid = models.UUIDField(default=uuid.uuid4, editable=False)
  22. class Meta:
  23. abstract = True
  24. def get_content_type(self):
  25. return ContentType.objects.get_for_model(self)
  26. def model_name(self):
  27. try:
  28. return self._meta.verbose_name
  29. except AttributeError:
  30. return ""
  31. def to_json(self):
  32. return serializers.serialize('json', [ self, ])
  33. class Comment(BaseModel):
  34. class Meta:
  35. verbose_name = "commentaire"
  36. verbose_name_plural = "commentaires"
  37. ordering = ('-created', )
  38. def __str__(self):
  39. return "De {}, le {:%Y-%m-%d}: {}".format(self.author.username, self.created, self.content[:30])
  40. content_type = models.ForeignKey(ContentType,on_delete=models.CASCADE)
  41. object_id = models.PositiveIntegerField()
  42. content_object = GenericForeignKey('content_type', 'object_id')
  43. content = MartorField(blank=False, default="", verbose_name="Commentaire")
  44. author = models.ForeignKey(User,
  45. on_delete=models.PROTECT,
  46. verbose_name="Auteur")
  47. created = models.DateTimeField(auto_now_add=True)
  48. class Project(BaseModel):
  49. class Meta:
  50. verbose_name = "projet"
  51. verbose_name_plural = "projets"
  52. name = models.CharField(max_length=200)
  53. description = MartorField(blank=True, default="", verbose_name="Description")
  54. color = models.CharField(max_length=7, default="#f6755e")
  55. def __str__(self):
  56. return self.name
  57. class Epic(BaseModel):
  58. class Meta:
  59. verbose_name = "epic"
  60. verbose_name_plural = "epics"
  61. ordering = ('-value', )
  62. SIZES = (("XXS","XXS (< 1 sprints)"),
  63. ("XS", "XS (1 sprint)"),
  64. ("S", "S (1-2 sprints)"),
  65. ("M", "M (2-3 sprints)"),
  66. ("L", "L (3-4 sprints)"),
  67. ("XL", "XL (4-6 sprints)"),
  68. ("XXL", "XXL (> 6 sprints)"))
  69. name = models.CharField(max_length=200, default="", verbose_name="Nom")
  70. size = models.CharField(max_length=10, default="M", choices=SIZES, verbose_name="Taille")
  71. value = models.IntegerField(default=0, verbose_name="Valeur")
  72. description = MartorField(blank=True, default="", verbose_name="Description")
  73. project = models.ForeignKey(Project, on_delete=models.PROTECT, null=True, verbose_name="Projet")
  74. closed = models.BooleanField(default=False, verbose_name="Clôturée")
  75. comments = GenericRelation(Comment)
  76. def __str__(self):
  77. return self.name
  78. def nb_stories(self):
  79. return len(self.stories.all())
  80. def nb_closed_stories(self):
  81. return len(self.stories.filter(closed=True))
  82. def nb_active_stories(self):
  83. res = 0
  84. for story in self.stories.filter(closed=False):
  85. if story.running():
  86. res += 1
  87. return res
  88. def contributors(self):
  89. qry = User.objects.raw("""SELECT DISTINCT auth_user.*
  90. FROM ((auth_user INNER JOIN main_story_assignees ON auth_user.id = main_story_assignees.user_id)
  91. INNER JOIN main_story ON main_story_assignees.story_id = main_story.id)
  92. WHERE main_story.epic_id = {};
  93. """.format(self.id))
  94. return list(qry)
  95. def contributors_str(self):
  96. return ", ".join([c.member.trigram for c in self.contributors()])
  97. def close(self):
  98. self.closed = True
  99. self.save()
  100. def reopen(self):
  101. self.closed = False
  102. self.save()
  103. class Sprint(BaseModel):
  104. class Meta:
  105. verbose_name = "sprint"
  106. verbose_name_plural = "sprints"
  107. ordering = ('-date_start', )
  108. date_start = models.DateField()
  109. date_end = models.DateField()
  110. closed = models.BooleanField(default=False, verbose_name="Terminé")
  111. retro = MartorField(blank=True, default="", verbose_name="Bilan / Rétrospective")
  112. def __str__(self):
  113. return "Sprint #{} ({:%d/%m/%Y} > {:%d/%m/%Y})".format(self.id, self.date_start, self.date_end)
  114. def running(self):
  115. return self.date_start < datetime.date.today() <= self.date_end
  116. def nb_stories(self):
  117. return self.stories.count()
  118. def planned_velocity(self):
  119. total = self.stories.aggregate(Sum('weight'))['weight__sum']
  120. return total if total is not None else "NA"
  121. def real_velocity(self):
  122. if datetime.date.today() < self.date_start:
  123. return "NA"
  124. sql = """SELECT SUM(main_story.weight) as vel
  125. FROM main_story
  126. INNER JOIN (SELECT story_id, max(sprint_id) as sprint_id
  127. FROM main_story_sprints
  128. GROUP BY story_id) as last_sprints
  129. ON main_story.id = last_sprints.story_id
  130. WHERE last_sprints.sprint_id = {} AND closed = True;
  131. """.format(self.id)
  132. with connection.cursor() as cursor:
  133. cursor.execute(sql)
  134. row = cursor.fetchone()
  135. return row[0] if row[0] else 0
  136. @classmethod
  137. def current(cls):
  138. try:
  139. return Sprint.objects.filter(closed = False).order_by('date_start')[0]
  140. except IndexError:
  141. return None
  142. @classmethod
  143. def next(cls):
  144. try:
  145. return Sprint.objects.filter(closed = False).order_by('date_start')[1]
  146. except IndexError:
  147. return None
  148. class Story(BaseModel):
  149. class Meta:
  150. verbose_name = "story"
  151. verbose_name_plural = "stories"
  152. ordering = ('closed', '-updated')
  153. WEIGHTS = ((None, '------'), (1, 1),(2, 2),(3, 3),(5, 5),(8, 8),(13, 13),(21, 21))
  154. epic = models.ForeignKey(Epic,
  155. on_delete=models.PROTECT,
  156. null=True, blank=True,
  157. related_name="stories",
  158. verbose_name="Epic")
  159. name = models.CharField(max_length=200, default="", verbose_name="Nom")
  160. weight = models.IntegerField(null=True, choices=WEIGHTS, verbose_name="Poids")
  161. description = MartorField(blank=True, default="", verbose_name="Description")
  162. closed = models.BooleanField(default=False, verbose_name="Clôturée")
  163. author = models.ForeignKey(User,
  164. on_delete=models.PROTECT,
  165. null=True, blank=True,
  166. related_name="stories",
  167. related_query_name="story",
  168. verbose_name="Auteur")
  169. assignees = models.ManyToManyField(User,
  170. blank=True,
  171. related_name="assigned",
  172. verbose_name="Assignés")
  173. sprints = models.ManyToManyField(Sprint,
  174. blank=True,
  175. related_name="stories",
  176. related_query_name="story",
  177. verbose_name="Sprints")
  178. comments = GenericRelation(Comment)
  179. def __str__(self):
  180. return self.name
  181. def running(self):
  182. for sprint in self.sprints.all():
  183. if sprint.running():
  184. return True
  185. return False
  186. def contributors(self):
  187. contributors = list(self.assignees.all())
  188. if not self.author in contributors:
  189. contributors += [self.author]
  190. return contributors
  191. def close(self):
  192. self.closed = True
  193. self.save()
  194. def reopen(self):
  195. self.closed = False
  196. self.save()