Download apps/cases/services/material/case_material_binding_workflow.py from jun2024/fc: direct link, hf CLI and curl.
- Browser
- Download file 10.4 kB
-
https://huggingface.co/spaces/jun2024/fc/resolve/main/apps/cases/services/material/case_material_binding_workflow.py
- Command line
-
hf download hf://spaces/jun2024/fc/apps/cases/services/material/case_material_binding_workflow.py
-
curl -L -o case_material_binding_workflow.py https://huggingface.co/spaces/jun2024/fc/resolve/main/apps/cases/services/material/case_material_binding_workflow.py
10.4 kB
| """Business workflow orchestration.""" | |
| from __future__ import annotations | |
| from collections.abc import Sequence | |
| from typing import Any | |
| from django.db import models, transaction | |
| from django.utils.translation import gettext_lazy as _ | |
| from apps.cases.models import ( | |
| CaseLogAttachment, | |
| CaseMaterial, | |
| CaseMaterialCategory, | |
| CaseMaterialGroupOrder, | |
| CaseMaterialSide, | |
| CaseMaterialType, | |
| CaseParty, | |
| SupervisingAuthority, | |
| ) | |
| from apps.core.exceptions import NotFoundError, ValidationException | |
| class CaseMaterialBindingWorkflow: | |
| def __init__(self, case_service: Any | None = None) -> None: | |
| self._case_service = case_service | |
| def case_service(self) -> Any: | |
| if self._case_service is None: | |
| raise RuntimeError("CaseMaterialBindingWorkflow.case_service 未注入") | |
| return self._case_service | |
| def bind_materials( | |
| self, | |
| case_id: int, | |
| items: Sequence[dict[str, Any]], | |
| user: Any | None = None, | |
| org_access: dict[str, Any] | None = None, | |
| perm_open_access: bool = False, | |
| ) -> list[CaseMaterial]: | |
| self.case_service.get_case(case_id, user=user, org_access=org_access, perm_open_access=perm_open_access) | |
| attachment_ids = [int(x["attachment_id"]) for x in items if x.get("attachment_id") is not None] | |
| attachments = { | |
| a.id: a | |
| for a in CaseLogAttachment.objects.filter(log__case_id=case_id, id__in=attachment_ids).select_related("log") | |
| } | |
| missing = [aid for aid in attachment_ids if aid not in attachments] | |
| if missing: | |
| raise NotFoundError(_("部分附件不存在或不属于该案件")) | |
| parties_by_id = {p.id: p for p in CaseParty.objects.filter(case_id=case_id).select_related("client").all()} | |
| authorities_by_id = {a.id: a for a in SupervisingAuthority.objects.filter(case_id=case_id).all()} | |
| law_firm_id = getattr(user, "law_firm_id", None) if user else None | |
| type_cache: dict[str, CaseMaterialType] = {} | |
| saved: list[CaseMaterial] = [] | |
| with transaction.atomic(): | |
| for payload in items: | |
| att_id_raw = payload.get("attachment_id") | |
| att_id = int(att_id_raw) if att_id_raw is not None else 0 | |
| category = (payload.get("category") or "").strip() | |
| type_id = payload.get("type_id") | |
| type_name = (payload.get("type_name") or "").strip() | |
| side = payload.get("side") | |
| party_ids = payload.get("party_ids") or [] | |
| supervising_authority_id = payload.get("supervising_authority_id") | |
| if category not in {CaseMaterialCategory.PARTY, CaseMaterialCategory.NON_PARTY}: | |
| raise ValidationException(message=_("材料大类不合法"), errors={"category": category}) | |
| if not type_name: | |
| raise ValidationException(message=_("类型名称不能为空"), errors={"type_name": "required"}) | |
| if category == CaseMaterialCategory.PARTY: | |
| if side not in {CaseMaterialSide.OUR, CaseMaterialSide.OPPONENT}: | |
| raise ValidationException(message=_("当事人方向不合法"), errors={"side": side}) | |
| supervising_authority_id = None | |
| else: | |
| side = None | |
| if not supervising_authority_id: | |
| raise ValidationException( | |
| message=_("必须选择主管机关"), errors={"supervising_authority_id": "required"} | |
| ) | |
| supervising_authority_id = int(supervising_authority_id) | |
| if supervising_authority_id not in authorities_by_id: | |
| raise ValidationException( | |
| message=_("主管机关不属于该案件"), | |
| errors={"supervising_authority_id": supervising_authority_id}, | |
| ) | |
| resolved_type = self._resolve_type_cached( | |
| cache=type_cache, category=category, type_id=type_id, type_name=type_name, law_firm_id=law_firm_id | |
| ) | |
| material, _created = CaseMaterial.objects.update_or_create( | |
| source_attachment=attachments[att_id], | |
| defaults={ | |
| "case_id": case_id, | |
| "category": category, | |
| "type": resolved_type, | |
| "type_name": resolved_type.name, | |
| "side": side, | |
| "supervising_authority_id": supervising_authority_id, | |
| }, | |
| ) | |
| if category == CaseMaterialCategory.PARTY: | |
| validated_party_ids = self._validate_party_ids(party_ids, parties_by_id, side) | |
| material.parties.set(validated_party_ids) | |
| else: | |
| material.parties.clear() | |
| saved.append(material) | |
| # 根据绑定顺序自动创建/更新分组排序,保留用户文件夹顺序 | |
| if saved: | |
| self._ensure_group_orders(case_id, saved) | |
| return saved | |
| def _ensure_group_orders(self, case_id: int, saved: list[CaseMaterial]) -> None: | |
| """根据绑定顺序自动创建/更新分组排序记录,保留用户文件夹顺序。 | |
| 遍历 saved 列表(按前端传入顺序),收集每个 (category, side, supervising_authority_id) | |
| 下的 type 首次出现顺序,然后用 update_or_create 写入 CaseMaterialGroupOrder。 | |
| 已存在的分组排序记录(之前通过拖拽设置的)会被保留,只补充新出现的分组。 | |
| """ | |
| # 收集每个 (category, side, authority_id) 下的 type 出现顺序 | |
| group_order_map: dict[tuple[str, str | None, int | None], list[int]] = {} | |
| for material in saved: | |
| if not material.type_id: | |
| continue | |
| key = (material.category, material.side, material.supervising_authority_id) | |
| if material.type_id not in group_order_map.get(key, []): | |
| group_order_map.setdefault(key, []).append(material.type_id) | |
| # 读取已有的分组排序,确定已有分组的 sort_index | |
| existing_orders = CaseMaterialGroupOrder.objects.filter(case_id=case_id) | |
| existing_max_index: dict[tuple[str, str | None, int | None], int] = {} | |
| for order in existing_orders: | |
| key = (order.category, order.side, order.supervising_authority_id) | |
| existing_max_index[key] = max(existing_max_index.get(key, -1), order.sort_index) | |
| # 对每个分组维度,为未设置排序的新分组追加 sort_index | |
| for (category, side, authority_id), type_ids in group_order_map.items(): | |
| existing_type_ids = set( | |
| CaseMaterialGroupOrder.objects.filter( | |
| case_id=case_id, | |
| category=category, | |
| side=side, | |
| supervising_authority_id=authority_id, | |
| ).values_list("type_id", flat=True) | |
| ) | |
| next_index = existing_max_index.get((category, side, authority_id), -1) + 1 | |
| for type_id in type_ids: | |
| if type_id in existing_type_ids: | |
| continue | |
| CaseMaterialGroupOrder.objects.update_or_create( | |
| case_id=case_id, | |
| category=category, | |
| side=side, | |
| supervising_authority_id=authority_id, | |
| type_id=type_id, | |
| defaults={"sort_index": next_index}, | |
| ) | |
| next_index += 1 | |
| def _resolve_type_cached( | |
| self, | |
| cache: dict[str, CaseMaterialType], | |
| *, | |
| category: str, | |
| type_id: int | None, | |
| type_name: str, | |
| law_firm_id: int | None, | |
| ) -> CaseMaterialType: | |
| cache_key = f"id:{type_id}" if type_id else f"name:{category}:{type_name}:{law_firm_id}" | |
| cached = cache.get(cache_key) | |
| if cached: | |
| return cached | |
| resolved = self._resolve_type(category=category, type_id=type_id, type_name=type_name, law_firm_id=law_firm_id) | |
| cache[cache_key] = resolved | |
| return resolved | |
| def _resolve_type( | |
| self, *, category: str, type_id: int | None, type_name: str, law_firm_id: int | None | |
| ) -> CaseMaterialType: | |
| if type_id: | |
| try: | |
| t = CaseMaterialType.objects.get(id=int(type_id), category=category, is_active=True) | |
| except CaseMaterialType.DoesNotExist: | |
| raise ValidationException(message=_("材料类型不存在"), errors={"type_id": type_id}) from None | |
| return t | |
| qs = CaseMaterialType.objects.filter(category=category, name=type_name, is_active=True).order_by( | |
| models.Case( | |
| models.When(law_firm_id=law_firm_id, then=0), | |
| models.When(law_firm_id=None, then=1), | |
| default=2, | |
| output_field=models.IntegerField(), | |
| ) | |
| ) | |
| t: CaseMaterialType | None = qs.first() # type: ignore | |
| if t is not None: | |
| return t | |
| return CaseMaterialType.objects.create( | |
| category=category, name=type_name, law_firm_id=law_firm_id, is_active=True | |
| ) | |
| def _validate_party_ids( | |
| self, party_ids: Sequence[Any], parties_by_id: dict[int, CaseParty], side: str | |
| ) -> list[int]: | |
| validated: list[int] = [] | |
| for pid in party_ids: | |
| try: | |
| pid_int = int(pid) | |
| except (TypeError, ValueError): | |
| continue | |
| party = parties_by_id.get(pid_int) | |
| if not party: | |
| raise ValidationException(message=_("包含无效当事人"), errors={"party_ids": pid_int}) | |
| is_our = bool(getattr(getattr(party, "client", None), "is_our_client", False)) | |
| if side == CaseMaterialSide.OUR and (not is_our): | |
| raise ValidationException(message=_("当事人不属于我方"), errors={"party_ids": pid_int}) | |
| if side == CaseMaterialSide.OPPONENT and is_our: | |
| raise ValidationException(message=_("当事人不属于对方"), errors={"party_ids": pid_int}) | |
| validated.append(pid_int) | |
| return validated | |