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)