"""Справочник типовых IT-работ (Directory it_work_category / it_work).""" from __future__ import annotations import json from pathlib import Path from typing import Any from django.db import transaction from core.models.directories import Directory, DirectoryType CAT_TYPE = 'it_work_category' WORK_TYPE = 'it_work' def _default_json_path() -> Path: acore_root = Path(__file__).resolve().parents[2] sibling = ( acore_root.parent / 'report-tool' / 'docs' / 'work-directory-consolidated' / 'work-directory.json' ) if sibling.is_file(): return sibling return Path( '/home/lokses/Рабочий стол/apms/report-tool/docs/' 'work-directory-consolidated/work-directory.json' ) DEFAULT_JSON = _default_json_path() def load_catalog_json(path: str | Path | None = None) -> dict: p = Path(path) if path else DEFAULT_JSON return json.loads(p.read_text(encoding='utf-8')) def detail_field_labels(catalog: dict | None = None) -> dict[str, str]: """code → label для UI.""" data = catalog if catalog is not None else {} fields = data.get('detail_fields') or {} out: dict[str, str] = {} for code, meta in fields.items(): if isinstance(meta, dict): out[code] = meta.get('label') or code else: out[code] = str(meta) return out @transaction.atomic def seed_from_json(path: str | Path | None = None) -> dict[str, int]: """Идемпотентный upsert категорий и работ. Возвращает счётчики.""" data = load_catalog_json(path) cat_type, _ = DirectoryType.objects.get_or_create( code=CAT_TYPE, defaults={ 'name': 'Категория IT-работ', 'hierarchy_type': 'flexible', 'description': 'Категории справочника типовых IT-работ', }, ) if cat_type.hierarchy_type != 'flexible': cat_type.hierarchy_type = 'flexible' cat_type.save(update_fields=['hierarchy_type', 'updated_at']) work_type, _ = DirectoryType.objects.get_or_create( code=WORK_TYPE, defaults={ 'name': 'Типовая IT-работа', 'hierarchy_type': 'flexible', 'description': 'Типовые работы IT-отдела', }, ) if work_type.hierarchy_type != 'flexible': work_type.hierarchy_type = 'flexible' work_type.save(update_fields=['hierarchy_type', 'updated_at']) labels = detail_field_labels(data) # храним лейблы на типе работ в description JSON-хвосте некрасиво — # кладём в первую категорию-маркер? Лучше отдельный Directory code=_meta meta, _ = Directory.objects.update_or_create( directory_type=work_type, code='_meta', defaults={ 'name': 'Метаданные справочника IT-работ', 'is_active': False, 'extra_data': { 'detail_fields': data.get('detail_fields') or {}, 'labels': labels, 'source_status': data.get('status', ''), }, }, ) cats_upserted = 0 works_upserted = 0 cat_by_code: dict[str, Directory] = {} for cat in data.get('categories') or []: code = cat['id'] obj, _ = Directory.objects.update_or_create( directory_type=cat_type, code=code, defaults={ 'name': cat['name'], 'is_active': True, 'parent': None, 'extra_data': {}, }, ) cat_by_code[code] = obj cats_upserted += 1 for work in cat.get('works') or []: wid = work['id'] extra: dict[str, Any] = { 'required_details': list(work.get('required_details') or []), } if work.get('notes'): extra['notes'] = work['notes'] if work.get('source_ids'): extra['source_ids'] = work['source_ids'] Directory.objects.update_or_create( directory_type=work_type, code=wid, defaults={ 'name': work['name'], 'is_active': True, 'parent': obj, 'extra_data': extra, }, ) works_upserted += 1 return { 'categories': cats_upserted, 'works': works_upserted, 'meta_id': meta.pk, } def catalog_payload_for_ui() -> dict: """JSON для формы: категории, работы, лейблы полей.""" cats = list( Directory.objects.filter(directory_type__code=CAT_TYPE, is_active=True) .order_by('name') ) works = list( Directory.objects.filter(directory_type__code=WORK_TYPE, is_active=True) .exclude(code='_meta') .select_related('parent') .order_by('parent__name', 'name') ) meta = Directory.objects.filter( directory_type__code=WORK_TYPE, code='_meta', ).first() labels = {} if meta and isinstance(meta.extra_data, dict): labels = meta.extra_data.get('labels') or {} if not labels and meta.extra_data.get('detail_fields'): labels = detail_field_labels({'detail_fields': meta.extra_data['detail_fields']}) return { 'categories': [{'id': c.pk, 'code': c.code, 'name': c.name} for c in cats], 'works': [ { 'id': w.pk, 'code': w.code, 'name': w.name, 'category_id': w.parent_id, 'required_details': list((w.extra_data or {}).get('required_details') or []), 'notes': (w.extra_data or {}).get('notes') or '', } for w in works ], 'detail_labels': labels, } def required_details_for(work: Directory | None) -> list[str]: if not work: return [] return list((work.extra_data or {}).get('required_details') or []) def detail_labels_from_db() -> dict[str, str]: meta = Directory.objects.filter( directory_type__code=WORK_TYPE, code='_meta', ).first() if not meta or not isinstance(meta.extra_data, dict): return {} labels = meta.extra_data.get('labels') or {} if labels: return dict(labels) if meta.extra_data.get('detail_fields'): return detail_field_labels({'detail_fields': meta.extra_data['detail_fields']}) return {} def parse_details_from_post(post, work: Directory | None) -> dict[str, str]: """Собрать details из POST (поля detail_).""" details: dict[str, str] = {} for key in required_details_for(work): details[key] = (post.get(f'detail_{key}') or '').strip() return details def validate_worklog_details(work: Directory, details: dict | None) -> str: """Пустая строка если ок, иначе текст ошибки.""" details = details or {} missing = [] for key in required_details_for(work): val = details.get(key) if val is None or (isinstance(val, str) and not val.strip()): missing.append(key) if not missing: return '' labels = detail_labels_from_db() named = [labels.get(k, k) for k in missing] return 'Заполните обязательные уточнения: ' + ', '.join(named) + '.' def format_details_short(details: dict | None, labels: dict[str, str] | None = None) -> str: if not details: return '' labels = labels or {} parts = [] for k, v in details.items(): if v is None or v == '': continue parts.append(f'{labels.get(k, k)}: {v}') return '; '.join(parts) def worklog_display_title(log, labels: dict[str, str] | None = None) -> str: """Заголовок для списка/отчёта.""" if getattr(log, 'catalog_work_id', None) and log.catalog_work_id: name = log.catalog_work.name short = format_details_short(log.details or {}, labels) return f'{name} ({short})' if short else name return (log.description or '').strip() or '—' def get_catalog_work(pk) -> Directory | None: if not pk: return None return ( Directory.objects.filter( pk=pk, directory_type__code=WORK_TYPE, is_active=True, ) .exclude(code='_meta') .select_related('parent') .first() )