forked from erp-dev/erp
feat: notifier beta
This commit is contained in:
335
api_v2/test_mission_api.py
Normal file
335
api_v2/test_mission_api.py
Normal file
@@ -0,0 +1,335 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.test import TestCase
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from basic_info import models as basic_models
|
||||
from mission import models as mission_models
|
||||
|
||||
|
||||
def grant_permission(user, codename):
|
||||
permission = Permission.objects.get(codename=codename)
|
||||
user.user_permissions.add(permission)
|
||||
|
||||
|
||||
class MissionV2APITest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
self.merchant = basic_models.Merchant.objects.create(
|
||||
name="任务商户",
|
||||
type=basic_models.MerchantTypeEnum.STORE,
|
||||
)
|
||||
self.other_merchant = basic_models.Merchant.objects.create(
|
||||
name="其他商户",
|
||||
type=basic_models.MerchantTypeEnum.STORE,
|
||||
)
|
||||
self.user = get_user_model().objects.create_user(username="mission-user", password="pass12345")
|
||||
self.employee = basic_models.Employee.objects.create(
|
||||
merchant=self.merchant,
|
||||
sys_user=self.user,
|
||||
name="任务员工",
|
||||
)
|
||||
self.default_category = mission_models.MissionCategory.objects.create(
|
||||
merchant=self.merchant,
|
||||
name="通用",
|
||||
)
|
||||
self.followup_category = mission_models.MissionCategory.objects.create(
|
||||
merchant=self.merchant,
|
||||
name="跟进",
|
||||
)
|
||||
self.other_category = mission_models.MissionCategory.objects.create(
|
||||
merchant=self.other_merchant,
|
||||
name="通用",
|
||||
)
|
||||
self.participant = basic_models.Employee.objects.create(
|
||||
merchant=self.merchant,
|
||||
name="参与者",
|
||||
)
|
||||
self.other_employee = basic_models.Employee.objects.create(
|
||||
merchant=self.other_merchant,
|
||||
name="其他员工",
|
||||
)
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
def test_create_mission_uses_default_status_and_current_employee(self):
|
||||
resp = self.client.post(
|
||||
"/api/v2/missions/",
|
||||
{
|
||||
"description": "跟进客户问题",
|
||||
"participant_ids": [self.participant.id],
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_code, 201)
|
||||
mission = mission_models.Mission.objects.get(id=resp.data["id"])
|
||||
self.assertEqual(mission.merchant, self.merchant)
|
||||
self.assertEqual(mission.creator, self.employee)
|
||||
self.assertEqual(mission.category, self.default_category)
|
||||
self.assertFalse(mission.is_urgent)
|
||||
self.assertFalse(mission.is_completed)
|
||||
self.assertFalse(mission.is_cancelled)
|
||||
self.assertEqual(resp.data["category"], self.default_category.id)
|
||||
self.assertEqual(resp.data["category_name"], "通用")
|
||||
self.assertEqual(list(mission.participants.values_list("employee_id", flat=True)), [self.participant.id])
|
||||
|
||||
def test_create_mission_rejects_status_fields(self):
|
||||
resp = self.client.post(
|
||||
"/api/v2/missions/",
|
||||
{
|
||||
"description": "非法状态",
|
||||
"is_completed": True,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertIn("is_completed", resp.data)
|
||||
|
||||
def test_create_mission_rejects_cross_merchant_category(self):
|
||||
resp = self.client.post(
|
||||
"/api/v2/missions/",
|
||||
{
|
||||
"description": "非法分类",
|
||||
"category": self.other_category.id,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertIn("category", resp.data)
|
||||
|
||||
def test_patch_mission_rejects_status_fields(self):
|
||||
mission = self._create_mission()
|
||||
|
||||
resp = self.client.patch(
|
||||
f"/api/v2/missions/{mission.id}/",
|
||||
{"is_cancelled": True},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
mission.refresh_from_db()
|
||||
self.assertFalse(mission.is_cancelled)
|
||||
|
||||
def test_patch_mission_rejects_is_urgent(self):
|
||||
mission = self._create_mission()
|
||||
|
||||
resp = self.client.patch(
|
||||
f"/api/v2/missions/{mission.id}/",
|
||||
{"is_urgent": True},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
mission.refresh_from_db()
|
||||
self.assertFalse(mission.is_urgent)
|
||||
|
||||
def test_patch_mission_updates_normal_fields_and_participants(self):
|
||||
mission = self._create_mission()
|
||||
|
||||
resp = self.client.patch(
|
||||
f"/api/v2/missions/{mission.id}/",
|
||||
{
|
||||
"description": "更新后的任务",
|
||||
"category": self.followup_category.id,
|
||||
"participant_ids": [self.participant.id],
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
mission.refresh_from_db()
|
||||
self.assertEqual(mission.description, "更新后的任务")
|
||||
self.assertEqual(mission.category, self.followup_category)
|
||||
self.assertEqual(resp.data["category"], self.followup_category.id)
|
||||
self.assertEqual(resp.data["category_name"], "跟进")
|
||||
self.assertEqual(list(mission.participants.values_list("employee_id", flat=True)), [self.participant.id])
|
||||
|
||||
def test_list_only_returns_current_merchant_missions(self):
|
||||
visible = self._create_mission(description="可见任务")
|
||||
mission_models.Mission.objects.create(
|
||||
merchant=self.other_merchant,
|
||||
category=self.other_category,
|
||||
creator=self.other_employee,
|
||||
description="不可见任务",
|
||||
)
|
||||
|
||||
resp = self.client.get("/api/v2/missions/")
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual([item["id"] for item in resp.data], [visible.id])
|
||||
|
||||
def test_delete_mission_is_not_allowed(self):
|
||||
mission = self._create_mission()
|
||||
|
||||
resp = self.client.delete(f"/api/v2/missions/{mission.id}/")
|
||||
|
||||
self.assertEqual(resp.status_code, 405)
|
||||
|
||||
def test_reopen_mission_requires_permission(self):
|
||||
mission = self._create_mission()
|
||||
|
||||
reply_resp = self.client.post(
|
||||
f"/api/v2/missions/{mission.id}/replies/",
|
||||
{"content": "已处理", "ends_task": True},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(reply_resp.status_code, 201)
|
||||
mission.refresh_from_db()
|
||||
self.assertTrue(mission.is_completed)
|
||||
|
||||
reopen_resp = self.client.post(f"/api/v2/missions/{mission.id}/reopen/", {}, format="json")
|
||||
self.assertEqual(reopen_resp.status_code, 403)
|
||||
|
||||
def test_create_ending_reply_and_reopen_mission(self):
|
||||
grant_permission(self.user, "reopen_mission")
|
||||
mission = self._create_mission()
|
||||
|
||||
reply_resp = self.client.post(
|
||||
f"/api/v2/missions/{mission.id}/replies/",
|
||||
{"content": "已处理", "ends_task": True},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(reply_resp.status_code, 201)
|
||||
mission.refresh_from_db()
|
||||
self.assertTrue(mission.is_completed)
|
||||
|
||||
reopen_resp = self.client.post(f"/api/v2/missions/{mission.id}/reopen/", {}, format="json")
|
||||
self.assertEqual(reopen_resp.status_code, 200)
|
||||
mission.refresh_from_db()
|
||||
reply = mission_models.MissionReply.objects.get(id=reply_resp.data["id"])
|
||||
self.assertFalse(mission.is_completed)
|
||||
self.assertFalse(reply.ends_task)
|
||||
self.assertTrue(reply.is_rejected)
|
||||
self.assertEqual(reply.rejected_by, self.employee)
|
||||
|
||||
def test_reject_reply_requires_permission(self):
|
||||
mission = self._create_mission()
|
||||
reply = mission_models.MissionReply.objects.create(
|
||||
merchant=self.merchant,
|
||||
mission=mission,
|
||||
responder=self.employee,
|
||||
content="结束",
|
||||
ends_task=True,
|
||||
)
|
||||
mission.is_completed = True
|
||||
mission.save(update_fields=["is_completed", "updated_at"])
|
||||
|
||||
resp = self.client.post(f"/api/v2/mission-replies/{reply.id}/reject/", {}, format="json")
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
|
||||
def test_reject_reply_api_reopens_mission_when_rejecting_ending_reply(self):
|
||||
grant_permission(self.user, "reject_mission_reply")
|
||||
mission = self._create_mission()
|
||||
reply = mission_models.MissionReply.objects.create(
|
||||
merchant=self.merchant,
|
||||
mission=mission,
|
||||
responder=self.employee,
|
||||
content="结束",
|
||||
ends_task=True,
|
||||
)
|
||||
mission.is_completed = True
|
||||
mission.save(update_fields=["is_completed", "updated_at"])
|
||||
|
||||
resp = self.client.post(f"/api/v2/mission-replies/{reply.id}/reject/", {}, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
mission.refresh_from_db()
|
||||
reply.refresh_from_db()
|
||||
self.assertFalse(mission.is_completed)
|
||||
self.assertTrue(reply.is_rejected)
|
||||
self.assertEqual(reply.rejected_by, self.employee)
|
||||
|
||||
def test_cancel_mission_api_records_cancelled_by(self):
|
||||
mission = self._create_mission()
|
||||
|
||||
resp = self.client.post(f"/api/v2/missions/{mission.id}/cancel/", {}, format="json")
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
mission.refresh_from_db()
|
||||
self.assertTrue(mission.is_cancelled)
|
||||
self.assertEqual(mission.cancelled_by, self.employee)
|
||||
self.assertIsNotNone(mission.cancelled_at)
|
||||
|
||||
def test_set_urgent_api_updates_urgent_status(self):
|
||||
mission = self._create_mission()
|
||||
|
||||
resp = self.client.post(
|
||||
f"/api/v2/missions/{mission.id}/set-urgent/",
|
||||
{"is_urgent": True},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
mission.refresh_from_db()
|
||||
self.assertTrue(mission.is_urgent)
|
||||
|
||||
def test_cross_merchant_mission_detail_returns_404(self):
|
||||
mission = mission_models.Mission.objects.create(
|
||||
merchant=self.other_merchant,
|
||||
category=self.other_category,
|
||||
creator=self.other_employee,
|
||||
description="其他商户任务",
|
||||
)
|
||||
|
||||
resp = self.client.get(f"/api/v2/missions/{mission.id}/")
|
||||
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
def test_cross_merchant_participant_is_rejected(self):
|
||||
resp = self.client.post(
|
||||
"/api/v2/missions/",
|
||||
{
|
||||
"description": "跨商户参与者",
|
||||
"participant_ids": [self.other_employee.id],
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
def test_mission_category_crud(self):
|
||||
list_resp = self.client.get("/api/v2/mission-categories/")
|
||||
self.assertEqual(list_resp.status_code, 200)
|
||||
self.assertEqual(
|
||||
[item["name"] for item in list_resp.data],
|
||||
["通用", "跟进"],
|
||||
)
|
||||
|
||||
create_resp = self.client.post(
|
||||
"/api/v2/mission-categories/",
|
||||
{"name": "售后"},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(create_resp.status_code, 201)
|
||||
category_id = create_resp.data["id"]
|
||||
|
||||
detail_resp = self.client.get(f"/api/v2/mission-categories/{category_id}/")
|
||||
self.assertEqual(detail_resp.status_code, 200)
|
||||
self.assertEqual(detail_resp.data["name"], "售后")
|
||||
|
||||
patch_resp = self.client.patch(
|
||||
f"/api/v2/mission-categories/{category_id}/",
|
||||
{"name": "售后跟进"},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(patch_resp.status_code, 200)
|
||||
self.assertEqual(patch_resp.data["name"], "售后跟进")
|
||||
|
||||
delete_resp = self.client.delete(f"/api/v2/mission-categories/{category_id}/")
|
||||
self.assertEqual(delete_resp.status_code, 204)
|
||||
|
||||
def test_delete_used_mission_category_is_rejected(self):
|
||||
mission = self._create_mission()
|
||||
|
||||
resp = self.client.delete(f"/api/v2/mission-categories/{mission.category_id}/")
|
||||
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
def _create_mission(self, description="测试任务"):
|
||||
return mission_models.Mission.objects.create(
|
||||
merchant=self.merchant,
|
||||
category=self.default_category,
|
||||
creator=self.employee,
|
||||
description=description,
|
||||
)
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.urls import path
|
||||
|
||||
from api_v2.views import (
|
||||
ContentTypeListView,
|
||||
HealthCheckView,
|
||||
QuickCreateEmployeeUserView,
|
||||
RoleListView,
|
||||
@@ -14,6 +15,15 @@ from api_v2.views import (
|
||||
PlateOrderBatchUpdateView,
|
||||
PrintingOrderBatchAdvanceRecordsView,
|
||||
BusinessObjectCloneView,
|
||||
MissionCancelView,
|
||||
MissionCategoryDetailView,
|
||||
MissionCategoryListCreateView,
|
||||
MissionDetailView,
|
||||
MissionListCreateView,
|
||||
MissionReopenView,
|
||||
MissionReplyListCreateView,
|
||||
MissionReplyRejectView,
|
||||
MissionSetUrgentView,
|
||||
)
|
||||
from api_v2.views.basic_info import CustomerEmployeeBindingView, MyVisiblePagesView
|
||||
|
||||
@@ -33,5 +43,14 @@ urlpatterns = [
|
||||
path('plate-orders/by-state-status/', PlateOrderByStateStatusView.as_view(), name='api_v2_plate_order_by_state_status'),
|
||||
path('plate-orders/batch-update/', PlateOrderBatchUpdateView.as_view(), name='api_v2_plate_order_batch_update'),
|
||||
path('stateflow/business-objects/clone/', BusinessObjectCloneView.as_view(), name='api_v2_stateflow_business_object_clone'),
|
||||
path('content-types/', ContentTypeListView.as_view(), name='api_v2_content_type_list'),
|
||||
path('mission-categories/', MissionCategoryListCreateView.as_view(), name='api_v2_mission_category_list_create'),
|
||||
path('mission-categories/<int:category_id>/', MissionCategoryDetailView.as_view(), name='api_v2_mission_category_detail'),
|
||||
path('missions/', MissionListCreateView.as_view(), name='api_v2_mission_list_create'),
|
||||
path('missions/<int:mission_id>/', MissionDetailView.as_view(), name='api_v2_mission_detail'),
|
||||
path('missions/<int:mission_id>/replies/', MissionReplyListCreateView.as_view(), name='api_v2_mission_reply_list_create'),
|
||||
path('missions/<int:mission_id>/reopen/', MissionReopenView.as_view(), name='api_v2_mission_reopen'),
|
||||
path('missions/<int:mission_id>/cancel/', MissionCancelView.as_view(), name='api_v2_mission_cancel'),
|
||||
path('missions/<int:mission_id>/set-urgent/', MissionSetUrgentView.as_view(), name='api_v2_mission_set_urgent'),
|
||||
path('mission-replies/<int:reply_id>/reject/', MissionReplyRejectView.as_view(), name='api_v2_mission_reply_reject'),
|
||||
]
|
||||
|
||||
|
||||
@@ -17,6 +17,18 @@ from .printing import (
|
||||
PrintingOrderBatchAdvanceRecordsView,
|
||||
)
|
||||
from .stateflow import BusinessObjectCloneView
|
||||
from .mission import (
|
||||
ContentTypeListView,
|
||||
MissionCancelView,
|
||||
MissionCategoryDetailView,
|
||||
MissionCategoryListCreateView,
|
||||
MissionDetailView,
|
||||
MissionListCreateView,
|
||||
MissionReopenView,
|
||||
MissionReplyListCreateView,
|
||||
MissionReplyRejectView,
|
||||
MissionSetUrgentView,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'HealthCheckView',
|
||||
@@ -33,5 +45,14 @@ __all__ = [
|
||||
'PlateOrderBatchUpdateView',
|
||||
'PrintingOrderBatchAdvanceRecordsView',
|
||||
'BusinessObjectCloneView',
|
||||
'MissionCancelView',
|
||||
'MissionCategoryDetailView',
|
||||
'MissionCategoryListCreateView',
|
||||
'MissionDetailView',
|
||||
'MissionListCreateView',
|
||||
'MissionReopenView',
|
||||
'MissionReplyListCreateView',
|
||||
'MissionReplyRejectView',
|
||||
'MissionSetUrgentView',
|
||||
'ContentTypeListView',
|
||||
]
|
||||
|
||||
|
||||
513
api_v2/views/mission.py
Normal file
513
api_v2/views/mission.py
Normal file
@@ -0,0 +1,513 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Q
|
||||
from django.db.models import ProtectedError
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import permissions, serializers, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from mission import models as mission_models
|
||||
from mission import services as mission_services
|
||||
|
||||
|
||||
STATUS_FIELDS = {"is_urgent", "is_completed", "is_cancelled", "cancelled_by", "cancelled_at", "rejected_by", "rejected_at"}
|
||||
|
||||
|
||||
def _get_employee(request):
|
||||
employee = getattr(request.user, "employee", None)
|
||||
if employee is None:
|
||||
raise serializers.ValidationError("当前用户未关联员工")
|
||||
return employee
|
||||
|
||||
|
||||
def _employee_payload(employee):
|
||||
if employee is None:
|
||||
return None
|
||||
return {
|
||||
"id": employee.id,
|
||||
"name": employee.name,
|
||||
"merchant_id": employee.merchant_id,
|
||||
}
|
||||
|
||||
|
||||
class MissionWriteSerializer(serializers.Serializer):
|
||||
description = serializers.CharField(required=False, allow_blank=False)
|
||||
category = serializers.IntegerField(required=False, min_value=1)
|
||||
content_type = serializers.IntegerField(required=False, allow_null=True, min_value=1)
|
||||
content_id = serializers.IntegerField(required=False, allow_null=True, min_value=1)
|
||||
participant_ids = serializers.ListField(
|
||||
child=serializers.IntegerField(min_value=1),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.is_create = kwargs.pop("is_create", False)
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.is_create:
|
||||
self.fields["description"].required = True
|
||||
|
||||
def validate_category(self, value):
|
||||
employee = self.context.get("employee")
|
||||
if employee is None:
|
||||
return value
|
||||
try:
|
||||
return mission_models.MissionCategory.objects.get(id=value, merchant=employee.merchant)
|
||||
except mission_models.MissionCategory.DoesNotExist as exc:
|
||||
raise serializers.ValidationError("任务分类不存在") from exc
|
||||
|
||||
def validate(self, attrs):
|
||||
forbidden_fields = STATUS_FIELDS.intersection(self.initial_data.keys())
|
||||
if forbidden_fields:
|
||||
raise serializers.ValidationError({field: "状态字段不能通过普通 CRUD 接口修改" for field in sorted(forbidden_fields)})
|
||||
|
||||
content_type_provided = "content_type" in self.initial_data
|
||||
content_id_provided = "content_id" in self.initial_data
|
||||
if content_type_provided != content_id_provided:
|
||||
raise serializers.ValidationError("content_type 与 content_id 必须同时提供或同时省略")
|
||||
|
||||
content_type_id = attrs.get("content_type")
|
||||
if content_type_id is not None:
|
||||
try:
|
||||
attrs["content_type"] = ContentType.objects.get(id=content_type_id)
|
||||
except ContentType.DoesNotExist as exc:
|
||||
raise serializers.ValidationError({"content_type": "ContentType 不存在"}) from exc
|
||||
return attrs
|
||||
|
||||
|
||||
class MissionReplyCreateSerializer(serializers.Serializer):
|
||||
content = serializers.CharField(allow_blank=False)
|
||||
ends_task = serializers.BooleanField(required=False, default=False)
|
||||
|
||||
|
||||
class MissionUrgentSerializer(serializers.Serializer):
|
||||
is_urgent = serializers.BooleanField()
|
||||
|
||||
|
||||
class MissionCategoryWriteSerializer(serializers.Serializer):
|
||||
name = serializers.CharField(allow_blank=False, max_length=50)
|
||||
|
||||
def validate_name(self, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
raise serializers.ValidationError("分类名称不能为空")
|
||||
employee = self.context["employee"]
|
||||
instance = self.context.get("instance")
|
||||
queryset = mission_models.MissionCategory.objects.filter(
|
||||
merchant=employee.merchant,
|
||||
name=value,
|
||||
)
|
||||
if instance is not None:
|
||||
queryset = queryset.exclude(id=instance.id)
|
||||
if queryset.exists():
|
||||
raise serializers.ValidationError("分类名称已存在")
|
||||
return value
|
||||
|
||||
|
||||
class MissionSerializer(serializers.ModelSerializer):
|
||||
creator = serializers.SerializerMethodField()
|
||||
cancelled_by = serializers.SerializerMethodField()
|
||||
participants = serializers.SerializerMethodField()
|
||||
content_type_label = serializers.SerializerMethodField()
|
||||
category = serializers.IntegerField(source="category_id", read_only=True)
|
||||
category_name = serializers.CharField(source="category.name", read_only=True)
|
||||
has_ending_reply = serializers.BooleanField(read_only=True)
|
||||
can_reply = serializers.BooleanField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = mission_models.Mission
|
||||
fields = [
|
||||
"id",
|
||||
"merchant",
|
||||
"description",
|
||||
"category",
|
||||
"category_name",
|
||||
"is_urgent",
|
||||
"is_completed",
|
||||
"is_cancelled",
|
||||
"cancelled_at",
|
||||
"creator",
|
||||
"cancelled_by",
|
||||
"participants",
|
||||
"content_type",
|
||||
"content_type_label",
|
||||
"content_id",
|
||||
"has_ending_reply",
|
||||
"can_reply",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
def get_creator(self, obj):
|
||||
return _employee_payload(obj.creator)
|
||||
|
||||
def get_cancelled_by(self, obj):
|
||||
return _employee_payload(obj.cancelled_by)
|
||||
|
||||
def get_participants(self, obj):
|
||||
return [
|
||||
_employee_payload(participant.employee)
|
||||
for participant in obj.participants.select_related("employee", "employee__merchant")
|
||||
]
|
||||
|
||||
def get_content_type_label(self, obj):
|
||||
if obj.content_type_id is None:
|
||||
return None
|
||||
return f"{obj.content_type.app_label}.{obj.content_type.model}"
|
||||
|
||||
|
||||
class MissionCategorySerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = mission_models.MissionCategory
|
||||
fields = [
|
||||
"id",
|
||||
"merchant",
|
||||
"name",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class MissionReplySerializer(serializers.ModelSerializer):
|
||||
responder = serializers.SerializerMethodField()
|
||||
rejected_by = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = mission_models.MissionReply
|
||||
fields = [
|
||||
"id",
|
||||
"mission",
|
||||
"merchant",
|
||||
"responder",
|
||||
"content",
|
||||
"replied_at",
|
||||
"ends_task",
|
||||
"is_rejected",
|
||||
"rejected_by",
|
||||
"rejected_at",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
def get_responder(self, obj):
|
||||
return _employee_payload(obj.responder)
|
||||
|
||||
def get_rejected_by(self, obj):
|
||||
return _employee_payload(obj.rejected_by)
|
||||
|
||||
|
||||
def _mission_queryset_for_employee(employee):
|
||||
return (
|
||||
mission_models.Mission.objects.filter(merchant=employee.merchant)
|
||||
.select_related(
|
||||
"merchant",
|
||||
"category",
|
||||
"creator",
|
||||
"creator__merchant",
|
||||
"cancelled_by",
|
||||
"cancelled_by__merchant",
|
||||
"content_type",
|
||||
)
|
||||
.prefetch_related("participants__employee", "participants__employee__merchant")
|
||||
.order_by("-created_at", "-id")
|
||||
)
|
||||
|
||||
|
||||
def _mission_category_queryset_for_employee(employee):
|
||||
return mission_models.MissionCategory.objects.filter(merchant=employee.merchant).order_by("id")
|
||||
|
||||
|
||||
class MissionCategoryListCreateView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
employee = _get_employee(request)
|
||||
queryset = _mission_category_queryset_for_employee(employee)
|
||||
return Response(MissionCategorySerializer(queryset, many=True).data)
|
||||
|
||||
def post(self, request):
|
||||
employee = _get_employee(request)
|
||||
serializer = MissionCategoryWriteSerializer(data=request.data, context={"employee": employee})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
try:
|
||||
category = mission_models.MissionCategory.objects.create(
|
||||
merchant=employee.merchant,
|
||||
name=serializer.validated_data["name"],
|
||||
)
|
||||
except IntegrityError:
|
||||
return Response({"name": ["分类名称已存在"]}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(MissionCategorySerializer(category).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class MissionCategoryDetailView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_object(self, request, category_id):
|
||||
employee = _get_employee(request)
|
||||
return get_object_or_404(_mission_category_queryset_for_employee(employee), id=category_id)
|
||||
|
||||
def get(self, request, category_id):
|
||||
category = self.get_object(request, category_id)
|
||||
return Response(MissionCategorySerializer(category).data)
|
||||
|
||||
def patch(self, request, category_id):
|
||||
employee = _get_employee(request)
|
||||
category = self.get_object(request, category_id)
|
||||
serializer = MissionCategoryWriteSerializer(
|
||||
data=request.data,
|
||||
partial=True,
|
||||
context={"employee": employee, "instance": category},
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
if "name" in serializer.validated_data:
|
||||
category.name = serializer.validated_data["name"]
|
||||
try:
|
||||
category.save(update_fields=["name", "updated_at"])
|
||||
except IntegrityError:
|
||||
return Response({"name": ["分类名称已存在"]}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(MissionCategorySerializer(category).data)
|
||||
|
||||
def delete(self, request, category_id):
|
||||
category = self.get_object(request, category_id)
|
||||
try:
|
||||
category.delete()
|
||||
except ProtectedError:
|
||||
return Response({"detail": "任务分类已被使用,不能删除"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class MissionListCreateView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
employee = _get_employee(request)
|
||||
queryset = _mission_queryset_for_employee(employee)
|
||||
|
||||
if request.query_params.get("category"):
|
||||
queryset = queryset.filter(category_id=request.query_params["category"])
|
||||
for field in ["is_urgent", "is_completed", "is_cancelled"]:
|
||||
value = request.query_params.get(field)
|
||||
if value is not None:
|
||||
queryset = queryset.filter(**{field: value.lower() in ["1", "true", "yes"]})
|
||||
if request.query_params.get("content_type"):
|
||||
queryset = queryset.filter(content_type_id=request.query_params["content_type"])
|
||||
if request.query_params.get("content_id"):
|
||||
queryset = queryset.filter(content_id=request.query_params["content_id"])
|
||||
|
||||
return Response(MissionSerializer(queryset, many=True).data)
|
||||
|
||||
def post(self, request):
|
||||
employee = _get_employee(request)
|
||||
serializer = MissionWriteSerializer(data=request.data, is_create=True, context={"employee": employee})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
data = serializer.validated_data
|
||||
try:
|
||||
mission = mission_services.create_mission(
|
||||
creator=employee,
|
||||
description=data["description"],
|
||||
category=data.get("category"),
|
||||
content_type=data.get("content_type"),
|
||||
content_id=data.get("content_id"),
|
||||
participant_ids=data.get("participant_ids"),
|
||||
)
|
||||
except ValueError as exc:
|
||||
return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(MissionSerializer(mission).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class MissionDetailView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_object(self, request, mission_id):
|
||||
employee = _get_employee(request)
|
||||
return get_object_or_404(_mission_queryset_for_employee(employee), id=mission_id)
|
||||
|
||||
def get(self, request, mission_id):
|
||||
mission = self.get_object(request, mission_id)
|
||||
return Response(MissionSerializer(mission).data)
|
||||
|
||||
def patch(self, request, mission_id):
|
||||
employee = _get_employee(request)
|
||||
mission = self.get_object(request, mission_id)
|
||||
serializer = MissionWriteSerializer(data=request.data, partial=True, context={"employee": employee})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
data = serializer.validated_data
|
||||
try:
|
||||
mission = mission_services.update_mission(
|
||||
mission=mission,
|
||||
updated_by=employee,
|
||||
description=data.get("description"),
|
||||
category=data.get("category"),
|
||||
content_type=data.get("content_type"),
|
||||
content_id=data.get("content_id"),
|
||||
update_content_object=("content_type" in request.data or "content_id" in request.data),
|
||||
participant_ids=data.get("participant_ids"),
|
||||
)
|
||||
except ValueError as exc:
|
||||
return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(MissionSerializer(mission).data)
|
||||
|
||||
def delete(self, request, mission_id):
|
||||
return Response({"detail": "Mission 删除接口未提供,请使用 cancel 接口取消任务"}, status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||
|
||||
|
||||
class MissionReplyListCreateView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get(self, request, mission_id):
|
||||
employee = _get_employee(request)
|
||||
mission = get_object_or_404(_mission_queryset_for_employee(employee), id=mission_id)
|
||||
queryset = (
|
||||
mission_models.MissionReply.objects.filter(mission=mission)
|
||||
.select_related("responder", "responder__merchant", "rejected_by", "rejected_by__merchant")
|
||||
.order_by("replied_at", "id")
|
||||
)
|
||||
for field in ["ends_task", "is_rejected"]:
|
||||
value = request.query_params.get(field)
|
||||
if value is not None:
|
||||
queryset = queryset.filter(**{field: value.lower() in ["1", "true", "yes"]})
|
||||
return Response(MissionReplySerializer(queryset, many=True).data)
|
||||
|
||||
def post(self, request, mission_id):
|
||||
employee = _get_employee(request)
|
||||
mission = get_object_or_404(_mission_queryset_for_employee(employee), id=mission_id)
|
||||
serializer = MissionReplyCreateSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
try:
|
||||
reply = mission_services.create_mission_reply(
|
||||
mission=mission,
|
||||
responder=employee,
|
||||
content=serializer.validated_data["content"],
|
||||
ends_task=serializer.validated_data.get("ends_task", False),
|
||||
)
|
||||
except ValueError as exc:
|
||||
return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(MissionReplySerializer(reply).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class MissionReopenView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def post(self, request, mission_id):
|
||||
if not request.user.has_perm("mission.reopen_mission"):
|
||||
return Response({"detail": "缺少重新打开任务权限"}, status=status.HTTP_403_FORBIDDEN)
|
||||
employee = _get_employee(request)
|
||||
mission = get_object_or_404(_mission_queryset_for_employee(employee), id=mission_id)
|
||||
try:
|
||||
mission = mission_services.reopen_mission(mission=mission, reopened_by=employee)
|
||||
except ValueError as exc:
|
||||
return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(MissionSerializer(mission).data)
|
||||
|
||||
|
||||
class MissionCancelView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def post(self, request, mission_id):
|
||||
employee = _get_employee(request)
|
||||
mission = get_object_or_404(_mission_queryset_for_employee(employee), id=mission_id)
|
||||
try:
|
||||
mission = mission_services.cancel_mission(mission=mission, cancelled_by=employee)
|
||||
except ValueError as exc:
|
||||
return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(MissionSerializer(mission).data)
|
||||
|
||||
|
||||
class MissionReplyRejectView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def post(self, request, reply_id):
|
||||
if not request.user.has_perm("mission.reject_mission_reply"):
|
||||
return Response({"detail": "缺少撤销任务回应权限"}, status=status.HTTP_403_FORBIDDEN)
|
||||
employee = _get_employee(request)
|
||||
reply = get_object_or_404(
|
||||
mission_models.MissionReply.objects.select_related("mission", "responder", "rejected_by").filter(
|
||||
merchant=employee.merchant,
|
||||
),
|
||||
id=reply_id,
|
||||
)
|
||||
try:
|
||||
reply = mission_services.reject_reply(reply=reply, rejected_by=employee)
|
||||
except ValueError as exc:
|
||||
return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(MissionReplySerializer(reply).data)
|
||||
|
||||
|
||||
class MissionSetUrgentView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def post(self, request, mission_id):
|
||||
employee = _get_employee(request)
|
||||
mission = get_object_or_404(_mission_queryset_for_employee(employee), id=mission_id)
|
||||
serializer = MissionUrgentSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
try:
|
||||
mission = mission_services.set_mission_urgent(
|
||||
mission=mission,
|
||||
updated_by=employee,
|
||||
is_urgent=serializer.validated_data["is_urgent"],
|
||||
)
|
||||
except ValueError as exc:
|
||||
return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(MissionSerializer(mission).data)
|
||||
|
||||
|
||||
class ContentTypeListView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
# Models the frontend is allowed to use as mission content_type targets.
|
||||
_ALLOWED_CONTENT_TYPES = [
|
||||
("printing", "plateorder"),
|
||||
("printing", "printingorder"),
|
||||
("printing", "printingjob"),
|
||||
("business", "purchaseorder"),
|
||||
("business", "presalesorder"),
|
||||
("business", "salesorder"),
|
||||
("business", "prepurchaseorder"),
|
||||
("business", "purchasereturnorder"),
|
||||
("business", "salesreturnorder"),
|
||||
("business", "paymentorder"),
|
||||
("business", "receiptorder"),
|
||||
("stock", "transferorder"),
|
||||
("shipment", "shipment"),
|
||||
("stateflow", "process"),
|
||||
]
|
||||
|
||||
# Human-readable labels keyed by (app_label, model).
|
||||
_LABELS = {
|
||||
("printing", "plateorder"): "开版单",
|
||||
("printing", "printingorder"): "印刷单",
|
||||
("printing", "printingjob"): "印刷任务",
|
||||
("business", "purchaseorder"): "采购单",
|
||||
("business", "presalesorder"): "预销售单",
|
||||
("business", "salesorder"): "销售单",
|
||||
("business", "prepurchaseorder"): "预采购单",
|
||||
("business", "purchasereturnorder"): "采购退货单",
|
||||
("business", "salesreturnorder"): "销售退货单",
|
||||
("business", "paymentorder"): "付款单",
|
||||
("business", "receiptorder"): "收款单",
|
||||
("stock", "transferorder"): "调拨单",
|
||||
("shipment", "shipment"): "发货单",
|
||||
("stateflow", "process"): "工艺流程实例",
|
||||
}
|
||||
|
||||
def get(self, request):
|
||||
filters = Q()
|
||||
for app_label, model in self._ALLOWED_CONTENT_TYPES:
|
||||
filters |= Q(app_label=app_label, model=model)
|
||||
cts = ContentType.objects.filter(filters).order_by("app_label", "model")
|
||||
data = [
|
||||
{
|
||||
"id": ct.id,
|
||||
"app_label": ct.app_label,
|
||||
"model": ct.model,
|
||||
"label": f"{ct.app_label}.{ct.model}",
|
||||
"name": self._LABELS.get((ct.app_label, ct.model), ct.model),
|
||||
}
|
||||
for ct in cts
|
||||
]
|
||||
return Response(data)
|
||||
Reference in New Issue
Block a user