Add conflictology map mode and fix graph rendering reliability.

Support conflict types, involvement sizing, and map-type toggles while keeping polar sector layout; improve GraphView init retries and edge matching on network maps.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-25 11:51:06 +03:00
co-authored by Cursor
parent e80d0d0e88
commit 85b8b2e9b8
29 changed files with 744 additions and 132 deletions
@@ -0,0 +1,52 @@
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contacts', '0008_alter_relation_interaction_intensity'),
]
operations = [
migrations.AddField(
model_name='networkmap',
name='conflictology_enabled',
field=models.BooleanField(default=False, verbose_name='Режим конфликтологии'),
),
migrations.AddField(
model_name='networkmap',
name='conflict_subject',
field=models.CharField(blank=True, max_length=255, verbose_name='Предмет конфликта (центр карты)'),
),
migrations.AddField(
model_name='networkmapmembership',
name='conflict_involvement',
field=models.PositiveSmallIntegerField(
default=3,
validators=[MinValueValidator(1), MaxValueValidator(5)],
verbose_name='Вовлечённость в конфликт (1–5)',
),
),
migrations.AlterField(
model_name='relation',
name='relation_type',
field=models.CharField(
choices=[
('colleague', 'Коллега'),
('friend', 'Друг'),
('family', 'Родственник'),
('acquaintance', 'Знакомый'),
('business', 'Деловой партнёр'),
('other', 'Другое'),
('conflict_open', 'Открытый конфликт'),
('conflict_tension', 'Напряжение'),
('conflict_alliance', 'Союз / поддержка'),
('conflict_neutral', 'Нейтральная связь'),
],
default='acquaintance',
max_length=50,
verbose_name='Тип связи',
),
),
]
@@ -0,0 +1,32 @@
from django.db import migrations, models
def migrate_conflictology_to_map_type(apps, schema_editor):
NetworkMap = apps.get_model('contacts', 'NetworkMap')
NetworkMapType = apps.get_model('contacts', 'NetworkMapType')
for network_map in NetworkMap.objects.filter(conflictology_enabled=True).select_related('map_type'):
map_type = network_map.map_type
if map_type and not map_type.conflictology_enabled:
map_type.conflictology_enabled = True
map_type.save(update_fields=['conflictology_enabled'])
class Migration(migrations.Migration):
dependencies = [
('contacts', '0009_conflictology'),
]
operations = [
migrations.AddField(
model_name='networkmaptype',
name='conflictology_enabled',
field=models.BooleanField(default=False, verbose_name='Режим конфликтологии'),
),
migrations.RunPython(migrate_conflictology_to_map_type, migrations.RunPython.noop),
migrations.RemoveField(
model_name='networkmap',
name='conflictology_enabled',
),
]
+16 -2
View File
@@ -2,8 +2,8 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from core.choices import (
ALL_RELATION_TYPES,
INTERACTION_INTENSITY,
RELATION_TYPES,
)
@@ -35,6 +35,10 @@ class NetworkMapType(models.Model):
sectors = models.JSONField(default=list, verbose_name='Секторы')
circles = models.JSONField(default=list, verbose_name='Круги')
is_default = models.BooleanField(default=False, verbose_name='Тип по умолчанию')
conflictology_enabled = models.BooleanField(
default=False,
verbose_name='Режим конфликтологии',
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -58,6 +62,11 @@ class NetworkMap(models.Model):
related_name='maps',
verbose_name='Тип карты',
)
conflict_subject = models.CharField(
max_length=255,
blank=True,
verbose_name='Предмет конфликта (центр карты)',
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -100,6 +109,11 @@ class NetworkMapMembership(models.Model):
validators=[MinValueValidator(1), MaxValueValidator(5)],
verbose_name='Важность (15)',
)
conflict_involvement = models.PositiveSmallIntegerField(
default=3,
validators=[MinValueValidator(1), MaxValueValidator(5)],
verbose_name='Вовлечённость в конфликт (1–5)',
)
map_angle = models.FloatField(null=True, blank=True, verbose_name='Угол позиции на карте')
map_radius_ratio = models.FloatField(
null=True,
@@ -136,7 +150,7 @@ class Relation(models.Model):
)
relation_type = models.CharField(
max_length=50,
choices=RELATION_TYPES,
choices=ALL_RELATION_TYPES,
default='acquaintance',
verbose_name='Тип связи',
)
+3 -3
View File
@@ -48,7 +48,7 @@ class NetworkMapTypeSerializer(serializers.ModelSerializer):
class Meta:
model = NetworkMapType
fields = [
'id', 'name', 'sectors', 'circles', 'is_default',
'id', 'name', 'sectors', 'circles', 'is_default', 'conflictology_enabled',
'created_at', 'updated_at',
]
read_only_fields = ['id', 'is_default', 'created_at', 'updated_at']
@@ -77,7 +77,7 @@ class NetworkMapSerializer(serializers.ModelSerializer):
class Meta:
model = NetworkMap
fields = [
'id', 'name', 'description', 'map_type',
'id', 'name', 'description', 'map_type', 'conflict_subject',
'created_at', 'updated_at', 'memberships_count',
]
read_only_fields = ['id', 'created_at', 'updated_at', 'memberships_count']
@@ -102,7 +102,7 @@ class NetworkMapMembershipSerializer(serializers.ModelSerializer):
model = NetworkMapMembership
fields = [
'id', 'map', 'map_name', 'contact', 'contact_name',
'life_sphere', 'network_circle', 'importance',
'life_sphere', 'network_circle', 'importance', 'conflict_involvement',
'map_angle', 'map_radius_ratio',
'created_at', 'updated_at',
]
+10
View File
@@ -30,10 +30,20 @@ RELATION_TYPES = [
('other', 'Другое'),
]
CONFLICT_RELATION_TYPES = [
('conflict_open', 'Открытый конфликт'),
('conflict_tension', 'Напряжение'),
('conflict_alliance', 'Союз / поддержка'),
('conflict_neutral', 'Нейтральная связь'),
]
ALL_RELATION_TYPES = RELATION_TYPES + CONFLICT_RELATION_TYPES
def choices_payload():
return {
'relation_types': [{'value': v, 'label': l} for v, l in RELATION_TYPES],
'conflict_relation_types': [{'value': v, 'label': l} for v, l in CONFLICT_RELATION_TYPES],
'life_spheres': [{'value': v, 'label': l} for v, l in LIFE_SPHERES],
'network_circles': [{'value': v, 'label': l} for v, l in NETWORK_CIRCLES],
'interaction_intensities': [{'value': v, 'label': l} for v, l in INTERACTION_INTENSITY],
+13 -3
View File
@@ -17,6 +17,7 @@ def node_from_membership(membership):
'life_sphere': membership.life_sphere,
'network_circle': membership.network_circle,
'importance': membership.importance,
'conflict_involvement': membership.conflict_involvement,
'map_angle': membership.map_angle,
'map_radius_ratio': membership.map_radius_ratio,
'membership_id': membership.id,
@@ -45,11 +46,15 @@ def build_full_graph():
def build_network_map_graph(map_id=None):
if not map_id:
default_map = NetworkMap.objects.order_by('id').first()
default_map = NetworkMap.objects.select_related('map_type').order_by('id').first()
if not default_map:
return {'nodes': [], 'edges': []}
return {'nodes': [], 'edges': [], 'conflictology': False, 'conflict_subject': ''}
map_id = default_map.id
network_map = NetworkMap.objects.select_related('map_type').filter(pk=map_id).first()
if not network_map:
return {'nodes': [], 'edges': [], 'conflictology': False, 'conflict_subject': ''}
memberships = list(
NetworkMapMembership.objects.filter(map_id=map_id)
.select_related('contact')
@@ -63,4 +68,9 @@ def build_network_map_graph(map_id=None):
for r in relations
if r.source_id in allowed_ids and r.target_id in allowed_ids
]
return {'nodes': nodes, 'edges': edges}
return {
'nodes': nodes,
'edges': edges,
'conflictology': network_map.map_type.conflictology_enabled,
'conflict_subject': network_map.conflict_subject or network_map.name,
}