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

122 lines
4.1 KiB
Python

from django.db import models, transaction
from core.models.base import BaseModel
from core.models.account import Account, AccountAddress
from core.models.project import Project
from core.models.project_scope import (
ProjectScope,
ProjectScopeCategory,
ProjectScopeTask,
)
class ProjectScopeTemplate(BaseModel):
"""Reusable blueprint for creating a ProjectScope with Categories and Tasks"""
name = models.CharField(max_length=255, unique=True)
description = models.TextField(blank=True)
is_active = models.BooleanField(default=True)
class Meta:
ordering = ['name']
verbose_name = "Project Scope Template"
verbose_name_plural = "Project Scope Templates"
indexes = [models.Index(fields=['is_active'])]
def __str__(self):
return self.name
@transaction.atomic
def instantiate(
self,
*,
project: Project,
account: Account | None = None,
account_address: AccountAddress | None = None,
name: str | None = None,
description: str | None = None,
is_active: bool = True,
) -> ProjectScope:
"""
Create a ProjectScope (and nested Categories/Tasks) from this template.
- If an account is not provided, tries to use project.account (when present).
"""
resolved_account = account or getattr(project, "account", None)
scope = ProjectScope.objects.create(
name=name or self.name,
account=resolved_account,
project=project,
account_address=account_address,
description=description if description is not None else self.description,
is_active=is_active,
)
# Create Categories and Tasks preserving order
category_templates = self.category_templates.all().order_by('order', 'name', 'id')
for ct in category_templates:
category = ProjectScopeCategory.objects.create(
scope=scope,
name=ct.name,
order=ct.order,
)
task_templates = ct.task_templates.all().order_by('order', 'id')
tasks_to_create = [
ProjectScopeTask(
category=category,
description=tt.description,
checklist_description=tt.checklist_description,
order=tt.order,
estimated_minutes=tt.estimated_minutes,
)
for tt in task_templates
]
if tasks_to_create:
ProjectScopeTask.objects.bulk_create(tasks_to_create)
return scope
class ProjectAreaTemplate(BaseModel):
"""Reusable category definition belonging to a ProjectScopeTemplate"""
scope_template = models.ForeignKey(
ProjectScopeTemplate,
on_delete=models.CASCADE,
related_name='category_templates',
)
name = models.CharField(max_length=255)
order = models.PositiveIntegerField(default=0)
class Meta:
ordering = ['scope_template', 'order', 'name']
verbose_name = "Project Area Template"
verbose_name_plural = "Project Area Templates"
indexes = [models.Index(fields=['scope_template', 'order'])]
def __str__(self):
return f"{self.scope_template.name} - {self.name}"
class ProjectTaskTemplate(BaseModel):
"""Reusable task definition belonging to a ProjectAreaTemplate"""
area_template = models.ForeignKey(
ProjectAreaTemplate,
on_delete=models.CASCADE,
related_name='task_templates',
)
description = models.TextField()
checklist_description = models.TextField(blank=True)
order = models.PositiveIntegerField(default=0)
estimated_minutes = models.PositiveIntegerField(null=True, blank=True)
class Meta:
ordering = ['area_template', 'order', 'id']
verbose_name = "Project Task Template"
verbose_name_plural = "Project Task Templates"
indexes = [
models.Index(fields=['area_template', 'order']),
]
def __str__(self):
return f"{self.area_template.name}: {self.description[:50]}..."