122 lines
4.1 KiB
Python
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]}..."
|