models.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  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 = ('-number', )
  103. number = models.IntegerField(default=0)
  104. date_start = models.DateField()
  105. date_end = models.DateField()
  106. closed = models.BooleanField(default=False, verbose_name="Terminé")
  107. retro = MartorField(blank=True, default="", verbose_name="Bilan / Rétrospective")
  108. def __str__(self):
  109. return "Sprint #{} ({:%d/%m/%Y} > {:%d/%m/%Y})".format(self.number, self.date_start, self.date_end)
  110. def running(self):
  111. return self.date_start <= datetime.date.today() <= self.date_end and not self.closed
  112. def nb_stories(self):
  113. return self.stories.count()
  114. def planned_velocity(self):
  115. total = self.stories.aggregate(Sum('weight'))['weight__sum']
  116. return total if total is not None else "NA"
  117. def real_velocity(self):
  118. if datetime.date.today() < self.date_start:
  119. return "NA"
  120. sql = """SELECT SUM(main_story.weight) as vel
  121. FROM main_story
  122. INNER JOIN (SELECT story_id, max(sprint_id) as sprint_id
  123. FROM main_story_sprints
  124. GROUP BY story_id) as last_sprints
  125. ON main_story.id = last_sprints.story_id
  126. WHERE last_sprints.sprint_id = {} AND closed = True;
  127. """.format(self.id)
  128. with connection.cursor() as cursor:
  129. cursor.execute(sql)
  130. row = cursor.fetchone()
  131. return row[0] if row[0] else 0
  132. @classmethod
  133. def current(cls):
  134. """ the current sprint is the first non-closed sprint """
  135. try:
  136. return Sprint.objects.filter(closed = False).order_by('number')[0]
  137. except IndexError:
  138. return None
  139. @classmethod
  140. def next(cls):
  141. try:
  142. return Sprint.objects.filter(closed = False).order_by('number')[1]
  143. except IndexError:
  144. return None
  145. class Story(BaseModel):
  146. class Meta:
  147. verbose_name = "story"
  148. verbose_name_plural = "stories"
  149. ordering = ('closed', '-updated')
  150. WEIGHTS = ((None, '------'), (1, 1),(2, 2),(3, 3),(5, 5),(8, 8),(13, 13),(21, 21))
  151. epic = models.ForeignKey(Epic,
  152. on_delete=models.PROTECT,
  153. null=True, blank=True,
  154. related_name="stories",
  155. verbose_name="Epic")
  156. name = models.CharField(max_length=200, default="", verbose_name="Nom")
  157. weight = models.IntegerField(null=True, choices=WEIGHTS, verbose_name="Poids")
  158. description = MartorField(blank=True, default="", verbose_name="Description")
  159. closed = models.BooleanField(default=False, verbose_name="Clôturée")
  160. author = models.ForeignKey(User,
  161. on_delete=models.PROTECT,
  162. null=True, blank=True,
  163. related_name="stories",
  164. related_query_name="story",
  165. verbose_name="Auteur")
  166. assignees = models.ManyToManyField(User,
  167. blank=True,
  168. related_name="assigned",
  169. verbose_name="Assignés")
  170. sprints = models.ManyToManyField(Sprint,
  171. blank=True,
  172. related_name="stories",
  173. related_query_name="story",
  174. verbose_name="Sprints")
  175. comments = GenericRelation(Comment)
  176. def __str__(self):
  177. return self.name
  178. def running(self):
  179. for sprint in self.sprints.all():
  180. if sprint.running():
  181. return True
  182. return False
  183. def contributors(self):
  184. contributors = list(self.assignees.all())
  185. if not self.author in contributors:
  186. contributors += [self.author]
  187. return contributors
  188. def close(self):
  189. self.closed = True
  190. self.save()
  191. def reopen(self):
  192. self.closed = False
  193. self.save()