class AuditLogEntry(models.BaseModel): """Auditlog model""" ACTION_ADDED = 10 ACTION_CHANGED = 20 ACTION_DELETED = 30 action_choices = [ (ACTION_ADDED, _('Added')), (ACTION_CHANGED, _('Changed')), (ACTION_DELETED, _('Deleted')), ] content_type = models.ForeignKey('contenttypes.ContentType', models.CASCADE, related_name='+') object_id = models.BigIntegerField(blank=True, null=True) content_object = fields.GenericForeignKey('content_type', 'object_id') object_verbose_name = models.TextField(default='', blank=True) user = models.ForeignKey(settings.AUTH_USER_MODEL, models.SET_NULL, blank=True, null=True, related_name='+') action = models.IntegerField(choices=action_choices) changes = models.JSONField() class Meta: """Model meta description""" indexes = [ models.Index(fields=['content_type', 'object_id']), ]
class LogEntry(models.Model): """Log entry event""" log = models.ForeignKey(Log, models.CASCADE, related_name='entries') log_time = models.DateTimeField(_('Log time')) user = models.ForeignKey(settings.AUTH_USER_MODEL, models.SET_NULL, null=True, blank=True, verbose_name=_('User')) path = models.TextField(_('Path'), default='') user_agent = models.TextField(_('User agent'), default='') class Meta: """Model meta description""" verbose_name = _('Log entry') verbose_name_plural = _('Log entries')
class Comment(models.BaseModel): item = models.ForeignKey(Item, related_name='comments', on_delete=models.CASCADE) comment = models.TextField(default='') def generate_verbose_name(self): comment = strip_tags(self.comment) if len(comment) > 20: return f"{comment:.20}..." return comment
class WorkLog(models.BaseModel): item = models.ForeignKey(Item, related_name='worklogs', on_delete=models.CASCADE) date = models.DateField() worked = models.FloatField() billed = models.FloatField( null=True, blank=True, help_text= 'On empty this wil be auto filled based on estimate and remaining billed hours, when there is no estimate billed will always be same as worked' ) description = models.TextField(default='', null=True, blank=True) def save(self, *args, **kwargs): result = self.item.worklogs.exclude(id=self.id).aggregate( total_worked=models.Sum('worked'), total_billed=models.Sum('billed'), ) self.item.total_worked = (float(result['total_worked']) if result['total_worked'] else 0.0) + float( self.worked) total_billed = float( result['total_billed']) if result['total_billed'] else 0.0 if self.item.non_billable: self.billed = 0 self.item.total_billed = 0 elif not self.billed and not self.item.estimate: self.billed = self.worked elif not self.billed: available = float(self.item.estimate or 0.0) - total_billed if available > 0: self.billed = available if self.worked > available else float( self.worked) else: self.billed = 0 self.item.total_billed = total_billed + float(self.billed) if not self.description: self.description = f'Working on item {self.item.code}' self.item.save() super().save(*args, **kwargs) def generate_verbose_name(self): description = strip_tags(self.description) if len(description) > 20: return f"{description:.20}..." return description
class UserAttribute(models.Model): """User attribute to store system values for user""" user = models.ForeignKey(settings.AUTH_USER_MODEL, models.CASCADE, related_name='attributes') code = models.CharField(max_length=128, null=False) value = models.JSONField() objects = UserAttributeManager() class Meta: """Model meta description""" unique_together = ('user', 'code') def __str__(self): """User Attribute representation""" return self.code
class Item(models.BaseModel): TYPE_FEATURE = 10 TYPE_ENHANCEMENT = 20 TYPE_TASK = 30 TYPE_BUG = 40 TYPE_QUESTION = 50 TYPE_CHOICES = ( (TYPE_FEATURE, _('Feature')), (TYPE_ENHANCEMENT, _('Enhancement')), (TYPE_TASK, _('Task')), (TYPE_BUG, _('Bug')), (TYPE_QUESTION, _('Question')), ) PRIORITY_HIGHEST = 50 PRIORITY_HIGH = 40 PRIORITY_MEDIUM = 30 PRIORITY_LOW = 20 PRIORITY_LOWEST = 10 PRIORITY_CHOICES = ( (PRIORITY_HIGHEST, _('Highest')), (PRIORITY_HIGH, _('High')), (PRIORITY_MEDIUM, _('Medium')), (PRIORITY_LOW, _('Low')), (PRIORITY_LOWEST, _('Lowest')), ) project = models.ForeignKey(Project, related_name='items', on_delete=models.CASCADE) item_type = models.IntegerField(choices=TYPE_CHOICES, default=TYPE_FEATURE, blank=True) priority = models.IntegerField(choices=PRIORITY_CHOICES, default=PRIORITY_MEDIUM, blank=True) code = models.CharField(max_length=32) name = models.CharField(max_length=256) description = models.TextField(default='', null=True, blank=True) completed_on = models.DateField(default=None, null=True, blank=True) estimate = models.FloatField(null=True, blank=True) non_billable = models.BooleanField(default=False, blank=True) # Item stats total_worked = models.FloatField(default=0.0, blank=True) total_billed = models.FloatField(default=0.0, blank=True) @property def estimate_used(self): return 0.0 def save(self, *args, **kwargs): super().save(*args, **kwargs) if not self.code and self.project: with CacheLock('set-item-code', self.project_id): self.project.refresh_from_db() # get latest value self.project.item_increment_id += 1 self.code = f"{self.project.code}-{self.project.item_increment_id}" self.save(update_fields=['code']) self.project.save(update_fields=['item_increment_id']) @classmethod def get_type_icon(cls, item_type): icon_mapping = { cls.TYPE_FEATURE: 'fa fa-star', cls.TYPE_ENHANCEMENT: 'fa fa-bolt', cls.TYPE_TASK: 'fa fa-check', cls.TYPE_BUG: 'fa fa-bug', cls.TYPE_QUESTION: 'fa fa-question-circle', } badge_mapping = { cls.TYPE_FEATURE: 'badge-feature', cls.TYPE_ENHANCEMENT: 'badge-enhancement', cls.TYPE_TASK: 'badge-task', cls.TYPE_BUG: 'badge-bug', cls.TYPE_QUESTION: 'badge-question', } return f"<span class='badge badge-icon {badge_mapping[item_type]}'><i class='{icon_mapping[item_type]}'></i></span>" @classmethod def get_priority_icon(cls, priority): icon = 'fa fa-long-arrow-up' if priority <= cls.PRIORITY_LOW: icon = 'fa fa-long-arrow-down' class_mapping = { cls.PRIORITY_HIGHEST: 'priority-icon-highest', cls.PRIORITY_HIGH: 'priority-icon-high', cls.PRIORITY_MEDIUM: 'priority-icon-medium', cls.PRIORITY_LOW: 'priority-icon-low', cls.PRIORITY_LOWEST: 'priority-icon-lowest', } return f"<i class='{class_mapping[priority]} {icon}'></i>"
class Project(models.BaseModel): STATUS_DRAFT = 10 STATUS_ACTIVE = 20 STATUS_ON_HOLD = 30 STATUS_COMPLETED = 40 STATUS_CANCELED = 99 STATUS_CHOICES = [ (STATUS_DRAFT, _('Draft')), (STATUS_ACTIVE, _('Active')), (STATUS_ON_HOLD, _('On hold')), (STATUS_COMPLETED, _('Completed')), (STATUS_CANCELED, _('Canceled')), ] TYPE_FIXED = 10 TYPE_HOURLY_BASED = 20 TYPE_CHOICES = [ (TYPE_FIXED, _('Fixed')), (TYPE_HOURLY_BASED, _('Hourly based')), ] # Connect Project to other Model for_object_type = models.ForeignKey( 'contenttypes.ContentType', models.SET_NULL, blank=True, null=True, verbose_name=_('Object type'), ) for_object_id = models.BigIntegerField(_('Object ID'), blank=True, null=True) for_object = GenericForeignKey('for_object_type', 'for_object_id') name = models.CharField(max_length=256) code = models.CharField(max_length=10, unique=True) status = models.IntegerField(choices=STATUS_CHOICES, default=STATUS_DRAFT) project_type = models.IntegerField(choices=TYPE_CHOICES, default=TYPE_FIXED) description = models.TextField(default='', null=True, blank=True) deadline = models.DateField(default=None, null=True, blank=True) started_on = models.DateField(default=None, null=True, blank=True) completed_on = models.DateField(default=None, null=True, blank=True) fixed_price = models.PriceField(null=True, blank=True) project_hourly_rate = models.PriceField(null=True, blank=True) item_increment_id = models.IntegerField(default=0) # Project stats open_items = models.IntegerField(default=0) completed_items = models.IntegerField(default=0) total_items_estimate = models.FloatField(default=0.0) total_worked = models.FloatField(default=0.0) total_billed = models.FloatField(default=0.0) @property def hourly_rate(self): return float(self.project_hourly_rate if self. project_hourly_rate else app_settings.HOURLY_RATE) @property def estimate_used(self): return 0.0 def save(self, *args, **kwargs): self.code = str(self.code).upper() super().save(*args, **kwargs)
class Task(models.BaseModel): """Task model""" SCHEDULED = 10 QUEUE = 20 LOCKED = 30 RUNNING = 40 COMPLETED = 50 FAILED = 99 STATUS_CHOICES = ( (SCHEDULED, _('Scheduled')), (QUEUE, _('Queue')), (LOCKED, _('Locked')), (RUNNING, _('Running')), (COMPLETED, _('Completed')), (FAILED, _('Failed')), ) celery_task_id = models.CharField(max_length=64, unique=True) status = models.IntegerField(_('Status'), choices=STATUS_CHOICES, default=QUEUE) identifier = models.CharField(_('Identifier'), max_length=255, default='not_set') description = models.TextField(_('Description'), default='') progress = models.PositiveIntegerField(_('Progress'), default=0) progress_output = models.JSONField(_('Progress output'), default=list) scheduled_at = models.DateTimeField(_('Scheduled at'), blank=True, null=True) started_at = models.DateTimeField(_('started at'), blank=True, null=True) execution_time = models.IntegerField(_('Execution time'), default=0) result = models.TextField(_('Result'), blank=True, default='') user = models.ForeignKey(settings.AUTH_USER_MODEL, models.SET_NULL, blank=True, null=True, verbose_name=_('Started by')) object_type = models.ForeignKey('contenttypes.ContentType', models.SET_NULL, blank=True, null=True, related_name='+', verbose_name=_('Object type')) object_id = models.BigIntegerField(_('Object ID'), blank=True, null=True) object = fields.GenericForeignKey('object_type', 'object_id') object_verbose_name = models.TextField(_('Object name'), blank=True, default='') class Meta: """Model meta description""" verbose_name = _('Task') verbose_name_plural = _('Tasks') def cancel_celery_task(self, kill=False): """ Make sure we cancel the task (if in queue/scheduled). :param: kill Also kill the task if it's running, defaults to False. """ celery_control = Control(current_app) celery_control.revoke(task_id=self.celery_task_id, terminate=kill)