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]}..."