nexus-5/core/models/project_scope.py
2026-01-26 11:09:40 -05:00

97 lines
3.7 KiB
Python

from django.db import models
from core.models.profile import TeamProfile
from core.models.account import Account, AccountAddress
from core.models.project import Project
from core.models.base import BaseModel
class ProjectScope(BaseModel):
"""Scope of work definition for a project"""
name = models.CharField(max_length=255)
account = models.ForeignKey(
Account,
on_delete=models.PROTECT,
related_name='project_scopes',
blank=True,
null=True,
)
project = models.ForeignKey(Project, on_delete=models.PROTECT, related_name='project_scopes')
account_address = models.ForeignKey(
AccountAddress,
on_delete=models.PROTECT,
related_name='project_scopes',
null=True,
blank=True,
)
description = models.TextField(blank=True)
is_active = models.BooleanField(default=True)
class Meta:
ordering = ['name']
verbose_name = "Project Scope"
verbose_name_plural = "Project Scopes"
indexes = [
models.Index(fields=['project', 'is_active']),
models.Index(fields=['account_address', 'is_active']),
]
constraints = [
# Ensure only one active scope per project/address combination (when address present)
models.UniqueConstraint(
fields=['project', 'account_address'],
condition=models.Q(is_active=True, account_address__isnull=False),
name='unique_active_project_scope_per_address',
),
]
def __str__(self):
project_label = str(self.project_id) if self.project_id else "Unassigned Project"
return f"{self.name} - {project_label}"
class ProjectScopeCategory(BaseModel):
"""Category of work definition for a project"""
name = models.CharField(max_length=255)
scope = models.ForeignKey(ProjectScope, on_delete=models.CASCADE, related_name='project_areas')
order = models.PositiveIntegerField(default=0)
class Meta:
ordering = ['scope', 'order', 'name']
verbose_name = "Project Scope Category"
verbose_name_plural = "Project Scope Categories"
indexes = [
models.Index(fields=['scope', 'order']),
]
def __str__(self):
return f"{self.scope.name} - {self.name}"
class ProjectScopeTask(BaseModel):
"""Specific task definition for a project"""
category = models.ForeignKey(ProjectScopeCategory, on_delete=models.CASCADE, related_name='project_tasks')
description = models.TextField()
checklist_description = models.TextField()
order = models.PositiveIntegerField(default=0)
estimated_minutes = models.PositiveIntegerField(null=True, blank=True)
class Meta:
ordering = ['category', 'order']
verbose_name = "Project Scope Task"
verbose_name_plural = "Project Scope Tasks"
indexes = [
models.Index(fields=['category', 'order']),
]
def __str__(self):
return f"{self.category.name}: {self.description[:50]}..."
class ProjectScopeTaskCompletion(BaseModel):
"""Record of a task template being completed during a project"""
task = models.ForeignKey(ProjectScopeTask, on_delete=models.PROTECT, related_name='completions')
project = models.ForeignKey(Project, on_delete=models.PROTECT, related_name='task_completions')
account = models.ForeignKey(Account, on_delete=models.PROTECT, related_name='task_completions', null=True, blank=True)
account_address = models.ForeignKey(AccountAddress, on_delete=models.PROTECT, null=True, blank=True, related_name='project_task_completions')
completed_by = models.ForeignKey(TeamProfile, on_delete=models.PROTECT)
completed_at = models.DateTimeField()
notes = models.TextField(blank=True)