Files
social-graph/backend/contacts/views.py
T
2026-04-08 15:29:52 +03:00

250 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import csv
import io
import json
from rest_framework import viewsets, status
from rest_framework.decorators import api_view, action
from rest_framework.response import Response
from .models import Contact, Relation, RELATION_TYPES
from .serializers import ContactSerializer, RelationSerializer, GraphSerializer
class ContactViewSet(viewsets.ModelViewSet):
queryset = Contact.objects.all()
serializer_class = ContactSerializer
def get_queryset(self):
qs = super().get_queryset()
q = self.request.query_params.get('search', '')
if q:
qs = qs.filter(name__icontains=q)
return qs
class RelationViewSet(viewsets.ModelViewSet):
queryset = Relation.objects.select_related('source', 'target').all()
serializer_class = RelationSerializer
@api_view(['GET'])
def graph_data(request):
"""Возвращает граф: nodes + edges для vis.js."""
contacts = Contact.objects.all()
nodes = [
{
'id': c.id,
'label': c.name,
'title': '\n'.join(filter(None, [c.organization, c.position, c.email])),
'group': c.organization or 'default',
}
for c in contacts
]
relations = Relation.objects.select_related('source', 'target').all()
edges = [
{
'id': r.id,
'from': r.source_id,
'to': r.target_id,
'label': r.get_relation_type_display(),
'title': r.description or r.get_relation_type_display(),
'relation_type': r.relation_type,
}
for r in relations
]
return Response({'nodes': nodes, 'edges': edges})
@api_view(['GET'])
def relation_types(request):
"""Список допустимых типов связей."""
return Response([{'value': v, 'label': l} for v, l in RELATION_TYPES])
def _monica_contact_fields(contact_data):
"""Из вложенного data контакта Monica (экспорт account.data) извлекает телефон, email, заметки."""
phone = ''
email = ''
notes_parts = []
for block in contact_data or []:
if block.get('type') == 'contact_field':
for val in block.get('values') or []:
props = val.get('properties') or {}
value = str(props.get('data') or '').strip()
if not value:
continue
if '@' in value and '.' in value:
email = email or value
else:
phone = phone or value
elif block.get('type') == 'note':
for val in block.get('values') or []:
body = str((val.get('properties') or {}).get('body') or '').strip()
if body:
notes_parts.append(body)
return phone, email, '\n'.join(notes_parts)
def _normalize_rows(data):
"""
Нормализует различные форматы JSON в плоский список словарей.
Поддерживает:
- Плоский массив: [{"name": ...}, ...]
- Monica CRM (экспорт): {"account": {"data": [{"type": "contact", "values": [...]}]}}
- Monica CRM (старый): {"contacts": [{"first_name": ..., "last_name": ...}, ...]}
- Обёртка results: {"results": [...]}
- Обёртка data: {"data": [...]}
"""
if isinstance(data, list):
return data
if isinstance(data, dict):
# Monica CRM полный экспорт: account.data, блоки type=contact, values[].properties + data
account = data.get('account')
if isinstance(account, dict):
account_data = account.get('data')
if isinstance(account_data, list):
rows = []
for block in account_data:
if block.get('type') != 'contact':
continue
for c in block.get('values') or []:
props = c.get('properties') or {}
first = str(props.get('first_name') or '').strip()
last = str(props.get('last_name') or '').strip()
middle = str(props.get('middle_name') or '').strip()
name = ' '.join(filter(None, [first, middle, last])) or ' '.join(
filter(None, [first, last])
)
if not name:
continue
phone, email, notes = _monica_contact_fields(c.get('data'))
rows.append({
'name': name,
'email': email,
'phone': phone,
'organization': '',
'position': '',
'notes': notes,
})
if rows:
return rows
# Monica CRM: ключ "contacts" с first_name/last_name (старый формат API)
if 'contacts' in data:
rows = []
for c in data['contacts']:
first = str(c.get('first_name') or '').strip()
last = str(c.get('last_name') or '').strip()
name = ' '.join(filter(None, [first, last]))
# Телефоны Monica хранятся в списке phone_numbers
phone = ''
for ph in c.get('phone_numbers') or []:
phone = str(ph.get('number') or ph.get('content') or '')
if phone:
break
# Email Monica — список emails
email = ''
for em in c.get('emails') or []:
email = str(em.get('email') or em.get('content') or '')
if email:
break
# Организации Monica — список companies
org = ''
position = ''
for comp in c.get('companies') or []:
org = str(comp.get('name') or comp.get('company_name') or '')
position = str(comp.get('job') or comp.get('position') or comp.get('title') or '')
if org:
break
# Также бывает прямое поле company
if not org:
org = str(c.get('company') or c.get('company_name') or '').strip()
position = str(c.get('job') or c.get('position') or '').strip()
rows.append({
'name': name,
'email': email,
'phone': phone,
'organization': org,
'position': position,
'notes': str(c.get('information') or c.get('description') or c.get('notes') or '').strip(),
})
return rows
# Другие обёртки
for key in ('results', 'data', 'items', 'people', 'persons'):
if key in data and isinstance(data[key], list):
return data[key]
return []
@api_view(['POST'])
def import_contacts(request):
"""
Импорт контактов из CSV или JSON.
CSV: name,email,phone,organization,position,notes
JSON (плоский): [{"name": "...", ...}, ...]
JSON (Monica CRM): {"contacts": [{"first_name": ..., "last_name": ...}, ...]}
"""
file = request.FILES.get('file')
if not file:
return Response({'error': 'Файл не передан.'}, status=status.HTTP_400_BAD_REQUEST)
filename = file.name.lower()
created = 0
skipped = 0
errors = []
try:
if filename.endswith('.csv'):
text = file.read().decode('utf-8-sig')
reader = csv.DictReader(io.StringIO(text))
rows = list(reader)
elif filename.endswith('.json'):
raw = json.loads(file.read().decode('utf-8'))
rows = _normalize_rows(raw)
if not rows:
return Response(
{'error': 'Не удалось распознать формат JSON. Ожидается массив контактов или экспорт Monica CRM.'},
status=status.HTTP_400_BAD_REQUEST,
)
else:
return Response(
{'error': 'Поддерживаются только CSV и JSON файлы.'},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
return Response({'error': f'Ошибка разбора файла: {e}'}, status=status.HTTP_400_BAD_REQUEST)
total_rows = len(rows)
for i, row in enumerate(rows):
name = str(
row.get('name') or row.get('Name') or row.get('ФИО') or
' '.join(filter(None, [
str(row.get('first_name') or '').strip(),
str(row.get('last_name') or '').strip(),
]))
).strip()
if not name:
errors.append(f'Строка {i + 1}: отсутствует поле "name"')
skipped += 1
continue
Contact.objects.get_or_create(
name=name,
defaults={
'email': str(row.get('email') or '').strip(),
'phone': str(row.get('phone') or '').strip(),
'organization': str(row.get('organization') or '').strip(),
'position': str(row.get('position') or '').strip(),
'notes': str(row.get('notes') or '').strip(),
},
)
created += 1
return Response({
'total': total_rows,
'created': created,
'skipped': skipped,
'errors': errors,
})