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.

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