You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

323 lines
9.2 KiB

import random
from django.conf import settings
from django.db import models
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from apps.profiles.models.institution import Institution
class Project(models.Model):
class Status(models.TextChoices):
PLANNING = 'planning', _('Planning')
ACTIVE = 'active', _('Active / In Progress')
ON_HOLD = 'on_hold', _('On Hold')
COMPLETED = 'completed', _('Completed')
title = models.CharField(
max_length=255,
verbose_name=_('Project Title')
)
slug = models.SlugField(
max_length=255,
unique=True,
allow_unicode=True,
blank=True,
null=True,
verbose_name=_('Slug / URL Identifier')
)
description = models.TextField(
blank=True,
null=True,
verbose_name=_('Project Description & Objectives')
)
owner_institution = models.ForeignKey(
Institution,
on_delete=models.CASCADE,
related_name='owned_projects',
verbose_name=_('Lead / Owner Institution')
)
collaborating_institutions = models.ManyToManyField(
Institution,
blank=True,
related_name='collaborative_projects',
verbose_name=_('Collaborating Institutions')
)
category = models.CharField(
max_length=100,
blank=True,
null=True,
verbose_name=_('Project Category'),
help_text=_('e.g. Cultural Diplomacy, Educational Academy, Interfaith Dialogue, Humanitarian Relief')
)
status = models.CharField(
max_length=20,
choices=Status.choices,
default=Status.ACTIVE,
verbose_name=_('Project Status')
)
progress_percentage = models.PositiveSmallIntegerField(
default=0,
verbose_name=_('Progress Percentage (0-100)')
)
budget = models.CharField(
max_length=100,
blank=True,
null=True,
verbose_name=_('Estimated Budget'),
help_text=_('e.g. $50,000 or €35,000')
)
team_size = models.PositiveIntegerField(
default=1,
verbose_name=_('Team Size')
)
cover_image = models.ImageField(
upload_to='projects/covers/%Y/%m/',
blank=True,
null=True,
verbose_name=_('Cover Image')
)
start_date = models.DateField(
null=True,
blank=True,
verbose_name=_('Start Date')
)
estimated_end_date = models.DateField(
null=True,
blank=True,
verbose_name=_('Estimated End Date')
)
actual_end_date = models.DateField(
null=True,
blank=True,
verbose_name=_('Actual Completion Date')
)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='created_projects',
verbose_name=_('Created By')
)
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name=_('Created At')
)
updated_at = models.DateTimeField(
auto_now=True,
verbose_name=_('Updated At')
)
class Meta:
ordering = ('-created_at',)
verbose_name = _('Collaborative Project')
verbose_name_plural = _('Collaborative Projects')
def __str__(self):
return f"{self.title} ({self.owner_institution.name})"
def save(self, *args, **kwargs):
is_new = self.pk is None
if not self.slug:
base_slug = slugify(self.title, allow_unicode=True) or f"project-{random.randint(1000, 9999)}"
slug = base_slug
counter = 1
while Project.objects.filter(slug=slug).exclude(pk=self.pk).exists():
slug = f"{base_slug}-{counter}"
counter += 1
self.slug = slug
super().save(*args, **kwargs)
if is_new:
self._create_default_kanban_columns()
def _create_default_kanban_columns(self):
default_columns = [
{'name': 'To Do', 'code': 'todo', 'order': 1, 'color': '#94a3b8'},
{'name': 'In Progress', 'code': 'in_progress', 'order': 2, 'color': '#3b82f6'},
{'name': 'Review', 'code': 'review', 'order': 3, 'color': '#f59e0b'},
{'name': 'Done', 'code': 'done', 'order': 4, 'color': '#10b981'},
]
for col in default_columns:
KanbanColumn.objects.create(
project=self,
name=col['name'],
code=col['code'],
order=col['order'],
color=col['color']
)
def can_user_edit(self, user):
if not user or not user.is_authenticated:
return False
if getattr(user, 'is_super_admin', False) or getattr(user, 'is_regional_admin', False):
return True
# Check owner institution
if self.owner_institution.is_editor(user):
return True
# Check collaborating institutions
for collab in self.collaborating_institutions.all():
if collab.is_editor(user):
return True
return False
class KanbanColumn(models.Model):
project = models.ForeignKey(
Project,
on_delete=models.CASCADE,
related_name='columns',
verbose_name=_('Project')
)
name = models.CharField(
max_length=100,
verbose_name=_('Column Name')
)
code = models.CharField(
max_length=50,
default='todo',
verbose_name=_('Column Code Identifier')
)
order = models.IntegerField(
default=0,
verbose_name=_('Display Order')
)
color = models.CharField(
max_length=20,
default='#6366f1',
blank=True,
null=True,
verbose_name=_('Badge / Header Color')
)
class Meta:
ordering = ('order', 'id')
verbose_name = _('Kanban Column')
verbose_name_plural = _('Kanban Columns')
def __str__(self):
return f"{self.project.title} - {self.name}"
class KanbanTask(models.Model):
class Priority(models.TextChoices):
LOW = 'low', _('Low')
MEDIUM = 'medium', _('Medium')
HIGH = 'high', _('High')
URGENT = 'urgent', _('Urgent')
project = models.ForeignKey(
Project,
on_delete=models.CASCADE,
related_name='tasks',
verbose_name=_('Project')
)
column = models.ForeignKey(
KanbanColumn,
on_delete=models.CASCADE,
related_name='tasks',
verbose_name=_('Kanban Column')
)
title = models.CharField(
max_length=255,
verbose_name=_('Task Title')
)
description = models.TextField(
blank=True,
null=True,
verbose_name=_('Task Description')
)
priority = models.CharField(
max_length=20,
choices=Priority.choices,
default=Priority.MEDIUM,
verbose_name=_('Priority')
)
assignee = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='assigned_tasks',
verbose_name=_('Assignee User')
)
due_date = models.DateField(
null=True,
blank=True,
verbose_name=_('Due Date')
)
labels = models.JSONField(
default=list,
blank=True,
verbose_name=_('Task Labels')
)
order = models.IntegerField(
default=0,
verbose_name=_('Display Order within Column')
)
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name=_('Created At')
)
updated_at = models.DateTimeField(
auto_now=True,
verbose_name=_('Updated At')
)
class Meta:
ordering = ('order', '-updated_at')
verbose_name = _('Kanban Task')
verbose_name_plural = _('Kanban Tasks')
def __str__(self):
return f"{self.title} ({self.column.name})"
class ProjectDocument(models.Model):
class DocumentType(models.TextChoices):
MOU = 'mou', _('MOU / Bilateral Agreement')
REPORT = 'report', _('Progress / Milestone Report')
CONTRACT = 'contract', _('Contract / Agreement')
OTHER = 'other', _('Other Document')
project = models.ForeignKey(
Project,
on_delete=models.CASCADE,
related_name='documents',
verbose_name=_('Project')
)
title = models.CharField(
max_length=255,
verbose_name=_('Document Title')
)
file = models.FileField(
upload_to='projects/docs/%Y/%m/',
verbose_name=_('Document File')
)
doc_type = models.CharField(
max_length=50,
choices=DocumentType.choices,
default=DocumentType.MOU,
verbose_name=_('Document Type')
)
uploaded_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='uploaded_project_documents',
verbose_name=_('Uploaded By')
)
uploaded_at = models.DateTimeField(
auto_now_add=True,
verbose_name=_('Uploaded At')
)
class Meta:
ordering = ('-uploaded_at',)
verbose_name = _('Project Document')
verbose_name_plural = _('Project Documents')
def __str__(self):
return f"{self.project.title} - {self.title}"