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:
2026-06-24 17:54:51 +03:00
co-authored by Cursor
parent 35ec4c81ec
commit 5155b3a37a
50 changed files with 3176 additions and 372 deletions
@@ -0,0 +1,119 @@
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
def migrate_contact_map_data(apps, schema_editor):
Contact = apps.get_model('contacts', 'Contact')
NetworkMap = apps.get_model('contacts', 'NetworkMap')
NetworkMapMembership = apps.get_model('contacts', 'NetworkMapMembership')
default_map, _ = NetworkMap.objects.get_or_create(
name='Основная карта',
defaults={'description': 'Мигрировано из прежней единой карты сети'},
)
for contact in Contact.objects.filter(include_on_network_map=True):
NetworkMapMembership.objects.create(
map=default_map,
contact=contact,
life_sphere=contact.life_sphere,
network_circle=contact.network_circle,
importance=contact.importance,
map_angle=contact.map_angle,
map_radius_ratio=contact.map_radius_ratio,
)
class Migration(migrations.Migration):
dependencies = [
('contacts', '0005_contact_map_angle_contact_map_radius_ratio'),
]
operations = [
migrations.CreateModel(
name='NetworkMap',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Название')),
('description', models.TextField(blank=True, verbose_name='Описание')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'Карта сети',
'verbose_name_plural': 'Карты сети',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='NetworkMapMembership',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('life_sphere', models.CharField(
choices=[
('work', 'Работа'), ('study', 'Учёба'), ('hobby', 'Хобби'),
('family', 'Семья'), ('health', 'Здоровье'), ('other', 'Другое'),
],
default='other',
max_length=32,
verbose_name='Сфера жизни',
)),
('network_circle', models.CharField(
choices=[
('support', 'Круг поддержки'),
('productivity', 'Круг продуктивности'),
('development', 'Круг развития'),
],
default='productivity',
max_length=32,
verbose_name='Круг сети',
)),
('importance', models.PositiveSmallIntegerField(
default=3,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(5),
],
verbose_name='Важность (15)',
)),
('map_angle', models.FloatField(blank=True, null=True, verbose_name='Угол позиции на карте')),
('map_radius_ratio', models.FloatField(
blank=True,
null=True,
validators=[
django.core.validators.MinValueValidator(0),
django.core.validators.MaxValueValidator(1),
],
verbose_name='Радиус позиции на карте (доля)',
)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('contact', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='map_memberships',
to='contacts.contact',
verbose_name='Контакт',
)),
('map', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='memberships',
to='contacts.networkmap',
verbose_name='Карта',
)),
],
options={
'verbose_name': 'Участие на карте',
'verbose_name_plural': 'Участия на картах',
'unique_together': {('map', 'contact')},
},
),
migrations.RunPython(migrate_contact_map_data, migrations.RunPython.noop),
migrations.RemoveField(model_name='contact', name='include_on_network_map'),
migrations.RemoveField(model_name='contact', name='life_sphere'),
migrations.RemoveField(model_name='contact', name='network_circle'),
migrations.RemoveField(model_name='contact', name='importance'),
migrations.RemoveField(model_name='contact', name='map_angle'),
migrations.RemoveField(model_name='contact', name='map_radius_ratio'),
]