From b93a677cc30d92a0efa0ca8be52614ed1eb67329 Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Mon, 14 Feb 2022 19:42:49 +0100 Subject: Geodata redesign: town migrations --- .../migrations/0107_auto_20220211_1630.py | 71 - .../migrations/0107_auto_20220214_1920.py | 71 + .../migrations/0107_auto_20220211_1630.py | 45 - .../migrations/0107_auto_20220214_1920.py | 45 + .../migrations/0109_auto_20220211_1630.py | 142 -- .../migrations/0109_auto_20220214_1920.py | 142 ++ .../migrations/0108_auto_20220211_1630.py | 17 - .../migrations/0108_auto_20220214_1920.py | 17 + .../migrations/0109_auto_20220211_1630.py | 201 -- .../migrations/0109_auto_20220214_1920.py | 201 ++ .../migrations/0113_auto_20220211_1630.py | 171 -- .../migrations/0113_auto_20220214_1920.py | 171 ++ ishtar_common/admin.py | 5 +- .../management/commands/migrate_to_geo_v4.py | 98 + .../migrations/0220_auto_20220211_1630.py | 114 -- .../migrations/0220_auto_20220214_1920.py | 124 ++ ishtar_common/models_common.py | 2135 ++++++++++---------- 17 files changed, 1951 insertions(+), 1819 deletions(-) delete mode 100644 archaeological_context_records/migrations/0107_auto_20220211_1630.py create mode 100644 archaeological_context_records/migrations/0107_auto_20220214_1920.py delete mode 100644 archaeological_files/migrations/0107_auto_20220211_1630.py create mode 100644 archaeological_files/migrations/0107_auto_20220214_1920.py delete mode 100644 archaeological_finds/migrations/0109_auto_20220211_1630.py create mode 100644 archaeological_finds/migrations/0109_auto_20220214_1920.py delete mode 100644 archaeological_operations/migrations/0108_auto_20220211_1630.py create mode 100644 archaeological_operations/migrations/0108_auto_20220214_1920.py delete mode 100644 archaeological_operations/migrations/0109_auto_20220211_1630.py create mode 100644 archaeological_operations/migrations/0109_auto_20220214_1920.py delete mode 100644 archaeological_warehouse/migrations/0113_auto_20220211_1630.py create mode 100644 archaeological_warehouse/migrations/0113_auto_20220214_1920.py create mode 100644 ishtar_common/management/commands/migrate_to_geo_v4.py delete mode 100644 ishtar_common/migrations/0220_auto_20220211_1630.py create mode 100644 ishtar_common/migrations/0220_auto_20220214_1920.py diff --git a/archaeological_context_records/migrations/0107_auto_20220211_1630.py b/archaeological_context_records/migrations/0107_auto_20220211_1630.py deleted file mode 100644 index 725cdbf7e..000000000 --- a/archaeological_context_records/migrations/0107_auto_20220211_1630.py +++ /dev/null @@ -1,71 +0,0 @@ -# Generated by Django 2.2.24 on 2022-02-11 16:30 - -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('ishtar_common', '0220_auto_20220211_1630'), - ('archaeological_context_records', '0106_auto_20210326_1434'), - ] - - operations = [ - migrations.AddField( - model_name='contextrecord', - name='geodata', - field=models.ManyToManyField(blank=True, related_name='related_items_archaeological_context_records_contextrecord', to='ishtar_common.GeoVectorData'), - ), - migrations.AddField( - model_name='contextrecord', - name='main_geodata', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='main_related_items_archaeological_context_records_contextrecord', to='ishtar_common.GeoVectorData'), - ), - migrations.AddField( - model_name='historicalcontextrecord', - name='main_geodata', - field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='ishtar_common.GeoVectorData'), - ), - migrations.AlterField( - model_name='contextrecord', - name='data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='contextrecord', - name='history_m2m', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='contextrecord', - name='spatial_reference_system', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.SpatialReferenceSystem', verbose_name='Spatial Reference System'), - ), - migrations.AlterField( - model_name='dating', - name='period', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='archaeological_operations.Period', verbose_name='Period'), - ), - migrations.AlterField( - model_name='historicalcontextrecord', - name='data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='historicalcontextrecord', - name='history_m2m', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='recordrelations', - name='relation_type', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='archaeological_context_records.RelationType'), - ), - migrations.AlterField( - model_name='relationtype', - name='inverse_relation', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='archaeological_context_records.RelationType', verbose_name='Inverse relation'), - ), - ] diff --git a/archaeological_context_records/migrations/0107_auto_20220214_1920.py b/archaeological_context_records/migrations/0107_auto_20220214_1920.py new file mode 100644 index 000000000..1e507d219 --- /dev/null +++ b/archaeological_context_records/migrations/0107_auto_20220214_1920.py @@ -0,0 +1,71 @@ +# Generated by Django 2.2.24 on 2022-02-14 19:20 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('ishtar_common', '0220_auto_20220214_1920'), + ('archaeological_context_records', '0106_auto_20210326_1434'), + ] + + operations = [ + migrations.AddField( + model_name='contextrecord', + name='geodata', + field=models.ManyToManyField(blank=True, related_name='related_items_archaeological_context_records_contextrecord', to='ishtar_common.GeoVectorData'), + ), + migrations.AddField( + model_name='contextrecord', + name='main_geodata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='main_related_items_archaeological_context_records_contextrecord', to='ishtar_common.GeoVectorData'), + ), + migrations.AddField( + model_name='historicalcontextrecord', + name='main_geodata', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='ishtar_common.GeoVectorData'), + ), + migrations.AlterField( + model_name='contextrecord', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='contextrecord', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='contextrecord', + name='spatial_reference_system', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.SpatialReferenceSystem', verbose_name='Spatial Reference System'), + ), + migrations.AlterField( + model_name='dating', + name='period', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='archaeological_operations.Period', verbose_name='Period'), + ), + migrations.AlterField( + model_name='historicalcontextrecord', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='historicalcontextrecord', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='recordrelations', + name='relation_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='archaeological_context_records.RelationType'), + ), + migrations.AlterField( + model_name='relationtype', + name='inverse_relation', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='archaeological_context_records.RelationType', verbose_name='Inverse relation'), + ), + ] diff --git a/archaeological_files/migrations/0107_auto_20220211_1630.py b/archaeological_files/migrations/0107_auto_20220211_1630.py deleted file mode 100644 index b5bcaebf1..000000000 --- a/archaeological_files/migrations/0107_auto_20220211_1630.py +++ /dev/null @@ -1,45 +0,0 @@ -# Generated by Django 2.2.24 on 2022-02-11 16:30 - -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('archaeological_files', '0106_auto_20210803_1730'), - ] - - operations = [ - migrations.AlterField( - model_name='file', - name='data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='file', - name='file_type', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='archaeological_files.FileType', verbose_name='File type'), - ), - migrations.AlterField( - model_name='file', - name='history_m2m', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='historicalfile', - name='data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='historicalfile', - name='history_m2m', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='job', - name='child', - field=models.ForeignKey(blank=True, help_text='Auto-add this job when a parent is added', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parents', to='archaeological_files.Job', verbose_name='Child'), - ), - ] diff --git a/archaeological_files/migrations/0107_auto_20220214_1920.py b/archaeological_files/migrations/0107_auto_20220214_1920.py new file mode 100644 index 000000000..ba14f1c46 --- /dev/null +++ b/archaeological_files/migrations/0107_auto_20220214_1920.py @@ -0,0 +1,45 @@ +# Generated by Django 2.2.24 on 2022-02-14 19:20 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('archaeological_files', '0106_auto_20210803_1730'), + ] + + operations = [ + migrations.AlterField( + model_name='file', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='file', + name='file_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='archaeological_files.FileType', verbose_name='File type'), + ), + migrations.AlterField( + model_name='file', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='historicalfile', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='historicalfile', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='job', + name='child', + field=models.ForeignKey(blank=True, help_text='Auto-add this job when a parent is added', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parents', to='archaeological_files.Job', verbose_name='Child'), + ), + ] diff --git a/archaeological_finds/migrations/0109_auto_20220211_1630.py b/archaeological_finds/migrations/0109_auto_20220211_1630.py deleted file mode 100644 index 42421f3ce..000000000 --- a/archaeological_finds/migrations/0109_auto_20220211_1630.py +++ /dev/null @@ -1,142 +0,0 @@ -# Generated by Django 2.2.24 on 2022-02-11 16:30 - -import archaeological_finds.models_treatments -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('ishtar_common', '0220_auto_20220211_1630'), - ('archaeological_finds', '0108_auto_20210602_2234'), - ] - - operations = [ - migrations.AddField( - model_name='basefind', - name='geodata', - field=models.ManyToManyField(blank=True, related_name='related_items_archaeological_finds_basefind', to='ishtar_common.GeoVectorData'), - ), - migrations.AddField( - model_name='basefind', - name='main_geodata', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='main_related_items_archaeological_finds_basefind', to='ishtar_common.GeoVectorData'), - ), - migrations.AddField( - model_name='historicalbasefind', - name='main_geodata', - field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='ishtar_common.GeoVectorData'), - ), - migrations.AlterField( - model_name='basefind', - name='data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='basefind', - name='history_m2m', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='basefind', - name='spatial_reference_system', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.SpatialReferenceSystem', verbose_name='Spatial Reference System'), - ), - migrations.AlterField( - model_name='find', - name='data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='find', - name='history_m2m', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='historicalbasefind', - name='data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='historicalbasefind', - name='history_m2m', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='historicalfind', - name='data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='historicalfind', - name='history_m2m', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='historicaltreatment', - name='data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='historicaltreatment', - name='history_m2m', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='historicaltreatmentfile', - name='data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='historicaltreatmentfile', - name='history_m2m', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='property', - name='data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='property', - name='history_m2m', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='treatment', - name='data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='treatment', - name='history_m2m', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='treatment', - name='treatment_state', - field=models.ForeignKey(default=archaeological_finds.models_treatments.TreatmentState.get_default, on_delete=django.db.models.deletion.PROTECT, to='archaeological_finds.TreatmentState', verbose_name='State'), - ), - migrations.AlterField( - model_name='treatmentfile', - name='data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='treatmentfile', - name='history_m2m', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='treatmentfile', - name='type', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='archaeological_finds.TreatmentFileType', verbose_name='Treatment request type'), - ), - migrations.AlterField( - model_name='treatmentfiletype', - name='treatment_type', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='archaeological_finds.TreatmentType'), - ), - ] diff --git a/archaeological_finds/migrations/0109_auto_20220214_1920.py b/archaeological_finds/migrations/0109_auto_20220214_1920.py new file mode 100644 index 000000000..90df20f3e --- /dev/null +++ b/archaeological_finds/migrations/0109_auto_20220214_1920.py @@ -0,0 +1,142 @@ +# Generated by Django 2.2.24 on 2022-02-14 19:20 + +import archaeological_finds.models_treatments +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('ishtar_common', '0220_auto_20220214_1920'), + ('archaeological_finds', '0108_auto_20210602_2234'), + ] + + operations = [ + migrations.AddField( + model_name='basefind', + name='geodata', + field=models.ManyToManyField(blank=True, related_name='related_items_archaeological_finds_basefind', to='ishtar_common.GeoVectorData'), + ), + migrations.AddField( + model_name='basefind', + name='main_geodata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='main_related_items_archaeological_finds_basefind', to='ishtar_common.GeoVectorData'), + ), + migrations.AddField( + model_name='historicalbasefind', + name='main_geodata', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='ishtar_common.GeoVectorData'), + ), + migrations.AlterField( + model_name='basefind', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='basefind', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='basefind', + name='spatial_reference_system', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.SpatialReferenceSystem', verbose_name='Spatial Reference System'), + ), + migrations.AlterField( + model_name='find', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='find', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='historicalbasefind', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='historicalbasefind', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='historicalfind', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='historicalfind', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='historicaltreatment', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='historicaltreatment', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='historicaltreatmentfile', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='historicaltreatmentfile', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='property', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='property', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='treatment', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='treatment', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='treatment', + name='treatment_state', + field=models.ForeignKey(default=archaeological_finds.models_treatments.TreatmentState.get_default, on_delete=django.db.models.deletion.PROTECT, to='archaeological_finds.TreatmentState', verbose_name='State'), + ), + migrations.AlterField( + model_name='treatmentfile', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='treatmentfile', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='treatmentfile', + name='type', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='archaeological_finds.TreatmentFileType', verbose_name='Treatment request type'), + ), + migrations.AlterField( + model_name='treatmentfiletype', + name='treatment_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='archaeological_finds.TreatmentType'), + ), + ] diff --git a/archaeological_operations/migrations/0108_auto_20220211_1630.py b/archaeological_operations/migrations/0108_auto_20220211_1630.py deleted file mode 100644 index 2a6f2d313..000000000 --- a/archaeological_operations/migrations/0108_auto_20220211_1630.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.24 on 2022-02-11 16:30 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('archaeological_operations', '0107_auto_20210326_1434'), - ] - - operations = [ - migrations.AlterModelOptions( - name='parcel', - options={'ordering': ('town', 'year', 'section', 'parcel_number'), 'verbose_name': 'Parcel', 'verbose_name_plural': 'Parcels'}, - ), - ] diff --git a/archaeological_operations/migrations/0108_auto_20220214_1920.py b/archaeological_operations/migrations/0108_auto_20220214_1920.py new file mode 100644 index 000000000..57f73219c --- /dev/null +++ b/archaeological_operations/migrations/0108_auto_20220214_1920.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.24 on 2022-02-14 19:20 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('archaeological_operations', '0107_auto_20210326_1434'), + ] + + operations = [ + migrations.AlterModelOptions( + name='parcel', + options={'ordering': ('town', 'year', 'section', 'parcel_number'), 'verbose_name': 'Parcel', 'verbose_name_plural': 'Parcels'}, + ), + ] diff --git a/archaeological_operations/migrations/0109_auto_20220211_1630.py b/archaeological_operations/migrations/0109_auto_20220211_1630.py deleted file mode 100644 index 12f509af1..000000000 --- a/archaeological_operations/migrations/0109_auto_20220211_1630.py +++ /dev/null @@ -1,201 +0,0 @@ -# Generated by Django 2.2.24 on 2022-02-11 16:30 - -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('archaeological_operations', '0108_auto_20220211_1630'), - ('ishtar_common', '0220_auto_20220211_1630'), - ] - - operations = [ - migrations.AddField( - model_name='archaeologicalsite', - name='geodata', - field=models.ManyToManyField(blank=True, related_name='related_items_archaeological_operations_archaeologicalsite', to='ishtar_common.GeoVectorData'), - ), - migrations.AddField( - model_name='archaeologicalsite', - name='main_geodata', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='main_related_items_archaeological_operations_archaeologicalsite', to='ishtar_common.GeoVectorData'), - ), - migrations.AddField( - model_name='historicalarchaeologicalsite', - name='main_geodata', - field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='ishtar_common.GeoVectorData'), - ), - migrations.AddField( - model_name='historicaloperation', - name='main_geodata', - field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='ishtar_common.GeoVectorData'), - ), - migrations.AddField( - model_name='operation', - name='geodata', - field=models.ManyToManyField(blank=True, related_name='related_items_archaeological_operations_operation', to='ishtar_common.GeoVectorData'), - ), - migrations.AddField( - model_name='operation', - name='main_geodata', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='main_related_items_archaeological_operations_operation', to='ishtar_common.GeoVectorData'), - ), - migrations.AlterField( - model_name='administrativeact', - name='act_type', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='archaeological_operations.ActType', verbose_name='Act type'), - ), - migrations.AlterField( - model_name='administrativeact', - name='data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='administrativeact', - name='history_m2m', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='archaeologicalsite', - name='data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='archaeologicalsite', - name='history_m2m', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='archaeologicalsite', - name='spatial_reference_system', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.SpatialReferenceSystem', verbose_name='Spatial Reference System'), - ), - migrations.AlterField( - model_name='historicaladministrativeact', - name='data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='historicaladministrativeact', - name='history_m2m', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='historicalarchaeologicalsite', - name='data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='historicalarchaeologicalsite', - name='history_m2m', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='historicaloperation', - name='data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='historicaloperation', - name='history_m2m', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='historicaloperation', - name='in_charge', - field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='ishtar_common.Person', verbose_name='Operation monitor'), - ), - migrations.AlterField( - model_name='historicaloperation', - name='scientist', - field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='ishtar_common.Person', verbose_name='Scientific manager'), - ), - migrations.AlterField( - model_name='operation', - name='applicant_authority', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='operation_applicant_authority', to='ishtar_common.Organization', verbose_name='Applicant authority'), - ), - migrations.AlterField( - model_name='operation', - name='data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='operation', - name='history_m2m', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='operation', - name='in_charge', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='operation_monitoring', to='ishtar_common.Person', verbose_name='Operation monitor'), - ), - migrations.AlterField( - model_name='operation', - name='minutes_writer', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='minutes_writer', to='ishtar_common.Person', verbose_name='Writer of the minutes'), - ), - migrations.AlterField( - model_name='operation', - name='operation_type', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='ishtar_common.OperationType', verbose_name='Operation type'), - ), - migrations.AlterField( - model_name='operation', - name='protagonist', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='operation_protagonist', to='ishtar_common.Person', verbose_name='Name of the protagonist'), - ), - migrations.AlterField( - model_name='operation', - name='scientist', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='operation_scientist_responsability', to='ishtar_common.Person', verbose_name='Scientific manager'), - ), - migrations.AlterField( - model_name='operation', - name='spatial_reference_system', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.SpatialReferenceSystem', verbose_name='Spatial Reference System'), - ), - migrations.AlterField( - model_name='parcel', - name='data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='parcel', - name='history_m2m', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='parcel', - name='town', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='parcels', to='ishtar_common.Town', verbose_name='Town'), - ), - migrations.AlterField( - model_name='parcelowner', - name='data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='parcelowner', - name='history_m2m', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='parcelowner', - name='owner', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='parcel_owner', to='ishtar_common.Person', verbose_name='Owner'), - ), - migrations.AlterField( - model_name='recordrelations', - name='relation_type', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='archaeological_operations.RelationType'), - ), - migrations.AlterField( - model_name='relationtype', - name='inverse_relation', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='archaeological_operations.RelationType', verbose_name='Inverse relation'), - ), - ] diff --git a/archaeological_operations/migrations/0109_auto_20220214_1920.py b/archaeological_operations/migrations/0109_auto_20220214_1920.py new file mode 100644 index 000000000..e5ef7152e --- /dev/null +++ b/archaeological_operations/migrations/0109_auto_20220214_1920.py @@ -0,0 +1,201 @@ +# Generated by Django 2.2.24 on 2022-02-14 19:20 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('archaeological_operations', '0108_auto_20220214_1920'), + ('ishtar_common', '0220_auto_20220214_1920'), + ] + + operations = [ + migrations.AddField( + model_name='archaeologicalsite', + name='geodata', + field=models.ManyToManyField(blank=True, related_name='related_items_archaeological_operations_archaeologicalsite', to='ishtar_common.GeoVectorData'), + ), + migrations.AddField( + model_name='archaeologicalsite', + name='main_geodata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='main_related_items_archaeological_operations_archaeologicalsite', to='ishtar_common.GeoVectorData'), + ), + migrations.AddField( + model_name='historicalarchaeologicalsite', + name='main_geodata', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='ishtar_common.GeoVectorData'), + ), + migrations.AddField( + model_name='historicaloperation', + name='main_geodata', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='ishtar_common.GeoVectorData'), + ), + migrations.AddField( + model_name='operation', + name='geodata', + field=models.ManyToManyField(blank=True, related_name='related_items_archaeological_operations_operation', to='ishtar_common.GeoVectorData'), + ), + migrations.AddField( + model_name='operation', + name='main_geodata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='main_related_items_archaeological_operations_operation', to='ishtar_common.GeoVectorData'), + ), + migrations.AlterField( + model_name='administrativeact', + name='act_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='archaeological_operations.ActType', verbose_name='Act type'), + ), + migrations.AlterField( + model_name='administrativeact', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='administrativeact', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='archaeologicalsite', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='archaeologicalsite', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='archaeologicalsite', + name='spatial_reference_system', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.SpatialReferenceSystem', verbose_name='Spatial Reference System'), + ), + migrations.AlterField( + model_name='historicaladministrativeact', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='historicaladministrativeact', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='historicalarchaeologicalsite', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='historicalarchaeologicalsite', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='historicaloperation', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='historicaloperation', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='historicaloperation', + name='in_charge', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='ishtar_common.Person', verbose_name='Operation monitor'), + ), + migrations.AlterField( + model_name='historicaloperation', + name='scientist', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='ishtar_common.Person', verbose_name='Scientific manager'), + ), + migrations.AlterField( + model_name='operation', + name='applicant_authority', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='operation_applicant_authority', to='ishtar_common.Organization', verbose_name='Applicant authority'), + ), + migrations.AlterField( + model_name='operation', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='operation', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='operation', + name='in_charge', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='operation_monitoring', to='ishtar_common.Person', verbose_name='Operation monitor'), + ), + migrations.AlterField( + model_name='operation', + name='minutes_writer', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='minutes_writer', to='ishtar_common.Person', verbose_name='Writer of the minutes'), + ), + migrations.AlterField( + model_name='operation', + name='operation_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='ishtar_common.OperationType', verbose_name='Operation type'), + ), + migrations.AlterField( + model_name='operation', + name='protagonist', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='operation_protagonist', to='ishtar_common.Person', verbose_name='Name of the protagonist'), + ), + migrations.AlterField( + model_name='operation', + name='scientist', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='operation_scientist_responsability', to='ishtar_common.Person', verbose_name='Scientific manager'), + ), + migrations.AlterField( + model_name='operation', + name='spatial_reference_system', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.SpatialReferenceSystem', verbose_name='Spatial Reference System'), + ), + migrations.AlterField( + model_name='parcel', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='parcel', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='parcel', + name='town', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='parcels', to='ishtar_common.Town', verbose_name='Town'), + ), + migrations.AlterField( + model_name='parcelowner', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='parcelowner', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='parcelowner', + name='owner', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='parcel_owner', to='ishtar_common.Person', verbose_name='Owner'), + ), + migrations.AlterField( + model_name='recordrelations', + name='relation_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='archaeological_operations.RelationType'), + ), + migrations.AlterField( + model_name='relationtype', + name='inverse_relation', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='archaeological_operations.RelationType', verbose_name='Inverse relation'), + ), + ] diff --git a/archaeological_warehouse/migrations/0113_auto_20220211_1630.py b/archaeological_warehouse/migrations/0113_auto_20220211_1630.py deleted file mode 100644 index 4b758ac05..000000000 --- a/archaeological_warehouse/migrations/0113_auto_20220211_1630.py +++ /dev/null @@ -1,171 +0,0 @@ -# Generated by Django 2.2.24 on 2022-02-11 16:30 - -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('ishtar_common', '0220_auto_20220211_1630'), - ('archaeological_warehouse', '0112_auto_20210308_1628'), - ] - - operations = [ - migrations.AddField( - model_name='container', - name='geodata', - field=models.ManyToManyField(blank=True, related_name='related_items_archaeological_warehouse_container', to='ishtar_common.GeoVectorData'), - ), - migrations.AddField( - model_name='container', - name='main_geodata', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='main_related_items_archaeological_warehouse_container', to='ishtar_common.GeoVectorData'), - ), - migrations.AddField( - model_name='historicalwarehouse', - name='main_geodata', - field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='ishtar_common.GeoVectorData'), - ), - migrations.AddField( - model_name='warehouse', - name='geodata', - field=models.ManyToManyField(blank=True, related_name='related_items_archaeological_warehouse_warehouse', to='ishtar_common.GeoVectorData'), - ), - migrations.AddField( - model_name='warehouse', - name='main_geodata', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='main_related_items_archaeological_warehouse_warehouse', to='ishtar_common.GeoVectorData'), - ), - migrations.AlterField( - model_name='container', - name='cached_weight', - field=models.FloatField(blank=True, help_text='Entered weight if available otherwise calculated weight.', null=True, verbose_name='Cached weight (g)'), - ), - migrations.AlterField( - model_name='container', - name='container_type', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='containers', to='archaeological_warehouse.ContainerType', verbose_name='Container type'), - ), - migrations.AlterField( - model_name='container', - name='data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='container', - name='history_m2m', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='container', - name='responsibility', - field=models.ForeignKey(blank=True, help_text='Warehouse that owns the container', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='responsibilities', to='archaeological_warehouse.Warehouse', verbose_name='Responsibility'), - ), - migrations.AlterField( - model_name='container', - name='responsible', - field=models.ForeignKey(blank=True, help_text='Deprecated - do not use', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owned_containers', to='archaeological_warehouse.Warehouse', verbose_name='Responsible warehouse'), - ), - migrations.AlterField( - model_name='container', - name='spatial_reference_system', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.SpatialReferenceSystem', verbose_name='Spatial Reference System'), - ), - migrations.AlterField( - model_name='container', - name='weight', - field=models.FloatField(blank=True, null=True, verbose_name='Measured weight (g)'), - ), - migrations.AlterField( - model_name='containertype', - name='stationary', - field=models.BooleanField(default=False, help_text='Container that will not usually be moved. Ex: building, room, span, shelf. Stationary containers are not automatically numbered.', verbose_name='Stationary'), - ), - migrations.AlterField( - model_name='historicalwarehouse', - name='data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='historicalwarehouse', - name='history_m2m', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='historicalwarehouse', - name='mobile_phone', - field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Mobile phone'), - ), - migrations.AlterField( - model_name='historicalwarehouse', - name='phone', - field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Phone'), - ), - migrations.AlterField( - model_name='historicalwarehouse', - name='phone2', - field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Phone description 2'), - ), - migrations.AlterField( - model_name='historicalwarehouse', - name='phone3', - field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Phone 3'), - ), - migrations.AlterField( - model_name='warehouse', - name='data', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='warehouse', - name='history_m2m', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), - ), - migrations.AlterField( - model_name='warehouse', - name='mobile_phone', - field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Mobile phone'), - ), - migrations.AlterField( - model_name='warehouse', - name='phone', - field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Phone'), - ), - migrations.AlterField( - model_name='warehouse', - name='phone2', - field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Phone description 2'), - ), - migrations.AlterField( - model_name='warehouse', - name='phone3', - field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Phone 3'), - ), - migrations.AlterField( - model_name='warehouse', - name='precise_town', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.Town', verbose_name='Town (precise)'), - ), - migrations.AlterField( - model_name='warehouse', - name='spatial_reference_system', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.SpatialReferenceSystem', verbose_name='Spatial Reference System'), - ), - migrations.AlterField( - model_name='warehouse', - name='warehouse_type', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='archaeological_warehouse.WarehouseType', verbose_name='Warehouse type'), - ), - migrations.AlterField( - model_name='warehousedivisionlink', - name='container_type', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='archaeological_warehouse.ContainerType'), - ), - migrations.AlterField( - model_name='warehousedivisionlink', - name='division', - field=models.ForeignKey(blank=True, help_text='Deprecated - do not use', null=True, on_delete=django.db.models.deletion.SET_NULL, to='archaeological_warehouse.WarehouseDivision'), - ), - ] diff --git a/archaeological_warehouse/migrations/0113_auto_20220214_1920.py b/archaeological_warehouse/migrations/0113_auto_20220214_1920.py new file mode 100644 index 000000000..1892b8adf --- /dev/null +++ b/archaeological_warehouse/migrations/0113_auto_20220214_1920.py @@ -0,0 +1,171 @@ +# Generated by Django 2.2.24 on 2022-02-14 19:20 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('ishtar_common', '0220_auto_20220214_1920'), + ('archaeological_warehouse', '0112_auto_20210308_1628'), + ] + + operations = [ + migrations.AddField( + model_name='container', + name='geodata', + field=models.ManyToManyField(blank=True, related_name='related_items_archaeological_warehouse_container', to='ishtar_common.GeoVectorData'), + ), + migrations.AddField( + model_name='container', + name='main_geodata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='main_related_items_archaeological_warehouse_container', to='ishtar_common.GeoVectorData'), + ), + migrations.AddField( + model_name='historicalwarehouse', + name='main_geodata', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='ishtar_common.GeoVectorData'), + ), + migrations.AddField( + model_name='warehouse', + name='geodata', + field=models.ManyToManyField(blank=True, related_name='related_items_archaeological_warehouse_warehouse', to='ishtar_common.GeoVectorData'), + ), + migrations.AddField( + model_name='warehouse', + name='main_geodata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='main_related_items_archaeological_warehouse_warehouse', to='ishtar_common.GeoVectorData'), + ), + migrations.AlterField( + model_name='container', + name='cached_weight', + field=models.FloatField(blank=True, help_text='Entered weight if available otherwise calculated weight.', null=True, verbose_name='Cached weight (g)'), + ), + migrations.AlterField( + model_name='container', + name='container_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='containers', to='archaeological_warehouse.ContainerType', verbose_name='Container type'), + ), + migrations.AlterField( + model_name='container', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='container', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='container', + name='responsibility', + field=models.ForeignKey(blank=True, help_text='Warehouse that owns the container', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='responsibilities', to='archaeological_warehouse.Warehouse', verbose_name='Responsibility'), + ), + migrations.AlterField( + model_name='container', + name='responsible', + field=models.ForeignKey(blank=True, help_text='Deprecated - do not use', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owned_containers', to='archaeological_warehouse.Warehouse', verbose_name='Responsible warehouse'), + ), + migrations.AlterField( + model_name='container', + name='spatial_reference_system', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.SpatialReferenceSystem', verbose_name='Spatial Reference System'), + ), + migrations.AlterField( + model_name='container', + name='weight', + field=models.FloatField(blank=True, null=True, verbose_name='Measured weight (g)'), + ), + migrations.AlterField( + model_name='containertype', + name='stationary', + field=models.BooleanField(default=False, help_text='Container that will not usually be moved. Ex: building, room, span, shelf. Stationary containers are not automatically numbered.', verbose_name='Stationary'), + ), + migrations.AlterField( + model_name='historicalwarehouse', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='historicalwarehouse', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='historicalwarehouse', + name='mobile_phone', + field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Mobile phone'), + ), + migrations.AlterField( + model_name='historicalwarehouse', + name='phone', + field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Phone'), + ), + migrations.AlterField( + model_name='historicalwarehouse', + name='phone2', + field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Phone description 2'), + ), + migrations.AlterField( + model_name='historicalwarehouse', + name='phone3', + field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Phone 3'), + ), + migrations.AlterField( + model_name='warehouse', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='warehouse', + name='history_m2m', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='warehouse', + name='mobile_phone', + field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Mobile phone'), + ), + migrations.AlterField( + model_name='warehouse', + name='phone', + field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Phone'), + ), + migrations.AlterField( + model_name='warehouse', + name='phone2', + field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Phone description 2'), + ), + migrations.AlterField( + model_name='warehouse', + name='phone3', + field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Phone 3'), + ), + migrations.AlterField( + model_name='warehouse', + name='precise_town', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.Town', verbose_name='Town (precise)'), + ), + migrations.AlterField( + model_name='warehouse', + name='spatial_reference_system', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.SpatialReferenceSystem', verbose_name='Spatial Reference System'), + ), + migrations.AlterField( + model_name='warehouse', + name='warehouse_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='archaeological_warehouse.WarehouseType', verbose_name='Warehouse type'), + ), + migrations.AlterField( + model_name='warehousedivisionlink', + name='container_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='archaeological_warehouse.ContainerType'), + ), + migrations.AlterField( + model_name='warehousedivisionlink', + name='division', + field=models.ForeignKey(blank=True, help_text='Deprecated - do not use', null=True, on_delete=django.db.models.deletion.SET_NULL, to='archaeological_warehouse.WarehouseDivision'), + ), + ] diff --git a/ishtar_common/admin.py b/ishtar_common/admin.py index 0ef72991f..b7cc5adf6 100644 --- a/ishtar_common/admin.py +++ b/ishtar_common/admin.py @@ -1022,7 +1022,7 @@ class TownAdmin(ImportGEOJSONActionAdmin, ImportActionAdmin): search_fields += ["numero_insee"] list_filter = ("areas",) form = AdminTownForm - autocomplete_fields = ["children"] + autocomplete_fields = ["children", "main_geodata", "geodata"] inlines = [TownParentInline] actions = [ export_as_csv_action(exclude=["center", "limit"]), @@ -2445,6 +2445,9 @@ class GeoVectorDataForm(forms.ModelForm): class GeoVectorDataAdmin(admin.ModelAdmin): model = models_common.GeoVectorData + search_fields = ["name"] + list_display = ["name", "origin", "data_type", "provider", "source_content_type"] + list_filter = ["origin", "data_type", "provider"] admin_site.register(models_common.GeoVectorData, GeoVectorDataAdmin) \ No newline at end of file diff --git a/ishtar_common/management/commands/migrate_to_geo_v4.py b/ishtar_common/management/commands/migrate_to_geo_v4.py new file mode 100644 index 000000000..f747cb72d --- /dev/null +++ b/ishtar_common/management/commands/migrate_to_geo_v4.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from django.contrib.contenttypes.models import ContentType +import csv +import datetime +import os +import sys + +from django.conf import settings +from django.core.management.base import BaseCommand + +from ishtar_common import models_common + + +log_path = os.sep.join([settings.ROOT_PATH, "logs"]) +if not os.path.exists(log_path): + os.mkdir(log_path, mode=0o770) + + +def migrate(quiet=False, log=True): + changed = [] + # create towns + q = models_common.Town.objects.exclude( + center__isnull=True, limit__isnull=True).exclude(main_geodata__isnull=False) + nb = q.count() + town_content_type = ContentType.objects.get(app_label='ishtar_common', model='town') + data_type, __ = models_common.GeoDataType.objects.get_or_create( + txt_idx="town-limit", defaults={"label": "Limites commune"}) + provider, __ = models_common.GeoProviderType.objects.get_or_create( + txt_idx="france-ign", defaults={"label": "IGN"}) + for idx, town in enumerate(q.all()): + if not quiet: + sys.stdout.write(f"\r[{percent(idx, nb)}] Migrate towns {idx + 1}/{nb}") + sys.stdout.flush() + attrs = { + "name": town._generate_cached_label(), + "source_content_type": town_content_type, + "source_id": town.pk, + "data_type": data_type, + "provider": provider, + } + if town.limit: + attrs["multi_polygon"] = town.limit + else: + attrs["point_2d"] = town.center + data, created = models_common.GeoVectorData.objects.get_or_create(**attrs) + if created: + changed.append(["geovectordata", data.name, data.pk]) + town.main_geodata = data + town.save() + + if log and changed: + filename = f"geo_migration-created-{get_time().replace(':', '')}.txt" + path = os.sep.join([log_path, filename]) + with open(path, 'w+') as fle: + writer = csv.writer(fle) + writer.writerow(["model", "name", "id"]) + for change in changed: + writer.writerow(change) + if not quiet: + sys.stdout.write(f"log: {path} written.") + if not quiet: + sys.stdout.write("\n") + + +def percent(current, total): + return f"{(current + 1) / total * 100:.1f}".rjust(4, "0") + "%" + + +def get_time(): + return datetime.datetime.now().isoformat().split(".")[0] + + +class Command(BaseCommand): + help = "Migrate to new geo data management" + + def add_arguments(self, parser): + parser.add_argument( + "--quiet", dest="quiet", action="store_true", help="Quiet output" + ) + parser.add_argument( + "--log", dest="log", action="store_false", help="Log into a file" + ) + + def handle(self, *args, **options): + log = options["log"] + quiet = options["quiet"] + if not quiet: + sys.stdout.write(f"[{get_time()}] Processing migration\n") + errors = migrate(quiet=quiet, log=log) + if not errors: + if not quiet: + sys.stdout.write(f"[{get_time()}] Migration finished\n") + sys.exit() + if not quiet: + sys.stdout.write("\n".join(errors)) + sys.exit(1) diff --git a/ishtar_common/migrations/0220_auto_20220211_1630.py b/ishtar_common/migrations/0220_auto_20220211_1630.py deleted file mode 100644 index 159b65fcd..000000000 --- a/ishtar_common/migrations/0220_auto_20220211_1630.py +++ /dev/null @@ -1,114 +0,0 @@ -# Generated by Django 2.2.24 on 2022-02-11 16:30 - -import django.contrib.gis.db.models.fields -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion -import ishtar_common.models_common -import re - - -class Migration(migrations.Migration): - - dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ('ishtar_common', '0219_auto_20220120_1552'), - ] - - operations = [ - migrations.CreateModel( - name='GeoDataType', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('label', models.TextField(verbose_name='Label')), - ('txt_idx', models.TextField(help_text='The slug is the standardized version of the name. It contains only lowercase letters, numbers and hyphens. Each slug must be unique.', unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+\\Z'), "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.", 'invalid')], verbose_name='Textual ID')), - ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), - ('available', models.BooleanField(default=True, verbose_name='Available')), - ('order', models.IntegerField(default=10, verbose_name='Order')), - ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.GeoDataType', verbose_name='Parent')), - ], - options={ - 'verbose_name': 'Geographic - Data type', - 'verbose_name_plural': 'Geographic - Data types', - 'ordering': ('order', 'label'), - }, - bases=(ishtar_common.models_common.Cached, models.Model), - ), - migrations.CreateModel( - name='GeoOriginType', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('label', models.TextField(verbose_name='Label')), - ('txt_idx', models.TextField(help_text='The slug is the standardized version of the name. It contains only lowercase letters, numbers and hyphens. Each slug must be unique.', unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+\\Z'), "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.", 'invalid')], verbose_name='Textual ID')), - ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), - ('available', models.BooleanField(default=True, verbose_name='Available')), - ('order', models.IntegerField(default=10, verbose_name='Order')), - ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.GeoOriginType', verbose_name='Parent')), - ], - options={ - 'verbose_name': 'Geographic - Origin type', - 'verbose_name_plural': 'Geographic - Origin types', - 'ordering': ('order', 'label'), - }, - bases=(ishtar_common.models_common.Cached, models.Model), - ), - migrations.CreateModel( - name='GeoProviderType', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('label', models.TextField(verbose_name='Label')), - ('txt_idx', models.TextField(help_text='The slug is the standardized version of the name. It contains only lowercase letters, numbers and hyphens. Each slug must be unique.', unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+\\Z'), "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.", 'invalid')], verbose_name='Textual ID')), - ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), - ('available', models.BooleanField(default=True, verbose_name='Available')), - ('order', models.IntegerField(default=10, verbose_name='Order')), - ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.GeoProviderType', verbose_name='Parent')), - ], - options={ - 'verbose_name': 'Geographic - Provider type', - 'verbose_name_plural': 'Geographic - Provider types', - 'ordering': ('order', 'label'), - }, - bases=(ishtar_common.models_common.Cached, models.Model), - ), - migrations.AlterModelOptions( - name='person', - options={'ordering': ['name', 'surname'], 'permissions': (('view_own_person', 'Can view own Person'), ('add_own_person', 'Can add own Person'), ('change_own_person', 'Can change own Person'), ('delete_own_person', 'Can delete own Person')), 'verbose_name': 'Person', 'verbose_name_plural': 'Persons'}, - ), - migrations.AlterModelOptions( - name='spatialreferencesystem', - options={'ordering': ('order', 'label'), 'verbose_name': 'Spatial reference system', 'verbose_name_plural': 'Spatial reference systems'}, - ), - migrations.CreateModel( - name='GeoVectorData', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(default='Default', max_length=200, verbose_name='Name')), - ('source_id', models.PositiveIntegerField()), - ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), - ('x', models.FloatField(blank=True, help_text='User input', null=True, verbose_name='X')), - ('y', models.FloatField(blank=True, help_text='User input', null=True, verbose_name='Y')), - ('z', models.FloatField(blank=True, help_text='User input', null=True, verbose_name='Z')), - ('cached_x', models.FloatField(blank=True, null=True, verbose_name='X (cached)')), - ('cached_y', models.FloatField(blank=True, null=True, verbose_name='Y (cached)')), - ('cached_z', models.FloatField(blank=True, null=True, verbose_name='Z (cached)')), - ('estimated_error_x', models.FloatField(blank=True, null=True, verbose_name='Estimated error for X')), - ('estimated_error_y', models.FloatField(blank=True, null=True, verbose_name='Estimated error for Y')), - ('estimated_error_z', models.FloatField(blank=True, null=True, verbose_name='Estimated error for Z')), - ('point_2d', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Point (2D)')), - ('point_3d', django.contrib.gis.db.models.fields.PointField(blank=True, dim=3, null=True, srid=4326, verbose_name='Point (3D)')), - ('multi_points', django.contrib.gis.db.models.fields.MultiPointField(blank=True, null=True, srid=4326, verbose_name='Multi points')), - ('multi_line', django.contrib.gis.db.models.fields.MultiLineStringField(blank=True, null=True, srid=4326, verbose_name='Multi line')), - ('multi_polygon', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=4326, verbose_name='Multi polygon')), - ('need_update', models.BooleanField(default=False, verbose_name='Need update')), - ('data_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.GeoDataType', verbose_name='Data type')), - ('origin', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.GeoOriginType', verbose_name='Origin')), - ('provider', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.GeoProviderType', verbose_name='Provider')), - ('source_content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='content_type_geovectordata', to='contenttypes.ContentType')), - ('spatial_reference_system', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.SpatialReferenceSystem', verbose_name='Spatial Reference System')), - ], - options={ - 'verbose_name': 'Geographic - Vector data', - 'verbose_name_plural': 'Geographic - Vector data', - }, - ), - ] diff --git a/ishtar_common/migrations/0220_auto_20220214_1920.py b/ishtar_common/migrations/0220_auto_20220214_1920.py new file mode 100644 index 000000000..2f40eab00 --- /dev/null +++ b/ishtar_common/migrations/0220_auto_20220214_1920.py @@ -0,0 +1,124 @@ +# Generated by Django 2.2.24 on 2022-02-14 19:20 + +import django.contrib.gis.db.models.fields +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import ishtar_common.models_common +import re + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('ishtar_common', '0219_auto_20220120_1552'), + ] + + operations = [ + migrations.CreateModel( + name='GeoDataType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('label', models.TextField(verbose_name='Label')), + ('txt_idx', models.TextField(help_text='The slug is the standardized version of the name. It contains only lowercase letters, numbers and hyphens. Each slug must be unique.', unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+\\Z'), "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.", 'invalid')], verbose_name='Textual ID')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('available', models.BooleanField(default=True, verbose_name='Available')), + ('order', models.IntegerField(default=10, verbose_name='Order')), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.GeoDataType', verbose_name='Parent')), + ], + options={ + 'verbose_name': 'Geographic - Data type', + 'verbose_name_plural': 'Geographic - Data types', + 'ordering': ('order', 'label'), + }, + bases=(ishtar_common.models_common.Cached, models.Model), + ), + migrations.CreateModel( + name='GeoOriginType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('label', models.TextField(verbose_name='Label')), + ('txt_idx', models.TextField(help_text='The slug is the standardized version of the name. It contains only lowercase letters, numbers and hyphens. Each slug must be unique.', unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+\\Z'), "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.", 'invalid')], verbose_name='Textual ID')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('available', models.BooleanField(default=True, verbose_name='Available')), + ('order', models.IntegerField(default=10, verbose_name='Order')), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.GeoOriginType', verbose_name='Parent')), + ], + options={ + 'verbose_name': 'Geographic - Origin type', + 'verbose_name_plural': 'Geographic - Origin types', + 'ordering': ('order', 'label'), + }, + bases=(ishtar_common.models_common.Cached, models.Model), + ), + migrations.CreateModel( + name='GeoProviderType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('label', models.TextField(verbose_name='Label')), + ('txt_idx', models.TextField(help_text='The slug is the standardized version of the name. It contains only lowercase letters, numbers and hyphens. Each slug must be unique.', unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+\\Z'), "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.", 'invalid')], verbose_name='Textual ID')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('available', models.BooleanField(default=True, verbose_name='Available')), + ('order', models.IntegerField(default=10, verbose_name='Order')), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.GeoProviderType', verbose_name='Parent')), + ], + options={ + 'verbose_name': 'Geographic - Provider type', + 'verbose_name_plural': 'Geographic - Provider types', + 'ordering': ('order', 'label'), + }, + bases=(ishtar_common.models_common.Cached, models.Model), + ), + migrations.AlterModelOptions( + name='person', + options={'ordering': ['name', 'surname'], 'permissions': (('view_own_person', 'Can view own Person'), ('add_own_person', 'Can add own Person'), ('change_own_person', 'Can change own Person'), ('delete_own_person', 'Can delete own Person')), 'verbose_name': 'Person', 'verbose_name_plural': 'Persons'}, + ), + migrations.AlterModelOptions( + name='spatialreferencesystem', + options={'ordering': ('order', 'label'), 'verbose_name': 'Spatial reference system', 'verbose_name_plural': 'Spatial reference systems'}, + ), + migrations.CreateModel( + name='GeoVectorData', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='Default', max_length=200, verbose_name='Name')), + ('source_id', models.PositiveIntegerField()), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('x', models.FloatField(blank=True, help_text='User input', null=True, verbose_name='X')), + ('y', models.FloatField(blank=True, help_text='User input', null=True, verbose_name='Y')), + ('z', models.FloatField(blank=True, help_text='User input', null=True, verbose_name='Z')), + ('cached_x', models.FloatField(blank=True, null=True, verbose_name='X (cached)')), + ('cached_y', models.FloatField(blank=True, null=True, verbose_name='Y (cached)')), + ('cached_z', models.FloatField(blank=True, null=True, verbose_name='Z (cached)')), + ('estimated_error_x', models.FloatField(blank=True, null=True, verbose_name='Estimated error for X')), + ('estimated_error_y', models.FloatField(blank=True, null=True, verbose_name='Estimated error for Y')), + ('estimated_error_z', models.FloatField(blank=True, null=True, verbose_name='Estimated error for Z')), + ('point_2d', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Point (2D)')), + ('point_3d', django.contrib.gis.db.models.fields.PointField(blank=True, dim=3, null=True, srid=4326, verbose_name='Point (3D)')), + ('multi_points', django.contrib.gis.db.models.fields.MultiPointField(blank=True, null=True, srid=4326, verbose_name='Multi points')), + ('multi_line', django.contrib.gis.db.models.fields.MultiLineStringField(blank=True, null=True, srid=4326, verbose_name='Multi line')), + ('multi_polygon', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=4326, verbose_name='Multi polygon')), + ('need_update', models.BooleanField(default=False, verbose_name='Need update')), + ('data_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.GeoDataType', verbose_name='Data type')), + ('origin', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.GeoOriginType', verbose_name='Origin')), + ('provider', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.GeoProviderType', verbose_name='Provider')), + ('source_content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='content_type_geovectordata', to='contenttypes.ContentType')), + ('spatial_reference_system', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.SpatialReferenceSystem', verbose_name='Spatial Reference System')), + ], + options={ + 'verbose_name': 'Geographic - Vector data', + 'verbose_name_plural': 'Geographic - Vector data', + }, + ), + migrations.AddField( + model_name='town', + name='geodata', + field=models.ManyToManyField(blank=True, related_name='related_items_ishtar_common_town', to='ishtar_common.GeoVectorData'), + ), + migrations.AddField( + model_name='town', + name='main_geodata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='main_related_items_ishtar_common_town', to='ishtar_common.GeoVectorData'), + ), + ] diff --git a/ishtar_common/models_common.py b/ishtar_common/models_common.py index ebf480ff0..cc3c61b05 100644 --- a/ishtar_common/models_common.py +++ b/ishtar_common/models_common.py @@ -1985,1213 +1985,1234 @@ class Canton(models.Model): return settings.JOINT.join((self.name, str(self.arrondissement))) -class TownManager(models.Manager): - def get_by_natural_key(self, numero_insee, year): - return self.get(numero_insee=numero_insee, year=year) - - -class Town(Imported, models.Model): - name = models.CharField(_("Name"), max_length=100) - surface = models.IntegerField(_("Surface (m2)"), blank=True, null=True) - center = models.PointField( - _("Localisation"), srid=settings.SRID, blank=True, null=True - ) - limit = models.MultiPolygonField(_("Limit"), blank=True, null=True) - numero_insee = models.CharField("Code commune (numéro INSEE)", max_length=120) - departement = models.ForeignKey( - Department, - verbose_name=_("Department"), - on_delete=models.SET_NULL, - null=True, - blank=True, - ) - year = models.IntegerField( - _("Year of creation"), - null=True, - blank=True, - help_text=_( - "Filling this field is relevant to distinguish old towns " "from new towns." - ), - ) - children = models.ManyToManyField( - "Town", verbose_name=_("Town children"), blank=True, related_name="parents" - ) - cached_label = models.CharField( - _("Cached name"), max_length=500, null=True, blank=True, db_index=True - ) - objects = TownManager() +class SpatialReferenceSystem(GeneralType): + order = models.IntegerField(_("Order"), default=10) + auth_name = models.CharField(_("Authority name"), default="EPSG", max_length=256) + srid = models.IntegerField(_("Authority SRID")) class Meta: - verbose_name = _("Town") - verbose_name_plural = _("Towns") - if settings.COUNTRY == "fr": - ordering = ["numero_insee"] - unique_together = (("numero_insee", "year"),) - - def natural_key(self): - return (self.numero_insee, self.year) - - def history_compress(self): - return {"numero_insee": self.numero_insee, "year": self.year or ""} + verbose_name = _("Spatial reference system") + verbose_name_plural = _("Spatial reference systems") + ordering = ( + "order", + "label", + ) @classmethod def get_documentation_string(cls): """ Used for automatic documentation generation """ - return "**name** {}, **numero_insee** {}, **cached_label** {}".format( - _("Name"), "Code commune (numéro INSEE)", _("Cached name") + doc = super(SpatialReferenceSystem, cls).get_documentation_string() + doc += ", **srid** {}, **auth_name** {}".format( + _("Authority SRID"), _("Authority name") ) + return doc - def get_values(self, prefix="", **kwargs): - return { - prefix or "label": str(self), - prefix + "name": self.name, - prefix + "numero_insee": self.numero_insee, - } - - @classmethod - def history_decompress(cls, full_value, create=False): - if not full_value: - return [] - res = [] - for value in full_value: - try: - res.append( - cls.objects.get( - numero_insee=value["numero_insee"], year=value["year"] or None - ) - ) - except cls.DoesNotExist: - continue - return res - - def __str__(self): - return self.cached_label or "" - - @property - def label_with_areas(self): - label = [self.name] - if self.numero_insee: - label.append("({})".format(self.numero_insee)) - for area in self.areas.all(): - label.append(" - ") - label.append(area.full_label) - return " ".join(label) - - def generate_geo(self, force=False): - force = self.generate_limit(force=force) - self.generate_center(force=force) - self.generate_area(force=force) - def generate_limit(self, force=False): - if not force and self.limit: - return - parents = None - if not self.parents.count(): - return - for parent in self.parents.all(): - if not parent.limit: - return - if not parents: - parents = parent.limit - else: - parents = parents.union(parent.limit) - # if union is a simple polygon make it a multi - if "MULTI" not in parents.wkt: - parents = parents.wkt.replace("POLYGON", "MULTIPOLYGON(") + ")" - if not parents: - return - self.limit = parents - self.save() - return True +post_save.connect(post_save_cache, sender=SpatialReferenceSystem) +post_delete.connect(post_save_cache, sender=SpatialReferenceSystem) - def generate_center(self, force=False): - if not force and (self.center or not self.limit): - return - self.center = self.limit.centroid - if not self.center: - return False - self.save() - return True - def generate_area(self, force=False): - if not force and (self.surface or not self.limit): - return - try: - surface = self.limit.transform(settings.SURFACE_SRID, clone=True).area - except GDALException: - return False - if surface > 214748364 or not surface: - return False - self.surface = surface - self.save() - return True +class GeoOriginType(HierarchicalType): + """ + ex: topographical surveys, georeferencing, ... + """ - def update_town_code(self): - if not self.numero_insee or not self.children.count() or not self.year: - return - old_num = self.numero_insee[:] - numero = old_num.split("-")[0] - self.numero_insee = "{}-{}".format(numero, self.year) - if self.numero_insee != old_num: - return True + order = models.IntegerField(_("Order"), default=10) - def _generate_cached_label(self): - cached_label = self.name - if settings.COUNTRY == "fr" and self.numero_insee: - dpt_len = 2 - if ( - self.numero_insee.startswith("97") - or self.numero_insee.startswith("98") - or self.numero_insee[0] - not in ("0", "1", "2", "3", "4", "5", "6", "7", "8", "9") - ): - dpt_len = 3 - cached_label = "%s - %s" % (self.name, self.numero_insee[:dpt_len]) - if self.year and self.children.count(): - cached_label += " ({})".format(self.year) - return cached_label + class Meta: + verbose_name = _("Geographic - Origin type") + verbose_name_plural = _("Geographic - Origin types") + ordering = ( + "order", + "label", + ) -def post_save_town(sender, **kwargs): - cached_label_changed(sender, **kwargs) - town = kwargs["instance"] - town.generate_geo() - if town.update_town_code(): - town.save() +class GeoDataType(HierarchicalType): + """ + ex: outline, z-sup, ... + """ + order = models.IntegerField(_("Order"), default=10) -post_save.connect(post_save_town, sender=Town) + class Meta: + verbose_name = _("Geographic - Data type") + verbose_name_plural = _("Geographic - Data types") + ordering = ( + "order", + "label", + ) -def town_child_changed(sender, **kwargs): - town = kwargs["instance"] - if town.update_town_code(): - town.save() +class GeoProviderType(HierarchicalType): + """ + ex: GeoNames, IGN, ... + """ + order = models.IntegerField(_("Order"), default=10) -m2m_changed.connect(town_child_changed, sender=Town.children.through) + class Meta: + verbose_name = _("Geographic - Provider type") + verbose_name_plural = _("Geographic - Provider types") + ordering = ( + "order", + "label", + ) -class Address(BaseHistorizedItem): - FIELDS = ( - "address", - "address_complement", - "postal_code", - "town", - "precise_town", - "country", - "alt_address", - "alt_address_complement", - "alt_postal_code", - "alt_town", - "alt_country", - "phone", - "phone_desc", - "phone2", - "phone_desc2", - "phone3", - "phone_desc3", - "raw_phone", - "mobile_phone", - "email", - "alt_address_is_prefered", - ) - address = models.TextField(_("Address"), blank=True, default="") - address_complement = models.TextField( - _("Address complement"), blank=True, default="" +class GeoVectorData(models.Model): + name = models.CharField(_("Name"), default=_("Default"), max_length=200) + source_content_type = models.ForeignKey( + ContentType, related_name="content_type_geovectordata", on_delete=models.CASCADE ) - postal_code = models.CharField( - _("Postal code"), max_length=10, null=True, blank=True + source_id = models.PositiveIntegerField() + source = GenericForeignKey("source_content_type", "source_id") + origin = models.ForeignKey( + GeoOriginType, + blank=True, + null=True, + on_delete=models.PROTECT, + verbose_name=_("Origin"), ) - town = models.CharField(_("Town (freeform)"), max_length=150, null=True, blank=True) - precise_town = models.ForeignKey( - Town, - verbose_name=_("Town (precise)"), - null=True, + data_type = models.ForeignKey( + GeoDataType, blank=True, - on_delete=models.SET_NULL, - ) - country = models.CharField(_("Country"), max_length=30, null=True, blank=True) - alt_address = models.TextField(_("Other address: address"), blank=True, default="") - alt_address_complement = models.TextField( - _("Other address: address complement"), blank=True, default="" - ) - alt_postal_code = models.CharField( - _("Other address: postal code"), max_length=10, null=True, blank=True - ) - alt_town = models.CharField( - _("Other address: town"), max_length=70, null=True, blank=True - ) - alt_country = models.CharField( - _("Other address: country"), max_length=30, null=True, blank=True - ) - phone = models.CharField(_("Phone"), max_length=32, null=True, blank=True) - phone_desc = models.CharField( - _("Phone description"), max_length=300, null=True, blank=True + null=True, + on_delete=models.PROTECT, + verbose_name=_("Data type"), ) - phone2 = models.CharField( - _("Phone description 2"), max_length=32, null=True, blank=True + provider = models.ForeignKey( + GeoProviderType, + blank=True, + null=True, + on_delete=models.PROTECT, + verbose_name=_("Provider"), ) - phone_desc2 = models.CharField( - _("Phone description 2"), max_length=300, null=True, blank=True + comment = models.TextField(_("Comment"), default="", blank=True) + x = models.FloatField(_("X"), blank=True, null=True, help_text=_("User input")) + y = models.FloatField(_("Y"), blank=True, null=True, help_text=_("User input")) + z = models.FloatField(_("Z"), blank=True, null=True, help_text=_("User input")) + # x == cached_x if user input else get it from other sources + # cached is converted to the display SRID + cached_x = models.FloatField(_("X (cached)"), blank=True, null=True) + cached_y = models.FloatField(_("Y (cached)"), blank=True, null=True) + cached_z = models.FloatField(_("Z (cached)"), blank=True, null=True) + estimated_error_x = models.FloatField( + _("Estimated error for X"), blank=True, null=True ) - phone3 = models.CharField(_("Phone 3"), max_length=32, null=True, blank=True) - phone_desc3 = models.CharField( - _("Phone description 3"), max_length=300, null=True, blank=True + estimated_error_y = models.FloatField( + _("Estimated error for Y"), blank=True, null=True ) - raw_phone = models.TextField(_("Raw phone"), blank=True, default="") - mobile_phone = models.CharField( - _("Mobile phone"), max_length=32, null=True, blank=True + estimated_error_z = models.FloatField( + _("Estimated error for Z"), blank=True, null=True ) - email = models.EmailField(_("Email"), max_length=300, blank=True, null=True) - alt_address_is_prefered = models.BooleanField( - _("Alternative address is prefered"), default=False + spatial_reference_system = models.ForeignKey( + SpatialReferenceSystem, + verbose_name=_("Spatial Reference System"), + blank=True, + null=True, + on_delete=models.PROTECT, ) - history = HistoricalRecords(inherit=True) - SUB_ADDRESSES = [] + point_2d = models.PointField(_("Point (2D)"), blank=True, null=True) + point_3d = models.PointField(_("Point (3D)"), blank=True, null=True, dim=3) + multi_points = models.MultiPointField(_("Multi points"), blank=True, null=True) + multi_line = models.MultiLineStringField(_("Multi line"), blank=True, null=True) + multi_polygon = models.MultiPolygonField(_("Multi polygon"), blank=True, null=True) + need_update = models.BooleanField(_("Need update"), default=False) class Meta: - abstract = True - - def get_short_html_items(self): - items = [] - if self.address: - items.append("""{}""".format(self.address)) - if self.address_complement: - items.append( - """{}""".format( - self.address_complement - ) - ) - if self.postal_code: - items.append( - """{}""".format(self.postal_code) - ) - if self.precise_town: - items.append( - """{}""".format(self.precise_town.name) - ) - elif self.town: - items.append("""{}""".format(self.town)) - if self.country: - items.append("""{}""".format(self.country)) - return items - - def get_short_html_detail(self): - html = """
""" - items = self.get_short_html_items() - if not items: - items = [ - "{}".format(_("No associated address")) - ] - html += "".join(items) - html += """
""" - return html - - def get_town_centroid(self): - if self.precise_town: - return self.precise_town.center, self._meta.verbose_name - for sub_address in self.SUB_ADDRESSES: - sub_item = getattr(self, sub_address) - if sub_item and sub_item.precise_town: - return sub_item.precise_town.center, sub_item._meta.verbose_name + verbose_name = _("Geographic - Vector data") + verbose_name_plural = _("Geographic - Vector data") - def get_town_polygons(self): - if self.precise_town: - return self.precise_town.limit, self._meta.verbose_name - for sub_address in self.SUB_ADDRESSES: - sub_item = getattr(self, sub_address) - if sub_item and sub_item.precise_town: - return sub_item.precise_town.limit, sub_item._meta.verbose_name + def __str__(self): + name = self.name + if self.data_type: + name += f" ({str(self.data_type).lower()})" + return name - def get_attribute(self, attr): - if self.town or self.precise_town: - return getattr(self, attr) - for sub_address in self.SUB_ADDRESSES: - sub_item = getattr(self, sub_address) - if not sub_item: - continue - if sub_item.town or sub_item.precise_town: - return getattr(sub_item, attr) - return getattr(self, attr) + def display_coordinates(self, rounded=5, dim=2, cache=True): + srid = None + profile = get_current_profile() + if profile.display_srs and profile.display_srs.srid: + srid = profile.display_srs.srid + return self.get_coordinates(rounded=rounded, srid=srid, dim=dim, cache=cache) - def get_address(self): - return self.get_attribute("address") + def get_coordinates(self, rounded=5, srid: int = None, dim=2, cache=False): + if dim not in (2, 3): + raise ValueError(_("Only 2 or 3 dimension")) + if cache: + coordinates = [self.cached_x, self.cached_y] + if dim == 3: + coordinates.append(self.cached_z) + else: + if self.x and self.y: # user input + if not srid or srid == self.spatial_reference_system.srid: + coordinates = [self.x, self.y] + if dim == 3: + coordinates.append(self.z) + else: + args = { + "x": self.x, + "y": self.y, + "srid": self.spatial_reference_system.srid, + } + if dim == 3: + args["z"] = self.z + point = Point(**args).transform(srid, clone=True) + coordinates = [point.x, point.y] + if dim == 3: + coordinates.append(point.z) + elif self.point_2d and dim == 2: + point = self.point_2d.transform(srid, clone=True) + coordinates = [point.x, point.y] + elif self.point_3d and dim == 3: + point = self.point_3d.transform(srid, clone=True) + coordinates = [point.x, point.y, point.z] + else: + return + if not rounded: + return coordinates + return [round(coord, rounded) for coord in coordinates] - def get_address_complement(self): - return self.get_attribute("address_complement") + def get_coordinates_from_polygon(self, rounded=5, srid: int = None): + if self.multi_polygon: + return self.convert_coordinates( + self.multi_polygon.centroid, rounded=rounded, srid=srid + ) - def get_postal_code(self): - return self.get_attribute("postal_code") + def get_x(self, srid: int = None) -> float: + coord = self.get_coordinates(srid) + if coord: + return coord[0] - def get_town(self): - return self.get_attribute("town") + def get_y(self, srid: int = None) -> float: + coord = self.get_coordinates(srid) + if coord: + return coord[1] - def get_precise_town(self): - return self.get_attribute("precise_town") + def get_z(self, srid: int = None) -> float: + coord = self.get_coordinates(srid, dim=3) + if coord: + return coord[2] - def get_country(self): - return self.get_attribute("country") + @property + def display_spatial_reference_system(self): + profile = get_current_profile() + if not profile.display_srs or not profile.display_srs.srid: + return self.spatial_reference_system + return profile.display_srs - def simple_lbl(self): - return str(self) + def get_geo_items(self, get_polygons, rounded=5): + label = self.label if hasattr(self, "label") else self.short_label + dct = {"type": "Feature", "geometry": {}, "properties": {"label": label}} + if get_polygons: + list_coords = [] + if self.multi_polygon: + for polygon in self.multi_polygon: + list_coords.append([]) + for linear_ring in range(len(polygon)): + list_coords[-1].append([]) + for coords in polygon[linear_ring].coords: + point_2d = Point( + coords[0], coords[1], srid=self.multi_polygon.srid + ) + list_coords[-1][linear_ring].append( + self.convert_coordinates(point_2d, rounded) + ) + dct["geometry"]["type"] = "MultiPolygon" + dct["geometry"]["coordinates"] = list_coords + else: + dct["geometry"]["type"] = "Point" + coords = self.display_coordinates() + if coords: + dct["geometry"]["coordinates"] = coords + elif self.multi_polygon: + dct["geometry"]["coordinates"] = self.convert_coordinates( + self.multi_polygon.centroid, rounded + ) + else: + return {} + return dct - def full_address(self): - lbl = self.simple_lbl() - if lbl: - lbl += "\n" - lbl += self.address_lbl() - return lbl + def convert_coordinates(self, point_2d, rounded=5, srid=None): + if not srid: + profile = get_current_profile() + if profile.display_srs and profile.display_srs.srid: + srid = profile.display_srs.srid + if not srid: + x, y = point_2d.x, point_2d.y + else: + point = point_2d.transform(srid, clone=True) + x, y = point.x, point.y + if rounded: + return [round(x, rounded), round(y, rounded)] + return [x, y] - def address_lbl(self, list=False): - lbls = [] - prefix = "" - if self.alt_address_is_prefered: - prefix = "alt_" - if getattr(self, prefix + "address"): - lbls.append(( - getattr(self, prefix + "address"), - _("Address") - )) - if getattr(self, prefix + "address_complement"): - lbls.append(( - getattr(self, prefix + "address_complement"), - _("Address complement") - )) - postal_code = getattr(self, prefix + "postal_code") - town = getattr(self, prefix + "town") - if postal_code or town: - lbls.append(( - " ".join([postal_code, town]), - _("Postal code - Town") - )) - if self.phone: - lbls.append(( - self.phone, - _("Phone") - )) - if self.mobile_phone: - lbls.append(( - self.mobile_phone, - _("Mobile") - )) - if self.email: - lbls.append(( - self.email, - _("Email") - )) - if list: - return lbls - return "\n".join([ - value for value, lbl in lbls - ]) + def most_precise_geo(self): + if self.multi_polygon: + return "multi_polygon" + if self.point_2d: + return "point" - def address_lbl_list(self): - return self.address_lbl(list=True) + def _geojson_serialize(self, geom_attr): + if not hasattr(self, geom_attr): + return "" + geojson = serialize( + "geojson", + self.__class__.objects.filter(pk=self.pk), + geometry_field=geom_attr, + fields=("name",), + ) + geojson_dct = json.loads(geojson) + profile = get_current_profile() + precision = profile.point_precision + features = geojson_dct.pop("features") + for idx in range(len(features)): + feature = features[idx] + lbl = feature["properties"].pop("name") + feature["properties"]["name"] = lbl + feature["properties"]["id"] = self.pk + if precision is not None: + geom_type = feature["geometry"].get("type", None) + if geom_type == "Point": + feature["geometry"]["coordinates"] = [ + round(coord, precision) + for coord in feature["geometry"]["coordinates"] + ] + geojson_dct["features"] = features + geojson_dct["link_template"] = simple_link_to_window(self).replace( + "999999", "" + ) + geojson = json.dumps(geojson_dct) + return geojson -class Merge(models.Model): - merge_key = models.TextField(_("Merge key"), blank=True, null=True) - merge_candidate = models.ManyToManyField("self", blank=True) - merge_exclusion = models.ManyToManyField("self", blank=True) - archived = models.NullBooleanField(default=False, blank=True, null=True) - # 1 for one word similarity, 2 for two word similarity, etc. - MERGE_CLEMENCY = None - EMPTY_MERGE_KEY = "--" - MERGE_ATTRIBUTE = "name" + @property + def point_2d_geojson(self): + return self._geojson_serialize("point_2d") - class Meta: - abstract = True + @property + def multi_polygon_geojson(self): + return self._geojson_serialize("multi_polygon") - def generate_merge_key(self): - if self.archived: - return - merge_attr = getattr(self, self.MERGE_ATTRIBUTE) - self.merge_key = slugify(merge_attr if merge_attr else "") - if not self.merge_key: - self.merge_key = self.EMPTY_MERGE_KEY - self.merge_key = self.merge_key - def generate_merge_candidate(self): - if self.archived: - return - if not self.merge_key: - self.generate_merge_key() - self.save(merge_key_generated=True) - if not self.pk or self.merge_key == self.EMPTY_MERGE_KEY: - return - q = ( - self.__class__.objects.exclude(pk=self.pk) - .exclude(merge_exclusion=self) - .exclude(merge_candidate=self) - .exclude(archived=True) - ) - if not self.MERGE_CLEMENCY: - q = q.filter(merge_key=self.merge_key) - else: - subkeys_front = "-".join(self.merge_key.split("-")[: self.MERGE_CLEMENCY]) - subkeys_back = "-".join(self.merge_key.split("-")[-self.MERGE_CLEMENCY :]) - q = q.filter( - Q(merge_key__istartswith=subkeys_front) - | Q(merge_key__iendswith=subkeys_back) - ) - for item in q.all(): - self.merge_candidate.add(item) +post_save.connect(post_save_geodata, sender=GeoVectorData) - def save(self, *args, **kwargs): - # prevent circular save - merge_key_generated = False - if "merge_key_generated" in kwargs: - merge_key_generated = kwargs.pop("merge_key_generated") - self.generate_merge_key() - item = super(Merge, self).save(*args, **kwargs) - if not merge_key_generated: - self.merge_candidate.clear() - self.generate_merge_candidate() - return item - def archive(self): - self.archived = True - self.save() - self.merge_candidate.clear() - self.merge_exclusion.clear() +class GeographicItem(models.Model): + main_geodata = models.ForeignKey( + GeoVectorData, + on_delete=models.SET_NULL, + blank=True, + null=True, + related_name="main_related_items_%(app_label)s_%(class)s", + ) + geodata = models.ManyToManyField( + GeoVectorData, blank=True, related_name="related_items_%(app_label)s_%(class)s" + ) - def merge(self, item, keep_old=False, exclude_fields=None): - merge_model_objects( - self, item, keep_old=keep_old, exclude_fields=exclude_fields + class Meta: + abstract = True + + def save( + self, force_insert=False, force_update=False, using=None, update_fields=None + ): + super(GeographicItem, self).save( + force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields, ) - self.generate_merge_candidate() + if self.main_geodata and not self.geodata.filter(pk=self.main_geodata.pk): + self.geodata.add(self.main_geodata) -def __get_stats_cache_values(model_name, model_pk): - StatsCache = apps.get_model("ishtar_common", "StatsCache") - q = StatsCache.objects.filter(model=model_name, model_pk=model_pk) - nb = q.count() - if nb >= 1: - sc = q.all()[0] - for extra in q.order_by("-id").all()[1:]: - extra.delete() - else: - sc = StatsCache.objects.create(model=model_name, model_pk=model_pk) - values = sc.values - if not values: - values = {} - return sc, values +class TownManager(models.Manager): + def get_by_natural_key(self, numero_insee, year): + return self.get(numero_insee=numero_insee, year=year) -@task() -def _update_stats(app, model, model_pk, funcname): - model_name = app + "." + model - model = apps.get_model(app, model) - try: - item = model.objects.get(pk=model_pk) - except model.DoesNotExist: - return - value = getattr(item, funcname)() - sc, current_values = __get_stats_cache_values(model_name, model_pk) - current_values[funcname] = value - sc.values = current_values - sc.update_requested = None - sc.updated = datetime.datetime.now() - sc.save() +class Town(GeographicItem, Imported, models.Model): + name = models.CharField(_("Name"), max_length=100) + surface = models.IntegerField(_("Surface (m2)"), blank=True, null=True) + center = models.PointField( + _("Localisation"), srid=settings.SRID, blank=True, null=True + ) + limit = models.MultiPolygonField(_("Limit"), blank=True, null=True) + numero_insee = models.CharField("Code commune (numéro INSEE)", max_length=120) + departement = models.ForeignKey( + Department, + verbose_name=_("Department"), + on_delete=models.SET_NULL, + null=True, + blank=True, + ) + year = models.IntegerField( + _("Year of creation"), + null=True, + blank=True, + help_text=_( + "Filling this field is relevant to distinguish old towns " "from new towns." + ), + ) + children = models.ManyToManyField( + "Town", verbose_name=_("Town children"), blank=True, related_name="parents" + ) + cached_label = models.CharField( + _("Cached name"), max_length=500, null=True, blank=True, db_index=True + ) + objects = TownManager() + class Meta: + verbose_name = _("Town") + verbose_name_plural = _("Towns") + if settings.COUNTRY == "fr": + ordering = ["numero_insee"] + unique_together = (("numero_insee", "year"),) -def update_stats(statscache, item, funcname): - if not settings.USE_BACKGROUND_TASK: - current_values = statscache.values - if not current_values: - current_values = {} - value = getattr(item, funcname)() - current_values[funcname] = value - statscache.values = current_values - statscache.updated = datetime.datetime.now() - statscache.save() - return current_values - - now = datetime.datetime.now() - app_name = item._meta.app_label - model_name = item._meta.model_name - statscache.update_requested = now.isoformat() - statscache.save() - _update_stats.delay(app_name, model_name, item.pk, funcname) - return statscache.values - + def natural_key(self): + return (self.numero_insee, self.year) -class DashboardFormItem: - """ - Provide methods to manage statistics - """ + def history_compress(self): + return {"numero_insee": self.numero_insee, "year": self.year or ""} - def last_stats_update(self): - model_name = self._meta.app_label + "." + self._meta.model_name - StatsCache = apps.get_model("ishtar_common", "StatsCache") - q = StatsCache.objects.filter(model=model_name, model_pk=self.pk).order_by( - "-updated" + @classmethod + def get_documentation_string(cls): + """ + Used for automatic documentation generation + """ + return "**name** {}, **numero_insee** {}, **cached_label** {}".format( + _("Name"), "Code commune (numéro INSEE)", _("Cached name") ) - if not q.count(): - return - return q.all()[0].updated - def _get_or_set_stats(self, funcname, update=False, expected_type=None): - model_name = self._meta.app_label + "." + self._meta.model_name - StatsCache = apps.get_model("ishtar_common", "StatsCache") - sc, __ = StatsCache.objects.get_or_create(model=model_name, model_pk=self.pk) - if not update: - values = sc.values - if funcname not in values: - if expected_type is not None: - return expected_type() - return 0 - else: - values = update_stats(sc, self, funcname) - if funcname in values: - values = values[funcname] - else: - values = 0 - if expected_type is not None and not isinstance(values, expected_type): - return expected_type() - return values + def get_values(self, prefix="", **kwargs): + return { + prefix or "label": str(self), + prefix + "name": self.name, + prefix + "numero_insee": self.numero_insee, + } @classmethod - def get_periods(cls, slice="month", fltr={}, date_source="creation"): - date_var = date_source + "_date" - q = cls.objects.filter(**{date_var + "__isnull": False}) - if fltr: - q = q.filter(**fltr) - if slice == "year": - return [ - res[date_var].year - for res in list(q.values(date_var).annotate(Count("id")).order_by()) - ] - elif slice == "month": - return [ - (res[date_var].year, res[date_var].month) - for res in list(q.values(date_var).annotate(Count("id")).order_by()) - ] - return [] + def history_decompress(cls, full_value, create=False): + if not full_value: + return [] + res = [] + for value in full_value: + try: + res.append( + cls.objects.get( + numero_insee=value["numero_insee"], year=value["year"] or None + ) + ) + except cls.DoesNotExist: + continue + return res - @classmethod - def get_by_year(cls, year, fltr={}, date_source="creation"): - date_var = date_source + "_date" - q = cls.objects.filter(**{date_var + "__isnull": False}) - if fltr: - q = q.filter(**fltr) - return q.filter(**{date_var + "__year": year}).order_by("pk").distinct("pk") + def __str__(self): + return self.cached_label or "" - @classmethod - def get_by_month(cls, year, month, fltr={}, date_source="creation"): - date_var = date_source + "_date" - q = cls.objects.filter(**{date_var + "__isnull": False}) - if fltr: - q = q.filter(**fltr) - q = q.filter(**{date_var + "__year": year, date_var + "__month": month}) - return q.order_by("pk").distinct("pk") + @property + def label_with_areas(self): + label = [self.name] + if self.numero_insee: + label.append("({})".format(self.numero_insee)) + for area in self.areas.all(): + label.append(" - ") + label.append(area.full_label) + return " ".join(label) - @classmethod - def get_total_number(cls, fltr=None): - q = cls.objects - if fltr: - q = q.filter(**fltr) - return q.order_by("pk").distinct("pk").count() + def generate_geo(self, force=False): + force = self.generate_limit(force=force) + self.generate_center(force=force) + self.generate_area(force=force) + def generate_limit(self, force=False): + if not force and self.limit: + return + parents = None + if not self.parents.count(): + return + for parent in self.parents.all(): + if not parent.limit: + return + if not parents: + parents = parent.limit + else: + parents = parents.union(parent.limit) + # if union is a simple polygon make it a multi + if "MULTI" not in parents.wkt: + parents = parents.wkt.replace("POLYGON", "MULTIPOLYGON(") + ")" + if not parents: + return + self.limit = parents + self.save() + return True -class DocumentItem: - ALT_NAMES = { - "documents__image__isnull": SearchAltName( - pgettext_lazy("key for text search", "has-image"), - "documents__image__isnull", - ), - "documents__associated_url__isnull": SearchAltName( - pgettext_lazy("key for text search", "has-url"), - "documents__associated_url__isnull", - ), - "documents__associated_file__isnull": SearchAltName( - pgettext_lazy("key for text search", "has-attached-file"), - "documents__associated_file__isnull", - ), - } + def generate_center(self, force=False): + if not force and (self.center or not self.limit): + return + self.center = self.limit.centroid + if not self.center: + return False + self.save() + return True - def documents_list(self) -> list: - Document = apps.get_model("ishtar_common", "Document") - return self.get_associated_main_item_list("documents", Document) + def generate_area(self, force=False): + if not force and (self.surface or not self.limit): + return + try: + surface = self.limit.transform(settings.SURFACE_SRID, clone=True).area + except GDALException: + return False + if surface > 214748364 or not surface: + return False + self.surface = surface + self.save() + return True - def public_representation(self): - images = [] - if getattr(self, "main_image", None): - images.append(self.main_image.public_representation()) - images += [ - image.public_representation() - for image in self.images_without_main_image.all() - ] - return {"images": images} + def update_town_code(self): + if not self.numero_insee or not self.children.count() or not self.year: + return + old_num = self.numero_insee[:] + numero = old_num.split("-")[0] + self.numero_insee = "{}-{}".format(numero, self.year) + if self.numero_insee != old_num: + return True - @property - def images(self): - if not hasattr(self, "documents"): - Document = apps.get_model("ishtar_common", "Document") - return Document.objects.none() - return ( - self.documents.filter(image__isnull=False).exclude(image="").order_by("pk") - ) + def _generate_cached_label(self): + cached_label = self.name + if settings.COUNTRY == "fr" and self.numero_insee: + dpt_len = 2 + if ( + self.numero_insee.startswith("97") + or self.numero_insee.startswith("98") + or self.numero_insee[0] + not in ("0", "1", "2", "3", "4", "5", "6", "7", "8", "9") + ): + dpt_len = 3 + cached_label = "%s - %s" % (self.name, self.numero_insee[:dpt_len]) + if self.year and self.children.count(): + cached_label += " ({})".format(self.year) + return cached_label - @property - def images_number(self): - return self.images.count() - @property - def images_without_main_image(self): - if not hasattr(self, "main_image") or not hasattr(self, "documents"): - return self.images - if not self.main_image: - return ( - self.documents.filter(image__isnull=False) - .exclude(image="") - .order_by("pk") - ) - return ( - self.documents.filter(image__isnull=False) - .exclude(image="") - .exclude(pk=self.main_image.pk) - .order_by("pk") - ) +def post_save_town(sender, **kwargs): + cached_label_changed(sender, **kwargs) + town = kwargs["instance"] + town.generate_geo() + if town.update_town_code(): + town.save() - @property - def pdf_attached(self): - for document in self.documents.filter( - Q(associated_file__isnull=False) | Q(source__associated_file__isnull=False) - ).all(): - return document.pdf_attached - def get_extra_actions(self, request): - """ - For sheet template: return "Add document / image" action - """ - # url, base_text, icon, extra_text, extra css class, is a quick action - try: - actions = super(DocumentItem, self).get_extra_actions(request) - except AttributeError: - actions = [] +post_save.connect(post_save_town, sender=Town) - if not hasattr(self, "SLUG"): - return actions - can_add_doc = self.can_do(request, "add_document") - if can_add_doc and ( - not hasattr(self, "is_locked") or not self.is_locked(request.user) - ): - actions += [ - ( - reverse("create-document") + "?{}={}".format(self.SLUG, self.pk), - _("Add document/image"), - "fa fa-plus", - _("doc./image"), - "", - False, +def town_child_changed(sender, **kwargs): + town = kwargs["instance"] + if town.update_town_code(): + town.save() + + +m2m_changed.connect(town_child_changed, sender=Town.children.through) + + +class Address(BaseHistorizedItem): + FIELDS = ( + "address", + "address_complement", + "postal_code", + "town", + "precise_town", + "country", + "alt_address", + "alt_address_complement", + "alt_postal_code", + "alt_town", + "alt_country", + "phone", + "phone_desc", + "phone2", + "phone_desc2", + "phone3", + "phone_desc3", + "raw_phone", + "mobile_phone", + "email", + "alt_address_is_prefered", + ) + address = models.TextField(_("Address"), blank=True, default="") + address_complement = models.TextField( + _("Address complement"), blank=True, default="" + ) + postal_code = models.CharField( + _("Postal code"), max_length=10, null=True, blank=True + ) + town = models.CharField(_("Town (freeform)"), max_length=150, null=True, blank=True) + precise_town = models.ForeignKey( + Town, + verbose_name=_("Town (precise)"), + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + country = models.CharField(_("Country"), max_length=30, null=True, blank=True) + alt_address = models.TextField(_("Other address: address"), blank=True, default="") + alt_address_complement = models.TextField( + _("Other address: address complement"), blank=True, default="" + ) + alt_postal_code = models.CharField( + _("Other address: postal code"), max_length=10, null=True, blank=True + ) + alt_town = models.CharField( + _("Other address: town"), max_length=70, null=True, blank=True + ) + alt_country = models.CharField( + _("Other address: country"), max_length=30, null=True, blank=True + ) + phone = models.CharField(_("Phone"), max_length=32, null=True, blank=True) + phone_desc = models.CharField( + _("Phone description"), max_length=300, null=True, blank=True + ) + phone2 = models.CharField( + _("Phone description 2"), max_length=32, null=True, blank=True + ) + phone_desc2 = models.CharField( + _("Phone description 2"), max_length=300, null=True, blank=True + ) + phone3 = models.CharField(_("Phone 3"), max_length=32, null=True, blank=True) + phone_desc3 = models.CharField( + _("Phone description 3"), max_length=300, null=True, blank=True + ) + raw_phone = models.TextField(_("Raw phone"), blank=True, default="") + mobile_phone = models.CharField( + _("Mobile phone"), max_length=32, null=True, blank=True + ) + email = models.EmailField(_("Email"), max_length=300, blank=True, null=True) + alt_address_is_prefered = models.BooleanField( + _("Alternative address is prefered"), default=False + ) + history = HistoricalRecords(inherit=True) + SUB_ADDRESSES = [] + + class Meta: + abstract = True + + def get_short_html_items(self): + items = [] + if self.address: + items.append("""{}""".format(self.address)) + if self.address_complement: + items.append( + """{}""".format( + self.address_complement ) + ) + if self.postal_code: + items.append( + """{}""".format(self.postal_code) + ) + if self.precise_town: + items.append( + """{}""".format(self.precise_town.name) + ) + elif self.town: + items.append("""{}""".format(self.town)) + if self.country: + items.append("""{}""".format(self.country)) + return items + + def get_short_html_detail(self): + html = """
""" + items = self.get_short_html_items() + if not items: + items = [ + "{}".format(_("No associated address")) ] - return actions + html += "".join(items) + html += """
""" + return html + def get_town_centroid(self): + if self.precise_town: + return self.precise_town.center, self._meta.verbose_name + for sub_address in self.SUB_ADDRESSES: + sub_item = getattr(self, sub_address) + if sub_item and sub_item.precise_town: + return sub_item.precise_town.center, sub_item._meta.verbose_name -def clean_duplicate_association(document, related_item, action): - profile = get_current_profile() - if not profile.clean_redundant_document_association or action != "post_add": - return - class_name = related_item.__class__.__name__ - if class_name not in ("Find", "ContextRecord", "Operation"): - return - if class_name == "Find": - for cr in document.context_records.filter( - base_finds__find__pk=related_item.pk - ).all(): - document.context_records.remove(cr) - for ope in document.operations.filter( - context_record__base_finds__find__pk=related_item.pk - ).all(): - document.operations.remove(ope) - return - if class_name == "ContextRecord": - for ope in document.operations.filter(context_record__pk=related_item.pk).all(): - document.operations.remove(ope) - if document.finds.filter(base_finds__context_record=related_item.pk).count(): - document.context_records.remove(related_item) - return - if class_name == "Operation": - if document.context_records.filter(operation=related_item.pk).count(): - document.operations.remove(related_item) - return - if document.finds.filter( - base_finds__context_record__operation=related_item.pk - ).count(): - document.operations.remove(related_item) - return + def get_town_polygons(self): + if self.precise_town: + return self.precise_town.limit, self._meta.verbose_name + for sub_address in self.SUB_ADDRESSES: + sub_item = getattr(self, sub_address) + if sub_item and sub_item.precise_town: + return sub_item.precise_town.limit, sub_item._meta.verbose_name + def get_attribute(self, attr): + if self.town or self.precise_town: + return getattr(self, attr) + for sub_address in self.SUB_ADDRESSES: + sub_item = getattr(self, sub_address) + if not sub_item: + continue + if sub_item.town or sub_item.precise_town: + return getattr(sub_item, attr) + return getattr(self, attr) -def document_attached_changed(sender, **kwargs): - # associate a default main image - instance = kwargs.get("instance", None) - model = kwargs.get("model", None) - pk_set = kwargs.get("pk_set", None) - if not instance or not model: - return + def get_address(self): + return self.get_attribute("address") - if hasattr(instance, "documents"): - items = [instance] - else: - if not pk_set: - return - try: - items = [model.objects.get(pk=pk) for pk in pk_set] - except model.DoesNotExist: - return + def get_address_complement(self): + return self.get_attribute("address_complement") - for item in items: - clean_duplicate_association(instance, item, kwargs.get("action", None)) - for doc in item.documents.all(): - doc.regenerate_all_ids() - q = item.documents.filter(image__isnull=False).exclude(image="") - if item.main_image: - if q.filter(pk=item.main_image.pk).count(): - return - # the association has disappear not the main image anymore - item.main_image = None - item.skip_history_when_saving = True - item.save() - if not q.count(): - return - # by default get the lowest pk - item.main_image = q.order_by("pk").all()[0] - item.skip_history_when_saving = True - item.save() + def get_postal_code(self): + return self.get_attribute("postal_code") + def get_town(self): + return self.get_attribute("town") -class QuickAction: - """ - Quick action available from tables - """ + def get_precise_town(self): + return self.get_attribute("precise_town") - def __init__( - self, - url, - icon_class="", - text="", - target=None, - rights=None, - module=None, - is_popup=True, - ): - self.url = url - self.icon_class = icon_class - self.text = text - self.rights = rights - self.target = target - self.module = module - self.is_popup = is_popup - assert self.target in ("one", "many", None) + def get_country(self): + return self.get_attribute("country") + + def simple_lbl(self): + return str(self) + + def full_address(self): + lbl = self.simple_lbl() + if lbl: + lbl += "\n" + lbl += self.address_lbl() + return lbl + + def address_lbl(self, list=False): + lbls = [] + prefix = "" + if self.alt_address_is_prefered: + prefix = "alt_" + if getattr(self, prefix + "address"): + lbls.append(( + getattr(self, prefix + "address"), + _("Address") + )) + if getattr(self, prefix + "address_complement"): + lbls.append(( + getattr(self, prefix + "address_complement"), + _("Address complement") + )) + postal_code = getattr(self, prefix + "postal_code") + town = getattr(self, prefix + "town") + if postal_code or town: + lbls.append(( + " ".join([postal_code, town]), + _("Postal code - Town") + )) + if self.phone: + lbls.append(( + self.phone, + _("Phone") + )) + if self.mobile_phone: + lbls.append(( + self.mobile_phone, + _("Mobile") + )) + if self.email: + lbls.append(( + self.email, + _("Email") + )) + if list: + return lbls + return "\n".join([ + value for value, lbl in lbls + ]) + + def address_lbl_list(self): + return self.address_lbl(list=True) - def is_available(self, user, session=None, obj=None): - if self.module and not getattr(get_current_profile(), self.module): - return False - if not self.rights: # no restriction - return True - if not user or not hasattr(user, "ishtaruser") or not user.ishtaruser: - return False - user = user.ishtaruser - for right in self.rights: - if user.has_perm(right, session=session, obj=obj): - return True - return False +class Merge(models.Model): + merge_key = models.TextField(_("Merge key"), blank=True, null=True) + merge_candidate = models.ManyToManyField("self", blank=True) + merge_exclusion = models.ManyToManyField("self", blank=True) + archived = models.NullBooleanField(default=False, blank=True, null=True) + # 1 for one word similarity, 2 for two word similarity, etc. + MERGE_CLEMENCY = None + EMPTY_MERGE_KEY = "--" + MERGE_ATTRIBUTE = "name" - @property - def rendered_icon(self): - if not self.icon_class: - return "" - return "".format(self.icon_class) + class Meta: + abstract = True - @property - def base_url(self): - if self.target is None: - url = reverse(self.url) + def generate_merge_key(self): + if self.archived: + return + merge_attr = getattr(self, self.MERGE_ATTRIBUTE) + self.merge_key = slugify(merge_attr if merge_attr else "") + if not self.merge_key: + self.merge_key = self.EMPTY_MERGE_KEY + self.merge_key = self.merge_key + + def generate_merge_candidate(self): + if self.archived: + return + if not self.merge_key: + self.generate_merge_key() + self.save(merge_key_generated=True) + if not self.pk or self.merge_key == self.EMPTY_MERGE_KEY: + return + q = ( + self.__class__.objects.exclude(pk=self.pk) + .exclude(merge_exclusion=self) + .exclude(merge_candidate=self) + .exclude(archived=True) + ) + if not self.MERGE_CLEMENCY: + q = q.filter(merge_key=self.merge_key) else: - # put arbitrary pk for the target - url = reverse(self.url, args=[0]) - url = url[:-2] # all quick action url have to finish with the - # pk of the selected item and a "/" - return url + subkeys_front = "-".join(self.merge_key.split("-")[: self.MERGE_CLEMENCY]) + subkeys_back = "-".join(self.merge_key.split("-")[-self.MERGE_CLEMENCY :]) + q = q.filter( + Q(merge_key__istartswith=subkeys_front) + | Q(merge_key__iendswith=subkeys_back) + ) + for item in q.all(): + self.merge_candidate.add(item) + def save(self, *args, **kwargs): + # prevent circular save + merge_key_generated = False + if "merge_key_generated" in kwargs: + merge_key_generated = kwargs.pop("merge_key_generated") + self.generate_merge_key() + item = super(Merge, self).save(*args, **kwargs) + if not merge_key_generated: + self.merge_candidate.clear() + self.generate_merge_candidate() + return item -class DynamicRequest: - def __init__( - self, - label, - app_name, - model_name, - form_key, - search_key, - type_query, - search_query, - ): - self.label = label - self.form_key = form_key - self.search_key = search_key - self.app_name = app_name - self.model_name = model_name - self.type_query = type_query - self.search_query = search_query + def archive(self): + self.archived = True + self.save() + self.merge_candidate.clear() + self.merge_exclusion.clear() - def get_all_types(self): - model = apps.get_app_config(self.app_name).get_model(self.model_name) - return model.objects.filter(available=True) + def merge(self, item, keep_old=False, exclude_fields=None): + merge_model_objects( + self, item, keep_old=keep_old, exclude_fields=exclude_fields + ) + self.generate_merge_candidate() - def get_form_fields(self): - fields = {} - for item in self.get_all_types().all(): - fields[self.form_key + "-" + item.txt_idx] = forms.CharField( - label=str(self.label) + " " + str(item), required=False - ) - return fields - def get_extra_query(self, slug): - return {self.type_query: slug} +def __get_stats_cache_values(model_name, model_pk): + StatsCache = apps.get_model("ishtar_common", "StatsCache") + q = StatsCache.objects.filter(model=model_name, model_pk=model_pk) + nb = q.count() + if nb >= 1: + sc = q.all()[0] + for extra in q.order_by("-id").all()[1:]: + extra.delete() + else: + sc = StatsCache.objects.create(model=model_name, model_pk=model_pk) + values = sc.values + if not values: + values = {} + return sc, values - def get_alt_names(self): - alt_names = {} - for item in self.get_all_types().all(): - alt_names[self.form_key + "-" + item.txt_idx] = SearchAltName( - self.search_key + "-" + item.txt_idx, - self.search_query, - self.get_extra_query(item.txt_idx), - distinct_query=True, - ) - return alt_names +@task() +def _update_stats(app, model, model_pk, funcname): + model_name = app + "." + model + model = apps.get_model(app, model) + try: + item = model.objects.get(pk=model_pk) + except model.DoesNotExist: + return + value = getattr(item, funcname)() + sc, current_values = __get_stats_cache_values(model_name, model_pk) + current_values[funcname] = value + sc.values = current_values + sc.update_requested = None + sc.updated = datetime.datetime.now() + sc.save() -class SpatialReferenceSystem(GeneralType): - order = models.IntegerField(_("Order"), default=10) - auth_name = models.CharField(_("Authority name"), default="EPSG", max_length=256) - srid = models.IntegerField(_("Authority SRID")) - class Meta: - verbose_name = _("Spatial reference system") - verbose_name_plural = _("Spatial reference systems") - ordering = ( - "order", - "label", - ) +def update_stats(statscache, item, funcname): + if not settings.USE_BACKGROUND_TASK: + current_values = statscache.values + if not current_values: + current_values = {} + value = getattr(item, funcname)() + current_values[funcname] = value + statscache.values = current_values + statscache.updated = datetime.datetime.now() + statscache.save() + return current_values - @classmethod - def get_documentation_string(cls): - """ - Used for automatic documentation generation - """ - doc = super(SpatialReferenceSystem, cls).get_documentation_string() - doc += ", **srid** {}, **auth_name** {}".format( - _("Authority SRID"), _("Authority name") + now = datetime.datetime.now() + app_name = item._meta.app_label + model_name = item._meta.model_name + statscache.update_requested = now.isoformat() + statscache.save() + _update_stats.delay(app_name, model_name, item.pk, funcname) + return statscache.values + + +class DashboardFormItem: + """ + Provide methods to manage statistics + """ + + def last_stats_update(self): + model_name = self._meta.app_label + "." + self._meta.model_name + StatsCache = apps.get_model("ishtar_common", "StatsCache") + q = StatsCache.objects.filter(model=model_name, model_pk=self.pk).order_by( + "-updated" ) - return doc + if not q.count(): + return + return q.all()[0].updated + def _get_or_set_stats(self, funcname, update=False, expected_type=None): + model_name = self._meta.app_label + "." + self._meta.model_name + StatsCache = apps.get_model("ishtar_common", "StatsCache") + sc, __ = StatsCache.objects.get_or_create(model=model_name, model_pk=self.pk) + if not update: + values = sc.values + if funcname not in values: + if expected_type is not None: + return expected_type() + return 0 + else: + values = update_stats(sc, self, funcname) + if funcname in values: + values = values[funcname] + else: + values = 0 + if expected_type is not None and not isinstance(values, expected_type): + return expected_type() + return values -post_save.connect(post_save_cache, sender=SpatialReferenceSystem) -post_delete.connect(post_save_cache, sender=SpatialReferenceSystem) + @classmethod + def get_periods(cls, slice="month", fltr={}, date_source="creation"): + date_var = date_source + "_date" + q = cls.objects.filter(**{date_var + "__isnull": False}) + if fltr: + q = q.filter(**fltr) + if slice == "year": + return [ + res[date_var].year + for res in list(q.values(date_var).annotate(Count("id")).order_by()) + ] + elif slice == "month": + return [ + (res[date_var].year, res[date_var].month) + for res in list(q.values(date_var).annotate(Count("id")).order_by()) + ] + return [] + @classmethod + def get_by_year(cls, year, fltr={}, date_source="creation"): + date_var = date_source + "_date" + q = cls.objects.filter(**{date_var + "__isnull": False}) + if fltr: + q = q.filter(**fltr) + return q.filter(**{date_var + "__year": year}).order_by("pk").distinct("pk") -class GeoOriginType(HierarchicalType): - """ - ex: topographical surveys, georeferencing, ... - """ + @classmethod + def get_by_month(cls, year, month, fltr={}, date_source="creation"): + date_var = date_source + "_date" + q = cls.objects.filter(**{date_var + "__isnull": False}) + if fltr: + q = q.filter(**fltr) + q = q.filter(**{date_var + "__year": year, date_var + "__month": month}) + return q.order_by("pk").distinct("pk") - order = models.IntegerField(_("Order"), default=10) + @classmethod + def get_total_number(cls, fltr=None): + q = cls.objects + if fltr: + q = q.filter(**fltr) + return q.order_by("pk").distinct("pk").count() - class Meta: - verbose_name = _("Geographic - Origin type") - verbose_name_plural = _("Geographic - Origin types") - ordering = ( - "order", - "label", - ) +class DocumentItem: + ALT_NAMES = { + "documents__image__isnull": SearchAltName( + pgettext_lazy("key for text search", "has-image"), + "documents__image__isnull", + ), + "documents__associated_url__isnull": SearchAltName( + pgettext_lazy("key for text search", "has-url"), + "documents__associated_url__isnull", + ), + "documents__associated_file__isnull": SearchAltName( + pgettext_lazy("key for text search", "has-attached-file"), + "documents__associated_file__isnull", + ), + } -class GeoDataType(HierarchicalType): - """ - ex: outline, z-sup, ... - """ + def documents_list(self) -> list: + Document = apps.get_model("ishtar_common", "Document") + return self.get_associated_main_item_list("documents", Document) - order = models.IntegerField(_("Order"), default=10) + def public_representation(self): + images = [] + if getattr(self, "main_image", None): + images.append(self.main_image.public_representation()) + images += [ + image.public_representation() + for image in self.images_without_main_image.all() + ] + return {"images": images} - class Meta: - verbose_name = _("Geographic - Data type") - verbose_name_plural = _("Geographic - Data types") - ordering = ( - "order", - "label", + @property + def images(self): + if not hasattr(self, "documents"): + Document = apps.get_model("ishtar_common", "Document") + return Document.objects.none() + return ( + self.documents.filter(image__isnull=False).exclude(image="").order_by("pk") ) + @property + def images_number(self): + return self.images.count() -class GeoProviderType(HierarchicalType): - """ - ex: GeoNames, IGN, ... - """ - - order = models.IntegerField(_("Order"), default=10) - - class Meta: - verbose_name = _("Geographic - Provider type") - verbose_name_plural = _("Geographic - Provider types") - ordering = ( - "order", - "label", + @property + def images_without_main_image(self): + if not hasattr(self, "main_image") or not hasattr(self, "documents"): + return self.images + if not self.main_image: + return ( + self.documents.filter(image__isnull=False) + .exclude(image="") + .order_by("pk") + ) + return ( + self.documents.filter(image__isnull=False) + .exclude(image="") + .exclude(pk=self.main_image.pk) + .order_by("pk") ) + @property + def pdf_attached(self): + for document in self.documents.filter( + Q(associated_file__isnull=False) | Q(source__associated_file__isnull=False) + ).all(): + return document.pdf_attached -class GeoVectorData(models.Model): - name = models.CharField(_("Name"), default=_("Default"), max_length=200) - source_content_type = models.ForeignKey( - ContentType, related_name="content_type_geovectordata", on_delete=models.CASCADE - ) - source_id = models.PositiveIntegerField() - source = GenericForeignKey("source_content_type", "source_id") - origin = models.ForeignKey( - GeoOriginType, - blank=True, - null=True, - on_delete=models.PROTECT, - verbose_name=_("Origin"), - ) - data_type = models.ForeignKey( - GeoDataType, - blank=True, - null=True, - on_delete=models.PROTECT, - verbose_name=_("Data type"), - ) - provider = models.ForeignKey( - GeoProviderType, - blank=True, - null=True, - on_delete=models.PROTECT, - verbose_name=_("Provider"), - ) - comment = models.TextField(_("Comment"), default="", blank=True) - x = models.FloatField(_("X"), blank=True, null=True, help_text=_("User input")) - y = models.FloatField(_("Y"), blank=True, null=True, help_text=_("User input")) - z = models.FloatField(_("Z"), blank=True, null=True, help_text=_("User input")) - # x == cached_x if user input else get it from other sources - # cached is converted to the display SRID - cached_x = models.FloatField(_("X (cached)"), blank=True, null=True) - cached_y = models.FloatField(_("Y (cached)"), blank=True, null=True) - cached_z = models.FloatField(_("Z (cached)"), blank=True, null=True) - estimated_error_x = models.FloatField( - _("Estimated error for X"), blank=True, null=True - ) - estimated_error_y = models.FloatField( - _("Estimated error for Y"), blank=True, null=True - ) - estimated_error_z = models.FloatField( - _("Estimated error for Z"), blank=True, null=True - ) - spatial_reference_system = models.ForeignKey( - SpatialReferenceSystem, - verbose_name=_("Spatial Reference System"), - blank=True, - null=True, - on_delete=models.PROTECT, - ) - point_2d = models.PointField(_("Point (2D)"), blank=True, null=True) - point_3d = models.PointField(_("Point (3D)"), blank=True, null=True, dim=3) - multi_points = models.MultiPointField(_("Multi points"), blank=True, null=True) - multi_line = models.MultiLineStringField(_("Multi line"), blank=True, null=True) - multi_polygon = models.MultiPolygonField(_("Multi polygon"), blank=True, null=True) - need_update = models.BooleanField(_("Need update"), default=False) + def get_extra_actions(self, request): + """ + For sheet template: return "Add document / image" action + """ + # url, base_text, icon, extra_text, extra css class, is a quick action + try: + actions = super(DocumentItem, self).get_extra_actions(request) + except AttributeError: + actions = [] - class Meta: - verbose_name = _("Geographic - Vector data") - verbose_name_plural = _("Geographic - Vector data") + if not hasattr(self, "SLUG"): + return actions - def display_coordinates(self, rounded=5, dim=2, cache=True): - srid = None - profile = get_current_profile() - if profile.display_srs and profile.display_srs.srid: - srid = profile.display_srs.srid - return self.get_coordinates(rounded=rounded, srid=srid, dim=dim, cache=cache) + can_add_doc = self.can_do(request, "add_document") + if can_add_doc and ( + not hasattr(self, "is_locked") or not self.is_locked(request.user) + ): + actions += [ + ( + reverse("create-document") + "?{}={}".format(self.SLUG, self.pk), + _("Add document/image"), + "fa fa-plus", + _("doc./image"), + "", + False, + ) + ] + return actions - def get_coordinates(self, rounded=5, srid: int = None, dim=2, cache=False): - if dim not in (2, 3): - raise ValueError(_("Only 2 or 3 dimension")) - if cache: - coordinates = [self.cached_x, self.cached_y] - if dim == 3: - coordinates.append(self.cached_z) - else: - if self.x and self.y: # user input - if not srid or srid == self.spatial_reference_system.srid: - coordinates = [self.x, self.y] - if dim == 3: - coordinates.append(self.z) - else: - args = { - "x": self.x, - "y": self.y, - "srid": self.spatial_reference_system.srid, - } - if dim == 3: - args["z"] = self.z - point = Point(**args).transform(srid, clone=True) - coordinates = [point.x, point.y] - if dim == 3: - coordinates.append(point.z) - elif self.point_2d and dim == 2: - point = self.point_2d.transform(srid, clone=True) - coordinates = [point.x, point.y] - elif self.point_3d and dim == 3: - point = self.point_3d.transform(srid, clone=True) - coordinates = [point.x, point.y, point.z] - else: - return - if not rounded: - return coordinates - return [round(coord, rounded) for coord in coordinates] - def get_coordinates_from_polygon(self, rounded=5, srid: int = None): - if self.multi_polygon: - return self.convert_coordinates( - self.multi_polygon.centroid, rounded=rounded, srid=srid - ) +def clean_duplicate_association(document, related_item, action): + profile = get_current_profile() + if not profile.clean_redundant_document_association or action != "post_add": + return + class_name = related_item.__class__.__name__ + if class_name not in ("Find", "ContextRecord", "Operation"): + return + if class_name == "Find": + for cr in document.context_records.filter( + base_finds__find__pk=related_item.pk + ).all(): + document.context_records.remove(cr) + for ope in document.operations.filter( + context_record__base_finds__find__pk=related_item.pk + ).all(): + document.operations.remove(ope) + return + if class_name == "ContextRecord": + for ope in document.operations.filter(context_record__pk=related_item.pk).all(): + document.operations.remove(ope) + if document.finds.filter(base_finds__context_record=related_item.pk).count(): + document.context_records.remove(related_item) + return + if class_name == "Operation": + if document.context_records.filter(operation=related_item.pk).count(): + document.operations.remove(related_item) + return + if document.finds.filter( + base_finds__context_record__operation=related_item.pk + ).count(): + document.operations.remove(related_item) + return - def get_x(self, srid: int = None) -> float: - coord = self.get_coordinates(srid) - if coord: - return coord[0] - def get_y(self, srid: int = None) -> float: - coord = self.get_coordinates(srid) - if coord: - return coord[1] +def document_attached_changed(sender, **kwargs): + # associate a default main image + instance = kwargs.get("instance", None) + model = kwargs.get("model", None) + pk_set = kwargs.get("pk_set", None) + if not instance or not model: + return - def get_z(self, srid: int = None) -> float: - coord = self.get_coordinates(srid, dim=3) - if coord: - return coord[2] + if hasattr(instance, "documents"): + items = [instance] + else: + if not pk_set: + return + try: + items = [model.objects.get(pk=pk) for pk in pk_set] + except model.DoesNotExist: + return - @property - def display_spatial_reference_system(self): - profile = get_current_profile() - if not profile.display_srs or not profile.display_srs.srid: - return self.spatial_reference_system - return profile.display_srs + for item in items: + clean_duplicate_association(instance, item, kwargs.get("action", None)) + for doc in item.documents.all(): + doc.regenerate_all_ids() + q = item.documents.filter(image__isnull=False).exclude(image="") + if item.main_image: + if q.filter(pk=item.main_image.pk).count(): + return + # the association has disappear not the main image anymore + item.main_image = None + item.skip_history_when_saving = True + item.save() + if not q.count(): + return + # by default get the lowest pk + item.main_image = q.order_by("pk").all()[0] + item.skip_history_when_saving = True + item.save() - def get_geo_items(self, get_polygons, rounded=5): - label = self.label if hasattr(self, "label") else self.short_label - dct = {"type": "Feature", "geometry": {}, "properties": {"label": label}} - if get_polygons: - list_coords = [] - if self.multi_polygon: - for polygon in self.multi_polygon: - list_coords.append([]) - for linear_ring in range(len(polygon)): - list_coords[-1].append([]) - for coords in polygon[linear_ring].coords: - point_2d = Point( - coords[0], coords[1], srid=self.multi_polygon.srid - ) - list_coords[-1][linear_ring].append( - self.convert_coordinates(point_2d, rounded) - ) - dct["geometry"]["type"] = "MultiPolygon" - dct["geometry"]["coordinates"] = list_coords - else: - dct["geometry"]["type"] = "Point" - coords = self.display_coordinates() - if coords: - dct["geometry"]["coordinates"] = coords - elif self.multi_polygon: - dct["geometry"]["coordinates"] = self.convert_coordinates( - self.multi_polygon.centroid, rounded - ) - else: - return {} - return dct - def convert_coordinates(self, point_2d, rounded=5, srid=None): - if not srid: - profile = get_current_profile() - if profile.display_srs and profile.display_srs.srid: - srid = profile.display_srs.srid - if not srid: - x, y = point_2d.x, point_2d.y - else: - point = point_2d.transform(srid, clone=True) - x, y = point.x, point.y - if rounded: - return [round(x, rounded), round(y, rounded)] - return [x, y] +class QuickAction: + """ + Quick action available from tables + """ - def most_precise_geo(self): - if self.multi_polygon: - return "multi_polygon" - if self.point_2d: - return "point" + def __init__( + self, + url, + icon_class="", + text="", + target=None, + rights=None, + module=None, + is_popup=True, + ): + self.url = url + self.icon_class = icon_class + self.text = text + self.rights = rights + self.target = target + self.module = module + self.is_popup = is_popup + assert self.target in ("one", "many", None) - def _geojson_serialize(self, geom_attr): - if not hasattr(self, geom_attr): - return "" - geojson = serialize( - "geojson", - self.__class__.objects.filter(pk=self.pk), - geometry_field=geom_attr, - fields=("name",), - ) - geojson_dct = json.loads(geojson) - profile = get_current_profile() - precision = profile.point_precision + def is_available(self, user, session=None, obj=None): + if self.module and not getattr(get_current_profile(), self.module): + return False + if not self.rights: # no restriction + return True + if not user or not hasattr(user, "ishtaruser") or not user.ishtaruser: + return False + user = user.ishtaruser - features = geojson_dct.pop("features") - for idx in range(len(features)): - feature = features[idx] - lbl = feature["properties"].pop("name") - feature["properties"]["name"] = lbl - feature["properties"]["id"] = self.pk - if precision is not None: - geom_type = feature["geometry"].get("type", None) - if geom_type == "Point": - feature["geometry"]["coordinates"] = [ - round(coord, precision) - for coord in feature["geometry"]["coordinates"] - ] - geojson_dct["features"] = features - geojson_dct["link_template"] = simple_link_to_window(self).replace( - "999999", "" - ) - geojson = json.dumps(geojson_dct) - return geojson + for right in self.rights: + if user.has_perm(right, session=session, obj=obj): + return True + return False @property - def point_2d_geojson(self): - return self._geojson_serialize("point_2d") + def rendered_icon(self): + if not self.icon_class: + return "" + return "".format(self.icon_class) @property - def multi_polygon_geojson(self): - return self._geojson_serialize("multi_polygon") + def base_url(self): + if self.target is None: + url = reverse(self.url) + else: + # put arbitrary pk for the target + url = reverse(self.url, args=[0]) + url = url[:-2] # all quick action url have to finish with the + # pk of the selected item and a "/" + return url +class DynamicRequest: + def __init__( + self, + label, + app_name, + model_name, + form_key, + search_key, + type_query, + search_query, + ): + self.label = label + self.form_key = form_key + self.search_key = search_key + self.app_name = app_name + self.model_name = model_name + self.type_query = type_query + self.search_query = search_query + + def get_all_types(self): + model = apps.get_app_config(self.app_name).get_model(self.model_name) + return model.objects.filter(available=True) + def get_form_fields(self): + fields = {} + for item in self.get_all_types().all(): + fields[self.form_key + "-" + item.txt_idx] = forms.CharField( + label=str(self.label) + " " + str(item), required=False + ) + return fields -post_save.connect(post_save_geodata, sender=GeoVectorData) + def get_extra_query(self, slug): + return {self.type_query: slug} + def get_alt_names(self): + alt_names = {} + for item in self.get_all_types().all(): + alt_names[self.form_key + "-" + item.txt_idx] = SearchAltName( + self.search_key + "-" + item.txt_idx, + self.search_query, + self.get_extra_query(item.txt_idx), + distinct_query=True, + ) + return alt_names -class GeoItem(models.Model): - main_geodata = models.ForeignKey( - GeoVectorData, - on_delete=models.SET_NULL, - blank=True, - null=True, - related_name="main_related_items_%(app_label)s_%(class)s", - ) - geodata = models.ManyToManyField( - GeoVectorData, blank=True, related_name="related_items_%(app_label)s_%(class)s" - ) +class GeoItem(GeographicItem): GEO_SOURCE = (("T", _("Town")), ("P", _("Precise")), ("M", _("Polygon"))) # gis -- cgit v1.2.3