models.py 8.7 KB

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