Django Library PaTT
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. import datetime
  2. from django.contrib.auth.models import User
  3. from django.db import models
  4. from django.utils.translation import gettext as _
  5. from simple_history.models import HistoricalRecords
  6. THRESHOLD_VERY_SOON_CHOICES = ((1, '1 day'), (2, '2 days'), (3, '3 days'), (4, '4 days'), (5, '5 days'), (7, '7 days'), (9, '9 days'), (12, '12 days'), ) # REQ-38
  7. THRESHOLD_SOON_CHOICES = ((1, '1 day'), (2, '2 days'), (3, '3 days'), (5, '5 days'), (7, '7 days'), (10, '10 days'), (14, '14 days'), (21, ' 21 days'), ) # REQ-39
  8. # PROJECTSTATE (REQ-??)
  9. #
  10. PROJECTSTATE_OPEN = 0
  11. PROJECTSTATE_CLOSED = 2
  12. PROJECTS_IN_WORK = (PROJECTSTATE_OPEN, )
  13. #
  14. PROJECTSTATE_CHOICES = (
  15. (PROJECTSTATE_OPEN, _('Open')),
  16. (PROJECTSTATE_CLOSED, _('Closed')),
  17. )
  18. # TASKSTATE (REQ-14)
  19. #
  20. TASKSTATE_OPEN = 0
  21. TASKSTATE_FINISHED = 1
  22. TASKSTATE_CLOSED = 2
  23. TASKSTATE_CANCELED = 3
  24. TASKS_IN_WORK = (TASKSTATE_OPEN, TASKSTATE_FINISHED, )
  25. #
  26. TASKSTATE_CHOICES = (
  27. (TASKSTATE_OPEN, _('Open')),
  28. (TASKSTATE_FINISHED, _('Finished')),
  29. (TASKSTATE_CLOSED, _('Closed')),
  30. (TASKSTATE_CANCELED, _('Cancelled')),
  31. )
  32. # TASKPRIORITY (REQ-??)
  33. #
  34. PRIO_CHOICES = (
  35. (1, _('1 - Highest')),
  36. (2, _('2 - High')),
  37. (3, _('3 - Above-Average')),
  38. (4, _('4 - Average')),
  39. (5, _('5 - Below-Average')),
  40. (6, _('6 - Low')),
  41. (7, _('7 - Lowest')),
  42. )
  43. # TASKPROGRESS (REQ-??)
  44. #
  45. PRORGRESS_CHOICES = (
  46. (0, '0 %'),
  47. (10, '10 %'),
  48. (20, '20 %'),
  49. (30, '30 %'),
  50. (40, '40 %'),
  51. (50, '50 %'),
  52. (60, '60 %'),
  53. (70, '70 %'),
  54. (80, '80 %'),
  55. (90, '90 %'),
  56. (100, '100 %'),
  57. )
  58. # COMMENTTYPE (REQ-??)
  59. #
  60. COMMENTTYPE_COMMENT = 0
  61. COMMENTTYPE_APPRAISAL = 1
  62. #
  63. COMMENTTYPE_CHOICES = (
  64. (COMMENTTYPE_APPRAISAL, _('Appraisal')),
  65. (COMMENTTYPE_COMMENT, _('Comment')),
  66. )
  67. # GENERAL Methods and Classes
  68. #
  69. def get_pattuserprofile(user):
  70. if user is None: # return a default profile if no (assigned_)user exist
  71. profile = PattUserProfile()
  72. else:
  73. try:
  74. profile = user.pattuserprofile
  75. except PattUserProfile.DoesNotExist:
  76. profile = PattUserProfile(user=user)
  77. profile.save()
  78. return profile
  79. # USERPROFILE Model
  80. #
  81. class PattUserProfile(models.Model):
  82. user = models.OneToOneField(User, unique=True, on_delete=models.CASCADE)
  83. threshold_very_soon = models.IntegerField(default=4, choices=THRESHOLD_VERY_SOON_CHOICES) # REQ-38
  84. threshold_soon = models.IntegerField(default=10, choices=THRESHOLD_SOON_CHOICES) # REQ-39
  85. class ModelList(list):
  86. def __init__(self, lst, chk=None, user=None):
  87. if chk is None:
  88. list.__init__(self, lst)
  89. else:
  90. list.__init__(self)
  91. for e in lst:
  92. acc = chk(e, user)
  93. if acc.read:
  94. self.append(e)
  95. def sort(self, **kwargs):
  96. list.sort(self, key=lambda entry: entry.sort_string(), reverse=kwargs.get('reverse', False))
  97. # PROCECT Model
  98. #
  99. class Project(models.Model):
  100. name = models.CharField(max_length=128) # REQ-7
  101. description = models.TextField(default='', blank=True) # REQ-8
  102. state = models.IntegerField(choices=PROJECTSTATE_CHOICES, default=PROJECTSTATE_OPEN) # REQ-26
  103. role_leader = models.ManyToManyField(User, related_name='role_leader') # REQ-9
  104. role_member = models.ManyToManyField(User, related_name='role_member', blank=True) # REQ-12
  105. role_visitor = models.ManyToManyField(User, related_name='role_visitor', blank=True) # REQ-33
  106. creation_date = models.DateTimeField(auto_now_add=True)
  107. @property
  108. def attachment_target_path(self):
  109. if self.id:
  110. return 'patt/project/%d' % self.id
  111. else:
  112. return 'patt/project/new'
  113. @property
  114. def formatted_leaders(self):
  115. if self.id: # in preview (before saving), the ManyToManyFields are not available
  116. return ', '.join([user.username for user in self.role_leader.all()]) or '-'
  117. else:
  118. return '...'
  119. @property
  120. def formatted_members(self):
  121. if self.id: # in preview (before saving), the ManyToManyFields are not available
  122. return ', '.join([user.username for user in self.role_member.all()]) or '-'
  123. else:
  124. return '...'
  125. @property
  126. def formatted_visitors(self):
  127. if self.id: # in preview (before saving), the ManyToManyFields are not available
  128. return ', '.join([user.username for user in self.role_visitor.all()]) or '-'
  129. else:
  130. return '...'
  131. def sort_string(self):
  132. return (self.state, self.name)
  133. def __str__(self):
  134. return 'Project #%d: %s' % (self.id, self.name)
  135. # TASK Model
  136. #
  137. class Task(models.Model):
  138. WARNING_GROUP_OVERDUE = 0 # REQ-40
  139. WARNING_GROUP_VERY_SOON = 1 # REQ-40
  140. WARNING_GROUP_SOON = 2 # REQ-40
  141. WARNING_GROUP_DEFAULT = 3 # REQ-40
  142. WARNING_GROUP_DONE = 4 # REQ-40
  143. #
  144. SAVE_ON_CHANGE_FIELDS = ['state', 'priority', 'targetdate', 'progress', 'name', 'description', 'assigned_user', 'project']
  145. #
  146. state = models.IntegerField(default=TASKSTATE_OPEN) # REQ-14
  147. priority = models.IntegerField(choices=PRIO_CHOICES, default=4) # REQ-15
  148. targetdate = models.DateField(null=True, blank=True) # REQ-16
  149. progress = models.IntegerField(default=0, choices=PRORGRESS_CHOICES) # REQ-17
  150. name = models.CharField(max_length=150, default='') # REQ-?? (18)
  151. description = models.TextField(default='', blank=True) # REQ-??
  152. assigned_user = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL) # REQ-31
  153. project = models.ForeignKey(Project, on_delete=models.CASCADE) # REQ-??
  154. creation_date = models.DateTimeField(auto_now_add=True)
  155. #
  156. history = HistoricalRecords()
  157. @property
  158. def taskname_prefix(self):
  159. if self.assigned_user:
  160. return '**#%d //(%s)//:** ' % (self.id, self.assigned_user.username)
  161. else:
  162. return '**#%d:** ' % self.id
  163. def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
  164. if self.id and not force_update:
  165. orig = Task.objects.get(id=self.id)
  166. for key in self.SAVE_ON_CHANGE_FIELDS:
  167. if getattr(self, key) != getattr(orig, key):
  168. break
  169. else:
  170. self.save_needed = False
  171. return False
  172. self.save_needed = True
  173. return models.Model.save(self, force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
  174. @property
  175. def attachment_target_path(self):
  176. if self.id:
  177. return 'patt/task/%d' % self.id
  178. else:
  179. return 'patt/task/new'
  180. @property
  181. def comments(self):
  182. rv = ModelList(self.comment_set.all())
  183. rv.sort(reverse=True)
  184. return rv
  185. def days_before_targetdate(self): # REQ-40, REQ-43
  186. if self.targetdate is None:
  187. return float('Inf')
  188. else:
  189. if type(self.targetdate) == datetime.date:
  190. targetdate = self.targetdate
  191. else:
  192. targetdate = datetime.datetime.strptime(self.targetdate, '%Y-%m-%d').date()
  193. rv = targetdate - datetime.datetime.now().date()
  194. return rv.days
  195. def warning_group(self): # REQ-40
  196. if self.state in [TASKSTATE_CANCELED, TASKSTATE_CLOSED, TASKSTATE_FINISHED]:
  197. return self.WARNING_GROUP_DONE
  198. else:
  199. dbt = self.days_before_targetdate()
  200. if dbt <= 0:
  201. return self.WARNING_GROUP_OVERDUE
  202. if dbt <= get_pattuserprofile(self.assigned_user).threshold_very_soon:
  203. return self.WARNING_GROUP_VERY_SOON
  204. if dbt <= get_pattuserprofile(self.assigned_user).threshold_soon:
  205. return self.WARNING_GROUP_SOON
  206. return self.WARNING_GROUP_DEFAULT
  207. @property
  208. def class_by_state(self):
  209. return {
  210. self.WARNING_GROUP_DONE: 'task-finished',
  211. self.WARNING_GROUP_DEFAULT: 'task-normal',
  212. self.WARNING_GROUP_SOON: 'task-soon',
  213. self.WARNING_GROUP_VERY_SOON: 'task-very-soon',
  214. self.WARNING_GROUP_OVERDUE: 'task-late',
  215. }.get(self.warning_group())
  216. def sort_string(self): # REQ-41
  217. return (self.warning_group(), self.priority, self.days_before_targetdate(), self.progress, self.name)
  218. def __str__(self):
  219. return 'Task #%d: %s' % (self.id, self.name)
  220. # COMMENT Model
  221. #
  222. class Comment(models.Model): # REQ-19 and REQ-20
  223. SAVE_ON_CHANGE_FIELDS = ['task', 'user', 'type', 'comment']
  224. task = models.ForeignKey(Task, on_delete=models.CASCADE)
  225. user = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
  226. type = models.IntegerField(choices=COMMENTTYPE_CHOICES, default=COMMENTTYPE_COMMENT)
  227. creation_date = models.DateTimeField(auto_now_add=True)
  228. comment = models.TextField()
  229. #
  230. history = HistoricalRecords()
  231. def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
  232. if self.id and not force_update:
  233. orig = Comment.objects.get(id=self.id)
  234. for key in self.SAVE_ON_CHANGE_FIELDS:
  235. if getattr(self, key) != getattr(orig, key):
  236. break
  237. else:
  238. self.save_needed = False
  239. return False
  240. self.save_needed = True
  241. return models.Model.save(self, force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
  242. @property
  243. def attachment_target_path(self):
  244. if self.id:
  245. return 'patt/comment/%d' % self.id
  246. else:
  247. return 'patt/comment/new'
  248. @property
  249. def style(self):
  250. return {
  251. COMMENTTYPE_APPRAISAL: 'taskappraisal',
  252. }.get(self.type, 'taskcomment')
  253. @property
  254. def is_comment(self):
  255. return self.type == COMMENTTYPE_COMMENT
  256. @property
  257. def is_appraisal(self):
  258. return self.type == COMMENTTYPE_APPRAISAL
  259. def sort_string(self):
  260. return (self.creation_date, self.type, self.comment)
  261. def __str__(self):
  262. return 'Comment #%d: %s' % (self.id, self.task.name)
  263. # SEARCH Model
  264. #
  265. class Search(models.Model):
  266. name = models.CharField(max_length=48)
  267. search_txt = models.TextField(default='', blank=True)
  268. user = models.ForeignKey(User, on_delete=models.CASCADE)