Add multiple network maps, graph UX improvements, and full backup import.
Support per-map contact membership with scoped graph views, relation editing on edges, layout caching, and automatic detection of full JSON backups so contacts and relations import together. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+72
-46
@@ -3,18 +3,25 @@ import io
|
||||
import json
|
||||
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.decorators import api_view, action
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
|
||||
from .models import (
|
||||
Contact,
|
||||
Relation,
|
||||
NetworkMap,
|
||||
NetworkMapMembership,
|
||||
RELATION_TYPES,
|
||||
LIFE_SPHERES,
|
||||
NETWORK_CIRCLES,
|
||||
INTERACTION_INTENSITY,
|
||||
)
|
||||
from .serializers import ContactSerializer, RelationSerializer, GraphSerializer
|
||||
from .serializers import (
|
||||
ContactSerializer,
|
||||
RelationSerializer,
|
||||
NetworkMapSerializer,
|
||||
NetworkMapMembershipSerializer,
|
||||
)
|
||||
|
||||
|
||||
class ContactViewSet(viewsets.ModelViewSet):
|
||||
@@ -34,6 +41,53 @@ class RelationViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = RelationSerializer
|
||||
|
||||
|
||||
class NetworkMapViewSet(viewsets.ModelViewSet):
|
||||
queryset = NetworkMap.objects.all()
|
||||
serializer_class = NetworkMapSerializer
|
||||
|
||||
|
||||
class NetworkMapMembershipViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = NetworkMapMembershipSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
map_id = self.kwargs.get('map_pk')
|
||||
return NetworkMapMembership.objects.filter(
|
||||
map_id=map_id
|
||||
).select_related('contact', 'map')
|
||||
|
||||
def perform_create(self, serializer):
|
||||
map_id = self.kwargs.get('map_pk')
|
||||
serializer.save(map_id=map_id)
|
||||
|
||||
|
||||
def _node_from_membership(membership):
|
||||
contact = membership.contact
|
||||
return {
|
||||
'id': contact.id,
|
||||
'label': contact.name,
|
||||
'title': '\n'.join(filter(None, [contact.organization, contact.position, contact.email])),
|
||||
'group': contact.organization or 'default',
|
||||
'life_sphere': membership.life_sphere,
|
||||
'network_circle': membership.network_circle,
|
||||
'importance': membership.importance,
|
||||
'map_angle': membership.map_angle,
|
||||
'map_radius_ratio': membership.map_radius_ratio,
|
||||
'membership_id': membership.id,
|
||||
}
|
||||
|
||||
|
||||
def _edge_from_relation(r):
|
||||
return {
|
||||
'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,
|
||||
'interaction_intensity': r.interaction_intensity,
|
||||
}
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
def graph_data(request):
|
||||
"""Возвращает граф: nodes + edges для vis.js."""
|
||||
@@ -44,62 +98,34 @@ def graph_data(request):
|
||||
'label': c.name,
|
||||
'title': '\n'.join(filter(None, [c.organization, c.position, c.email])),
|
||||
'group': c.organization or 'default',
|
||||
'life_sphere': c.life_sphere,
|
||||
'network_circle': c.network_circle,
|
||||
'importance': c.importance,
|
||||
'map_angle': c.map_angle,
|
||||
'map_radius_ratio': c.map_radius_ratio,
|
||||
}
|
||||
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,
|
||||
'interaction_intensity': r.interaction_intensity,
|
||||
}
|
||||
for r in relations
|
||||
]
|
||||
edges = [_edge_from_relation(r) for r in relations]
|
||||
return Response({'nodes': nodes, 'edges': edges})
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
def network_map_graph(request):
|
||||
"""Граф только для карты сети: контакты с include_on_network_map и связи между ними."""
|
||||
contacts = list(
|
||||
Contact.objects.filter(include_on_network_map=True).order_by('name')
|
||||
"""Граф для конкретной карты сети: участники и связи между ними."""
|
||||
map_id = request.query_params.get('map_id')
|
||||
if not map_id:
|
||||
default_map = NetworkMap.objects.order_by('id').first()
|
||||
if not default_map:
|
||||
return Response({'nodes': [], 'edges': []})
|
||||
map_id = default_map.id
|
||||
|
||||
memberships = list(
|
||||
NetworkMapMembership.objects.filter(map_id=map_id)
|
||||
.select_related('contact')
|
||||
.order_by('contact__name')
|
||||
)
|
||||
allowed_ids = {c.id for c in contacts}
|
||||
nodes = [
|
||||
{
|
||||
'id': c.id,
|
||||
'label': c.name,
|
||||
'title': '\n'.join(filter(None, [c.organization, c.position, c.email])),
|
||||
'group': c.organization or 'default',
|
||||
'life_sphere': c.life_sphere,
|
||||
'network_circle': c.network_circle,
|
||||
'importance': c.importance,
|
||||
'map_angle': c.map_angle,
|
||||
'map_radius_ratio': c.map_radius_ratio,
|
||||
}
|
||||
for c in contacts
|
||||
]
|
||||
allowed_ids = {m.contact_id for m in memberships}
|
||||
nodes = [_node_from_membership(m) for m in memberships]
|
||||
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,
|
||||
'interaction_intensity': r.interaction_intensity,
|
||||
}
|
||||
_edge_from_relation(r)
|
||||
for r in relations
|
||||
if r.source_id in allowed_ids and r.target_id in allowed_ids
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user