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): return True if getattr(user, 'is_regional_admin', False): if hasattr(user, 'can_manage_region'): if self.owner_institution and user.can_manage_region(self.owner_institution.region): return True for collab in self.collaborating_institutions.all(): if user.can_manage_region(collab.region): return True return False # Check owner institution if self.owner_institution and 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}"