123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316 |
- import datetime
- from django.contrib.auth.models import User
- from django.db import models
- from django.utils.translation import gettext as _
- from simple_history.models import HistoricalRecords
-
-
- 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
- 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, ' days'), ) # REQ-39
-
-
- # PROJECTSTATE (REQ-??)
- #
- PROJECTSTATE_OPEN = 0
- PROJECTSTATE_CLOSED = 2
- PROJECTS_IN_WORK = (PROJECTSTATE_OPEN, )
- #
- PROJECTSTATE_CHOICES = (
- (PROJECTSTATE_OPEN, _('Open')),
- (PROJECTSTATE_CLOSED, _('Closed')),
- )
-
-
- # TASKSTATE (REQ-14)
- #
- TASKSTATE_OPEN = 0
- TASKSTATE_FINISHED = 1
- TASKSTATE_CLOSED = 2
- TASKSTATE_CANCELED = 3
- TASKS_IN_WORK = (TASKSTATE_OPEN, TASKSTATE_FINISHED, )
- #
- TASKSTATE_CHOICES = (
- (TASKSTATE_OPEN, _('Open')),
- (TASKSTATE_FINISHED, _('Finished')),
- (TASKSTATE_CLOSED, _('Closed')),
- (TASKSTATE_CANCELED, _('Cancelled')),
- )
-
-
- # TASKPRIORITY (REQ-??)
- #
- PRIO_CHOICES = (
- (1, _('1 - Highest')),
- (2, _('2 - High')),
- (3, _('3 - Above-Average')),
- (4, _('4 - Average')),
- (5, _('5 - Below-Average')),
- (6, _('6 - Low')),
- (7, _('7 - Lowest')),
- )
-
-
- # TASKPROGRESS (REQ-??)
- #
- PRORGRESS_CHOICES = (
- (0, '0 %'),
- (10, '10 %'),
- (20, '20 %'),
- (30, '30 %'),
- (40, '40 %'),
- (50, '50 %'),
- (60, '60 %'),
- (70, '70 %'),
- (80, '80 %'),
- (90, '90 %'),
- (100, '100 %'),
- )
-
- # COMMENTTYPE (REQ-??)
- #
- COMMENTTYPE_COMMENT = 0
- COMMENTTYPE_APPRAISAL = 1
- #
- COMMENTTYPE_CHOICES = (
- (COMMENTTYPE_APPRAISAL, _('Appraisal')),
- (COMMENTTYPE_COMMENT, _('Comment')),
- )
-
-
- # GENERAL Methods and Classes
- #
- def get_pattuserprofile(user):
- if user is None: # return a default profile if no (assigned_)user exist
- profile = PattUserProfile()
- else:
- try:
- profile = user.pattuserprofile
- except PattUserProfile.DoesNotExist:
- profile = PattUserProfile(user=user)
- profile.save()
- return profile
-
-
- # USERPROFILE Model
- #
- class PattUserProfile(models.Model):
- user = models.OneToOneField(User, unique=True, on_delete=models.CASCADE)
- threshold_very_soon = models.IntegerField(default=4, choices=THRESHOLD_VERY_SOON_CHOICES) # REQ-38
- threshold_soon = models.IntegerField(default=10, choices=THRESHOLD_SOON_CHOICES) # REQ-39
-
-
- class ModelList(list):
- def __init__(self, lst, chk=None, user=None):
- if chk is None:
- list.__init__(self, lst)
- else:
- list.__init__(self)
- for e in lst:
- acc = chk(e, user)
- if acc.read:
- self.append(e)
-
- def sort(self, **kwargs):
- list.sort(self, key=lambda entry: entry.sort_string(), reverse=kwargs.get('reverse', False))
-
-
- # PROCECT Model
- #
- class Project(models.Model):
- name = models.CharField(max_length=128) # REQ-7
- description = models.TextField(default='', blank=True) # REQ-8
- state = models.IntegerField(choices=PROJECTSTATE_CHOICES, default=PROJECTSTATE_OPEN) # REQ-26
- role_leader = models.ManyToManyField(User, related_name='role_leader') # REQ-9
- role_member = models.ManyToManyField(User, related_name='role_member', blank=True) # REQ-12
- role_visitor = models.ManyToManyField(User, related_name='role_visitor', blank=True) # REQ-33
- creation_date = models.DateTimeField(auto_now_add=True)
-
- @property
- def attachment_target_path(self):
- if self.id:
- return 'patt/project/%d' % self.id
- else:
- return 'patt/project/new'
-
- @property
- def formatted_leaders(self):
- if self.id: # in preview (before saving), the ManyToManyFields are not available
- return ', '.join([user.username for user in self.role_leader.all()]) or '-'
- else:
- return '...'
-
- @property
- def formatted_members(self):
- if self.id: # in preview (before saving), the ManyToManyFields are not available
- return ', '.join([user.username for user in self.role_member.all()]) or '-'
- else:
- return '...'
-
- @property
- def formatted_visitors(self):
- if self.id: # in preview (before saving), the ManyToManyFields are not available
- return ', '.join([user.username for user in self.role_visitor.all()]) or '-'
- else:
- return '...'
-
- def sort_string(self):
- return (self.state, self.name)
-
- def __str__(self):
- return 'Project #%d: %s' % (self.id, self.name)
-
-
- # TASK Model
- #
- class Task(models.Model):
- WARNING_GROUP_OVERDUE = 0 # REQ-40
- WARNING_GROUP_VERY_SOON = 1 # REQ-40
- WARNING_GROUP_SOON = 2 # REQ-40
- WARNING_GROUP_DEFAULT = 3 # REQ-40
- WARNING_GROUP_DONE = 4 # REQ-40
- #
- SAVE_ON_CHANGE_FIELDS = ['state', 'priority', 'targetdate', 'progress', 'name', 'description', 'assigned_user', 'project']
- #
- state = models.IntegerField(default=TASKSTATE_OPEN) # REQ-14
- priority = models.IntegerField(choices=PRIO_CHOICES, default=4) # REQ-15
- targetdate = models.DateField(null=True, blank=True) # REQ-16
- progress = models.IntegerField(default=0, choices=PRORGRESS_CHOICES) # REQ-17
- name = models.CharField(max_length=150, default='') # REQ-?? (18)
- description = models.TextField(default='', blank=True) # REQ-??
- assigned_user = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL) # REQ-31
- project = models.ForeignKey(Project, on_delete=models.CASCADE) # REQ-??
- creation_date = models.DateTimeField(auto_now_add=True)
- #
- history = HistoricalRecords()
-
- @property
- def taskname_prefix(self):
- if self.assigned_user:
- return '**#%d //(%s)//:** ' % (self.id, self.assigned_user.username)
- else:
- return '**#%d:** ' % self.id
-
- def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
- if self.id and not force_update:
- orig = Task.objects.get(id=self.id)
- for key in self.SAVE_ON_CHANGE_FIELDS:
- if getattr(self, key) != getattr(orig, key):
- break
- else:
- self.save_needed = False
- return False
- self.save_needed = True
- return models.Model.save(self, force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
-
- @property
- def attachment_target_path(self):
- if self.id:
- return 'patt/task/%d' % self.id
- else:
- return 'patt/task/new'
-
- @property
- def comments(self):
- rv = ModelList(self.comment_set.all())
- rv.sort(reverse=True)
- return rv
-
- def days_before_targetdate(self): # REQ-40, REQ-43
- if self.targetdate is None:
- return float('Inf')
- else:
- if type(self.targetdate) == datetime.date:
- targetdate = self.targetdate
- else:
- targetdate = datetime.datetime.strptime(self.targetdate, '%Y-%m-%d').date()
- rv = targetdate - datetime.datetime.now().date()
- return rv.days
-
- def warning_group(self): # REQ-40
- if self.state in [TASKSTATE_CANCELED, TASKSTATE_CLOSED, TASKSTATE_FINISHED]:
- return self.WARNING_GROUP_DONE
- else:
- dbt = self.days_before_targetdate()
- if dbt <= 0:
- return self.WARNING_GROUP_OVERDUE
- if dbt <= get_pattuserprofile(self.assigned_user).threshold_very_soon:
- return self.WARNING_GROUP_VERY_SOON
- if dbt <= get_pattuserprofile(self.assigned_user).threshold_soon:
- return self.WARNING_GROUP_SOON
- return self.WARNING_GROUP_DEFAULT
-
- @property
- def class_by_state(self):
- return {
- self.WARNING_GROUP_DONE: 'task-finished',
- self.WARNING_GROUP_DEFAULT: 'task-normal',
- self.WARNING_GROUP_SOON: 'task-soon',
- self.WARNING_GROUP_VERY_SOON: 'task-very-soon',
- self.WARNING_GROUP_OVERDUE: 'task-late',
- }.get(self.warning_group())
-
- def sort_string(self): # REQ-41
- return (self.warning_group(), self.priority, self.days_before_targetdate(), self.progress, self.name)
-
- def __str__(self):
- return 'Task #%d: %s' % (self.id, self.name)
-
-
- # COMMENT Model
- #
- class Comment(models.Model): # REQ-19 and REQ-20
- SAVE_ON_CHANGE_FIELDS = ['task', 'user', 'type', 'comment']
- task = models.ForeignKey(Task, on_delete=models.CASCADE)
- user = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
- type = models.IntegerField(choices=COMMENTTYPE_CHOICES, default=COMMENTTYPE_COMMENT)
- creation_date = models.DateTimeField(auto_now_add=True)
- comment = models.TextField()
- #
- history = HistoricalRecords()
-
- def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
- if self.id and not force_update:
- orig = Comment.objects.get(id=self.id)
- for key in self.SAVE_ON_CHANGE_FIELDS:
- if getattr(self, key) != getattr(orig, key):
- break
- else:
- self.save_needed = False
- return False
- self.save_needed = True
- return models.Model.save(self, force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
-
- @property
- def attachment_target_path(self):
- if self.id:
- return 'patt/comment/%d' % self.id
- else:
- return 'patt/comment/new'
-
- @property
- def style(self):
- return {
- COMMENTTYPE_APPRAISAL: 'taskappraisal',
- }.get(self.type, 'taskcomment')
-
- @property
- def is_comment(self):
- return self.type == COMMENTTYPE_COMMENT
-
- @property
- def is_appraisal(self):
- return self.type == COMMENTTYPE_APPRAISAL
-
- def sort_string(self):
- return (self.creation_date, self.type, self.comment)
-
- def __str__(self):
- return 'Comment #%d: %s' % (self.id, self.task.name)
-
-
- # SEARCH Model
- #
- class Search(models.Model):
- name = models.CharField(max_length=48)
- search_txt = models.TextField(default='', blank=True)
- user = models.ForeignKey(User, on_delete=models.CASCADE)
|