317 lignes
11 KiB
Python
317 lignes
11 KiB
Python
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)
|