models.py 7.1 KB

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