models.py 8.3 KB

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