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.
289 lines
9.9 KiB
289 lines
9.9 KiB
from django.contrib.auth import get_user_model
|
|
from rest_framework import serializers
|
|
from drf_spectacular.utils import extend_schema_field
|
|
|
|
from apps.projects.models.project import (
|
|
Project,
|
|
KanbanColumn,
|
|
KanbanTask,
|
|
ProjectDocument,
|
|
)
|
|
from apps.profiles.models.institution import Institution
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
class ProjectUserMiniSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = User
|
|
fields = ['id', 'email', 'fullname', 'avatar']
|
|
read_only_fields = fields
|
|
|
|
|
|
class ProjectInstitutionMiniSerializer(serializers.ModelSerializer):
|
|
type_display = serializers.CharField(source='get_type_display', read_only=True)
|
|
|
|
class Meta:
|
|
model = Institution
|
|
fields = ['id', 'name', 'slug', 'type', 'type_display', 'avatar', 'city', 'country']
|
|
read_only_fields = fields
|
|
|
|
|
|
class KanbanTaskSerializer(serializers.ModelSerializer):
|
|
priority_display = serializers.CharField(source='get_priority_display', read_only=True)
|
|
column_name = serializers.CharField(source='column.name', read_only=True)
|
|
column_code = serializers.CharField(source='column.code', read_only=True)
|
|
assignee = ProjectUserMiniSerializer(read_only=True)
|
|
|
|
class Meta:
|
|
model = KanbanTask
|
|
fields = [
|
|
'id',
|
|
'project',
|
|
'column',
|
|
'column_name',
|
|
'column_code',
|
|
'title',
|
|
'description',
|
|
'priority',
|
|
'priority_display',
|
|
'assignee',
|
|
'due_date',
|
|
'labels',
|
|
'order',
|
|
'created_at',
|
|
'updated_at',
|
|
]
|
|
read_only_fields = ['id', 'project', 'priority_display', 'column_name', 'column_code', 'created_at', 'updated_at']
|
|
|
|
|
|
class KanbanTaskCreateUpdateSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = KanbanTask
|
|
fields = [
|
|
'column',
|
|
'title',
|
|
'description',
|
|
'priority',
|
|
'assignee',
|
|
'due_date',
|
|
'labels',
|
|
'order',
|
|
]
|
|
|
|
def validate(self, attrs):
|
|
project = self.context.get('project')
|
|
column = attrs.get('column')
|
|
if project and column and column.project != project:
|
|
raise serializers.ValidationError({"column": "Selected column does not belong to this project."})
|
|
return attrs
|
|
|
|
|
|
class KanbanColumnSerializer(serializers.ModelSerializer):
|
|
tasks = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = KanbanColumn
|
|
fields = [
|
|
'id',
|
|
'project',
|
|
'name',
|
|
'code',
|
|
'order',
|
|
'color',
|
|
'tasks',
|
|
]
|
|
read_only_fields = ['id', 'project']
|
|
|
|
@extend_schema_field(KanbanTaskSerializer(many=True))
|
|
def get_tasks(self, obj):
|
|
tasks = obj.tasks.all().select_related('assignee').order_by('order', 'id')
|
|
return KanbanTaskSerializer(tasks, many=True).data
|
|
|
|
|
|
class ProjectDocumentSerializer(serializers.ModelSerializer):
|
|
doc_type_display = serializers.CharField(source='get_doc_type_display', read_only=True)
|
|
uploaded_by_email = serializers.EmailField(source='uploaded_by.email', read_only=True, allow_null=True)
|
|
|
|
class Meta:
|
|
model = ProjectDocument
|
|
fields = [
|
|
'id',
|
|
'project',
|
|
'title',
|
|
'file',
|
|
'doc_type',
|
|
'doc_type_display',
|
|
'uploaded_by_email',
|
|
'uploaded_at',
|
|
]
|
|
read_only_fields = ['id', 'project', 'doc_type_display', 'uploaded_by_email', 'uploaded_at']
|
|
|
|
def create(self, validated_data):
|
|
request = self.context.get('request')
|
|
if request and request.user.is_authenticated:
|
|
validated_data['uploaded_by'] = request.user
|
|
return super().create(validated_data)
|
|
|
|
|
|
class ProjectListSerializer(serializers.ModelSerializer):
|
|
status_display = serializers.CharField(source='get_status_display', read_only=True)
|
|
owner_institution = ProjectInstitutionMiniSerializer(read_only=True)
|
|
collaborating_institutions_count = serializers.SerializerMethodField()
|
|
tasks_count = serializers.SerializerMethodField()
|
|
completed_tasks_count = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = Project
|
|
fields = [
|
|
'id',
|
|
'title',
|
|
'slug',
|
|
'owner_institution',
|
|
'collaborating_institutions_count',
|
|
'category',
|
|
'status',
|
|
'status_display',
|
|
'progress_percentage',
|
|
'budget',
|
|
'team_size',
|
|
'cover_image',
|
|
'start_date',
|
|
'estimated_end_date',
|
|
'tasks_count',
|
|
'completed_tasks_count',
|
|
'created_at',
|
|
]
|
|
read_only_fields = fields
|
|
|
|
@extend_schema_field(serializers.IntegerField)
|
|
def get_collaborating_institutions_count(self, obj) -> int:
|
|
return obj.collaborating_institutions.count()
|
|
|
|
@extend_schema_field(serializers.IntegerField)
|
|
def get_tasks_count(self, obj) -> int:
|
|
return obj.tasks.count()
|
|
|
|
@extend_schema_field(serializers.IntegerField)
|
|
def get_completed_tasks_count(self, obj) -> int:
|
|
return obj.tasks.filter(column__code='done').count()
|
|
|
|
|
|
class ProjectDetailSerializer(serializers.ModelSerializer):
|
|
status_display = serializers.CharField(source='get_status_display', read_only=True)
|
|
owner_institution = ProjectInstitutionMiniSerializer(read_only=True)
|
|
collaborating_institutions = ProjectInstitutionMiniSerializer(many=True, read_only=True)
|
|
columns = KanbanColumnSerializer(many=True, read_only=True)
|
|
documents = ProjectDocumentSerializer(many=True, read_only=True)
|
|
can_edit = serializers.SerializerMethodField()
|
|
created_by_email = serializers.EmailField(source='created_by.email', read_only=True, allow_null=True)
|
|
|
|
class Meta:
|
|
model = Project
|
|
fields = [
|
|
'id',
|
|
'title',
|
|
'slug',
|
|
'description',
|
|
'owner_institution',
|
|
'collaborating_institutions',
|
|
'category',
|
|
'status',
|
|
'status_display',
|
|
'progress_percentage',
|
|
'budget',
|
|
'team_size',
|
|
'cover_image',
|
|
'start_date',
|
|
'estimated_end_date',
|
|
'actual_end_date',
|
|
'columns',
|
|
'documents',
|
|
'can_edit',
|
|
'created_by_email',
|
|
'created_at',
|
|
'updated_at',
|
|
]
|
|
read_only_fields = [
|
|
'id',
|
|
'slug',
|
|
'columns',
|
|
'documents',
|
|
'can_edit',
|
|
'created_by_email',
|
|
'created_at',
|
|
'updated_at',
|
|
]
|
|
|
|
@extend_schema_field(serializers.BooleanField)
|
|
def get_can_edit(self, obj) -> bool:
|
|
request = self.context.get('request')
|
|
if not request or not request.user.is_authenticated:
|
|
return False
|
|
return obj.can_user_edit(request.user)
|
|
|
|
|
|
class ProjectCreateUpdateSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = Project
|
|
fields = [
|
|
'title',
|
|
'description',
|
|
'owner_institution',
|
|
'collaborating_institutions',
|
|
'category',
|
|
'status',
|
|
'progress_percentage',
|
|
'budget',
|
|
'team_size',
|
|
'cover_image',
|
|
'start_date',
|
|
'estimated_end_date',
|
|
'actual_end_date',
|
|
]
|
|
|
|
def validate(self, attrs):
|
|
request = self.context.get('request')
|
|
if request and request.user and request.user.is_authenticated:
|
|
user = request.user
|
|
if getattr(user, 'is_super_admin', False) or getattr(user, 'is_superuser', False):
|
|
return attrs
|
|
|
|
if not self.instance:
|
|
# Project creation: user must be permitted to create on behalf of owner institution
|
|
owner = attrs.get('owner_institution')
|
|
if owner:
|
|
if getattr(user, 'is_regional_admin', False):
|
|
if hasattr(user, 'can_manage_region') and not user.can_manage_region(owner.region):
|
|
raise serializers.ValidationError({
|
|
"owner_institution": "You do not have administrative authority over institutions in this region."
|
|
})
|
|
elif not owner.is_editor(user):
|
|
raise serializers.ValidationError({
|
|
"owner_institution": "You are not an authorized editor or administrator of this institution."
|
|
})
|
|
else:
|
|
# Project update: only validate owner if it is explicitly being modified
|
|
if 'owner_institution' in attrs:
|
|
new_owner = attrs['owner_institution']
|
|
if getattr(user, 'is_regional_admin', False):
|
|
if hasattr(user, 'can_manage_region') and not user.can_manage_region(new_owner.region):
|
|
raise serializers.ValidationError({
|
|
"owner_institution": "You do not have administrative authority over institutions in this region."
|
|
})
|
|
elif not new_owner.is_editor(user):
|
|
raise serializers.ValidationError({
|
|
"owner_institution": "You are not an authorized editor or administrator of this institution."
|
|
})
|
|
return attrs
|
|
|
|
def create(self, validated_data):
|
|
request = self.context.get('request')
|
|
collaborating = validated_data.pop('collaborating_institutions', [])
|
|
if request and request.user.is_authenticated:
|
|
validated_data['created_by'] = request.user
|
|
|
|
project = super().create(validated_data)
|
|
if collaborating:
|
|
project.collaborating_institutions.set(collaborating)
|
|
return project
|