feat: project group can be belonged to many departments

This commit is contained in:
2025-09-22 11:33:18 +08:00
parent 2d6761a436
commit 24256f588c
8 changed files with 71 additions and 104 deletions
+4 -3
View File
@@ -312,10 +312,11 @@ class EmailNotificationMiddleware(MiddlewareMixin):
notification_data = { notification_data = {
'id': project_group_data.get('id', ''), 'id': project_group_data.get('id', ''),
'name': project_group_data.get('name', ''), 'name': project_group_data.get('name', ''),
'department': project_group_data.get('department', ''), 'departments': project_group_data.get('departments', ''),
'department_name': project_group_data.get('department_name', ''), 'department_names': project_group_data.get('department_names', ''),
'departments_info': project_group_data.get('departments_info', ''),
'description': project_group_data.get('description', ''), 'description': project_group_data.get('description', ''),
'timestamp': project_group_data.get('updated_at', ''), 'timestamp': project_group_data.get('created_at', ''),
'operation_path': request.path, 'operation_path': request.path,
'operation_method': request.method 'operation_method': request.method
} }
+10 -1
View File
@@ -66,6 +66,7 @@ LABEL_MAP = {
'grade_major': '年级专业', 'grade_major': '年级专业',
'project_group': '项目组', 'project_group': '项目组',
'project_group_name': '项目组', 'project_group_name': '项目组',
'department_names': '所属部门',
'position': '职位', 'position': '职位',
'start_date': '开始日期', 'start_date': '开始日期',
'end_date': '结束日期', 'end_date': '结束日期',
@@ -91,7 +92,15 @@ def _format_value(value):
return str(value) return str(value)
if isinstance(value, bool): if isinstance(value, bool):
return '' if value else '' return '' if value else ''
if isinstance(value, (list, dict)): if isinstance(value, list):
# 对于列表,优先显示为逗号分隔的字符串
if all(isinstance(item, str) for item in value):
return ', '.join(value)
try:
return json.dumps(value, ensure_ascii=False)
except Exception:
return str(value)
if isinstance(value, dict):
try: try:
return json.dumps(value, ensure_ascii=False) return json.dumps(value, ensure_ascii=False)
except Exception: except Exception:
+10 -69
View File
@@ -6,12 +6,18 @@ from .models import Personnel, ProjectGroup
@admin.register(ProjectGroup) @admin.register(ProjectGroup)
class ProjectGroupAdmin(admin.ModelAdmin): class ProjectGroupAdmin(admin.ModelAdmin):
"""项目组管理""" """项目组管理"""
list_display = ['name', 'department', 'description', 'created_at'] list_display = ['name', 'get_departments', 'description', 'created_at']
list_filter = ['department', 'created_at'] list_filter = ['departments', 'created_at']
search_fields = ['name', 'description'] search_fields = ['name', 'description']
ordering = ['department', 'name'] ordering = ['name']
filter_horizontal = ['departments']
def get_departments(self, obj):
"""获取部门列表显示"""
return ", ".join([dept.name for dept in obj.departments.all()])
get_departments.short_description = '所属部门'
import django_filters
@admin.register(Personnel) @admin.register(Personnel)
class PersonnelAdmin(admin.ModelAdmin): class PersonnelAdmin(admin.ModelAdmin):
"""人员信息管理""" """人员信息管理"""
@@ -51,68 +57,3 @@ class PersonnelAdmin(admin.ModelAdmin):
def get_queryset(self, request): def get_queryset(self, request):
"""优化查询""" """优化查询"""
return super().get_queryset(request).select_related('department', 'project_group') return super().get_queryset(request).select_related('department', 'project_group')
from .models import Personnel, ProjectGroup
from finance.models import Department
class PersonnelFilter(django_filters.FilterSet):
"""人员信息过滤器"""
# 基本筛选
department = django_filters.ModelChoiceFilter(
queryset=Department.objects.all(),
field_name='department',
label='部门'
)
project_group = django_filters.ModelChoiceFilter(
queryset=ProjectGroup.objects.all(),
field_name='project_group',
label='项目组'
)
position = django_filters.CharFilter(
field_name='position',
lookup_expr='icontains',
label='职位'
)
gender = django_filters.ChoiceFilter(
choices=Personnel.GENDER_CHOICES,
field_name='gender',
label='性别'
)
is_active = django_filters.BooleanFilter(
field_name='is_active',
label='在职状态'
)
# 日期范围筛选
start_date_from = django_filters.DateFilter(
field_name='start_date',
lookup_expr='gte',
label='任职开始时间(从)'
)
start_date_to = django_filters.DateFilter(
field_name='start_date',
lookup_expr='lte',
label='任职开始时间(到)'
)
end_date_from = django_filters.DateFilter(
field_name='end_date',
lookup_expr='gte',
label='任职结束时间(从)'
)
end_date_to = django_filters.DateFilter(
field_name='end_date',
lookup_expr='lte',
label='任职结束时间(到)'
)
class Meta:
model = Personnel
fields = ['department', 'project_group', 'position', 'gender', 'is_active']
+9 -3
View File
@@ -6,17 +6,23 @@ from finance.models import Department
class ProjectGroup(models.Model): class ProjectGroup(models.Model):
"""项目组模型""" """项目组模型"""
name = models.CharField(max_length=100, verbose_name="项目组名称") name = models.CharField(max_length=100, verbose_name="项目组名称")
department = models.ForeignKey(Department, on_delete=models.CASCADE, verbose_name="所属部门") departments = models.ManyToManyField(Department, verbose_name="所属部门", related_name="project_groups")
description = models.TextField(blank=True, null=True, verbose_name="项目组描述") description = models.TextField(blank=True, null=True, verbose_name="项目组描述")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
class Meta: class Meta:
verbose_name = "项目组" verbose_name = "项目组"
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
ordering = ['department', 'name'] ordering = ['name']
def __str__(self): def __str__(self):
return f"{self.department.name} - {self.name}" department_names = ", ".join([dept.name for dept in self.departments.all()])
return f"{self.name} ({department_names})" if department_names else self.name
@property
def department_names(self):
"""返回所属部门名称列表"""
return [dept.name for dept in self.departments.all()]
class Personnel(models.Model): class Personnel(models.Model):
+12 -5
View File
@@ -6,13 +6,18 @@ from .models import Personnel, ProjectGroup
class ProjectGroupSerializer(serializers.ModelSerializer): class ProjectGroupSerializer(serializers.ModelSerializer):
"""项目组序列化器""" """项目组序列化器"""
department_name = serializers.CharField(source='department.name', read_only=True) department_names = serializers.ListField(read_only=True)
departments_info = serializers.SerializerMethodField()
class Meta: class Meta:
model = ProjectGroup model = ProjectGroup
fields = ['id', 'name', 'department', 'department_name', 'description', 'created_at'] fields = ['id', 'name', 'departments', 'department_names', 'departments_info', 'description', 'created_at']
read_only_fields = ['created_at'] read_only_fields = ['created_at']
def get_departments_info(self, obj):
"""获取部门详细信息"""
return [{'id': dept.id, 'name': dept.name} for dept in obj.departments.all()]
class PersonnelReadSerializer(serializers.ModelSerializer): class PersonnelReadSerializer(serializers.ModelSerializer):
"""人员信息读取序列化器""" """人员信息读取序列化器"""
@@ -45,11 +50,13 @@ class PersonnelWriteSerializer(serializers.ModelSerializer):
] ]
def validate_project_group(self, value): def validate_project_group(self, value):
"""验证项目组是否属于选定的部门""" """验证项目组是否包含选定的部门"""
if value and hasattr(self, 'initial_data'): if value and hasattr(self, 'initial_data'):
department_id = self.initial_data.get('department') department_id = self.initial_data.get('department')
if department_id and value.department_id != int(department_id): if department_id:
raise serializers.ValidationError("项目组必须属于选定的部门") # 检查项目组的部门列表中是否包含选定的部门
if not value.departments.filter(id=int(department_id)).exists():
raise serializers.ValidationError("项目组必须包含选定的部门")
return value return value
def validate(self, attrs): def validate(self, attrs):
+4 -14
View File
@@ -197,16 +197,16 @@ class ProjectGroupViewSet(viewsets.ModelViewSet):
queryset = ProjectGroup.objects.all() queryset = ProjectGroup.objects.all()
serializer_class = ProjectGroupSerializer serializer_class = ProjectGroupSerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['department'] filterset_fields = ['departments']
search_fields = ['name', 'description'] search_fields = ['name', 'description']
ordering = ['department', 'name'] ordering = ['name']
@action(detail=False, methods=['get']) @action(detail=False, methods=['get'])
def by_department(self, request): def by_department(self, request):
"""按部门获取项目组""" """按部门获取项目组"""
department_id = request.query_params.get('department_id') department_id = request.query_params.get('department_id')
if department_id: if department_id:
project_groups = ProjectGroup.objects.filter(department_id=department_id) project_groups = ProjectGroup.objects.filter(departments__id=department_id).distinct()
serializer = self.get_serializer(project_groups, many=True) serializer = self.get_serializer(project_groups, many=True)
return Response(serializer.data) return Response(serializer.data)
return Response([]) return Response([])
@@ -221,7 +221,7 @@ class ProjectGroupViewSet(viewsets.ModelViewSet):
project_group_data = { project_group_data = {
'id': project_group.id, 'id': project_group.id,
'name': "[已删除]" + project_group.name, 'name': "[已删除]" + project_group.name,
'department_name': project_group.department.name if project_group.department else '', 'department_names': project_group.department_names,
'description': project_group.description, 'description': project_group.description,
'created_at': str(project_group.created_at), 'created_at': str(project_group.created_at),
'operation_path': request.path, 'operation_path': request.path,
@@ -267,13 +267,3 @@ class ProjectGroupViewSet(viewsets.ModelViewSet):
else: else:
ip = request.META.get('REMOTE_ADDR') ip = request.META.get('REMOTE_ADDR')
return ip return ip
@action(detail=False, methods=['get'])
def by_department(self, request):
"""按部门获取项目组"""
department_id = request.query_params.get('department_id')
if department_id:
project_groups = ProjectGroup.objects.filter(department_id=department_id)
serializer = self.get_serializer(project_groups, many=True)
return Response(serializer.data)
return Response([])
@@ -54,7 +54,13 @@
<el-table :data="filteredProjectGroups" style="width: 100%" v-loading="projectGroupLoading"> <el-table :data="filteredProjectGroups" style="width: 100%" v-loading="projectGroupLoading">
<el-table-column prop="name" label="项目组名称" /> <el-table-column prop="name" label="项目组名称" />
<el-table-column prop="department_name" label="所属部门" /> <el-table-column label="所属部门" min-width="150">
<template #default="scope">
<el-tag v-for="dept in scope.row.departments_info" :key="dept.id" size="small" style="margin-right: 5px;">
{{ dept.name }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="description" label="描述" show-overflow-tooltip /> <el-table-column prop="description" label="描述" show-overflow-tooltip />
<el-table-column label="操作" width="150"> <el-table-column label="操作" width="150">
<template #default="scope"> <template #default="scope">
@@ -86,8 +92,8 @@
<el-form-item label="项目组名称" prop="name"> <el-form-item label="项目组名称" prop="name">
<el-input v-model="projectGroupForm.name" placeholder="请输入项目组名称" /> <el-input v-model="projectGroupForm.name" placeholder="请输入项目组名称" />
</el-form-item> </el-form-item>
<el-form-item label="所属部门" prop="department"> <el-form-item label="所属部门" prop="departments">
<el-select v-model="projectGroupForm.department" placeholder="请选择所属部门"> <el-select v-model="projectGroupForm.departments" placeholder="请选择所属部门" multiple>
<el-option <el-option
v-for="dept in departments" v-for="dept in departments"
:key="dept.id" :key="dept.id"
@@ -152,14 +158,14 @@ export default {
projectGroupForm: { projectGroupForm: {
id: null, id: null,
name: '', name: '',
department: '', departments: [],
description: '' description: ''
}, },
projectGroupRules: { projectGroupRules: {
name: [ name: [
{ required: true, message: '请输入项目组名称', trigger: 'blur' } { required: true, message: '请输入项目组名称', trigger: 'blur' }
], ],
department: [ departments: [
{ required: true, message: '请选择所属部门', trigger: 'change' } { required: true, message: '请选择所属部门', trigger: 'change' }
] ]
} }
@@ -170,7 +176,9 @@ export default {
if (!this.departmentFilter) { if (!this.departmentFilter) {
return this.projectGroups return this.projectGroups
} }
return this.projectGroups.filter(group => group.department === parseInt(this.departmentFilter)) return this.projectGroups.filter(group =>
group.departments && group.departments.includes(parseInt(this.departmentFilter))
)
} }
}, },
async created() { async created() {
@@ -270,13 +278,16 @@ export default {
// 项目组管理方法 // 项目组管理方法
showAddProjectGroupDialog() { showAddProjectGroupDialog() {
this.projectGroupForm = { id: null, name: '', department: '', description: '' } this.projectGroupForm = { id: null, name: '', departments: [], description: '' }
this.projectGroupDialogTitle = '添加项目组' this.projectGroupDialogTitle = '添加项目组'
this.projectGroupDialogVisible = true this.projectGroupDialogVisible = true
}, },
editProjectGroup(projectGroup) { editProjectGroup(projectGroup) {
this.projectGroupForm = { ...projectGroup } this.projectGroupForm = {
...projectGroup,
departments: projectGroup.departments || []
}
this.projectGroupDialogTitle = '编辑项目组' this.projectGroupDialogTitle = '编辑项目组'
this.projectGroupDialogVisible = true this.projectGroupDialogVisible = true
}, },
+3 -1
View File
@@ -209,7 +209,9 @@ export default {
return [] return []
} }
const departmentId = parseInt(this.form.department) const departmentId = parseInt(this.form.department)
return this.projectGroups.filter(group => group.department === departmentId) return this.projectGroups.filter(group =>
group.departments && group.departments.includes(departmentId)
)
} }
}, },
watch: { watch: {