diff options
author | Étienne Loks <etienne.loks@iggdrasil.net> | 2020-03-24 19:04:54 +0100 |
---|---|---|
committer | Étienne Loks <etienne.loks@iggdrasil.net> | 2021-02-28 12:15:20 +0100 |
commit | 2d7a40f22bc9da10515330becff08c589012c0ba (patch) | |
tree | 75e1154c53fa775498c3fb91d70c6a7f7a6d59f1 | |
parent | d357a8d182d9eb7b3df67d9eb9e127d83bb952fe (diff) | |
download | Ishtar-2d7a40f22bc9da10515330becff08c589012c0ba.tar.bz2 Ishtar-2d7a40f22bc9da10515330becff08c589012c0ba.zip |
Container: add parents - localisation refactoring
-rw-r--r-- | archaeological_warehouse/forms.py | 11 | ||||
-rw-r--r-- | archaeological_warehouse/management/__init__.py | 0 | ||||
-rw-r--r-- | archaeological_warehouse/management/commands/__init__.py | 0 | ||||
-rw-r--r-- | archaeological_warehouse/management/commands/migrate_to_new_container_management.py | 106 | ||||
-rw-r--r-- | archaeological_warehouse/migrations/0102_auto_20200324_1142.py | 100 | ||||
-rw-r--r-- | archaeological_warehouse/models.py | 21 | ||||
-rw-r--r-- | ishtar_common/models.py | 2 | ||||
-rw-r--r-- | requirements.txt | 2 |
8 files changed, 233 insertions, 9 deletions
diff --git a/archaeological_warehouse/forms.py b/archaeological_warehouse/forms.py index a7b6c575e..e03918965 100644 --- a/archaeological_warehouse/forms.py +++ b/archaeological_warehouse/forms.py @@ -259,14 +259,23 @@ class ContainerForm(CustomForm, ManageOldType, forms.Form): form_admin_name = _(u"Container - 010 - General") form_slug = "container-010-general" file_upload = True - extra_form_modals = ["warehouse", "organization", "person"] + extra_form_modals = ["warehouse", "organization", "person", "container"] associated_models = {'container_type': models.ContainerType, 'location': models.Warehouse, + 'parent': models.Container, 'responsible': models.Warehouse} reference = forms.CharField(label=_(u"Ref."), max_length=200) old_reference = forms.CharField(label=_(u"Old reference"), required=False, max_length=200) container_type = forms.ChoiceField(label=_(u"Container type"), choices=[]) + parent = forms.IntegerField( + label=_("Parent container"), + widget=widgets.JQueryAutoComplete( + reverse_lazy('autocomplete-container'), + associated_model=models.Container, new=True), + validators=[valid_id(models.Container)], + required=False + ) responsible = forms.IntegerField( label=_(u"Responsible warehouse"), widget=widgets.JQueryAutoComplete( diff --git a/archaeological_warehouse/management/__init__.py b/archaeological_warehouse/management/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/archaeological_warehouse/management/__init__.py diff --git a/archaeological_warehouse/management/commands/__init__.py b/archaeological_warehouse/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/archaeological_warehouse/management/commands/__init__.py diff --git a/archaeological_warehouse/management/commands/migrate_to_new_container_management.py b/archaeological_warehouse/management/commands/migrate_to_new_container_management.py new file mode 100644 index 000000000..b5885cbf0 --- /dev/null +++ b/archaeological_warehouse/management/commands/migrate_to_new_container_management.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (C) 2020 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# See the file COPYING for details. + +import csv +import sys + +from django.core.management.base import BaseCommand +from django.template.defaultfilters import slugify +from archaeological_warehouse import models + + +class Command(BaseCommand): + help = 'Migrate to new container management (v3.0.6)' + + def handle(self, *args, **options): + to_update = models.Container.objects.filter( + division__pk__isnull=False) + container_types = {} + created_nb = 0 + for div_type in models.WarehouseDivision.objects.all(): + container_type, c = models.ContainerType.objects.get_or_create( + txt_idx=slugify(div_type.label), + defaults={"label": div_type.label}) + if c: + created_nb += 1 + sys.stdout.write("-> {} created\n".format( + div_type.label)) + container_types[div_type.pk] = container_type + if created_nb: + sys.stdout.write("* {} container types created\n".format( + created_nb)) + to_be_done = to_update.count() + created_nb = 0 + potential_duplicate = {} + data = [("id", "warehouse", "reference", + "old cached division", "new cached division")] + for idx, container in enumerate(models.Container.objects.filter( + division__pk__isnull=False).all()): + sys.stdout.write("* Updating: {}/{}\r".format(idx + 1, to_be_done)) + sys.stdout.flush() + if container.responsible_id not in potential_duplicate: + potential_duplicate[container.responsible_id] = {} + parent = None + cached_division = container.cached_division + for division in container.division.order_by( + "division__order").all(): + ref = division.reference.strip() + if not ref or ref == "-": + continue + new_container, created = models.Container.objects.get_or_create( + reference=division.reference.strip(), + parent=parent, + container_type=container_types[ + division.division.division_id], + location=container.responsible, + responsible=container.responsible) + if created: + created_nb += 1 + ref = "{} || {}".format(str(new_container.container_type), + slugify(division.reference.strip())) + if ref not in potential_duplicate[container.responsible_id]: + potential_duplicate[container.responsible_id][ref] = [] + if division.reference.strip() not in \ + potential_duplicate[container.responsible_id][ref]: + potential_duplicate[container.responsible_id][ + ref].append(division.reference.strip()) + parent = new_container + if parent: + container.parent = parent + container.save() + data.append((container.id, str(container.responsible), + container.reference, cached_division, + container._generate_cached_division())) + sys.stdout.write("\n* Potential duplicate:") + for warehouse_id in potential_duplicate.keys(): + warehouse = models.Warehouse.objects.get(pk=warehouse_id) + for ref in potential_duplicate[warehouse_id]: + items = potential_duplicate[warehouse_id][ref] + if len(items) > 1: + sys.stdout.write( + "\n-> {}: {}".format(warehouse, " ; ".join(items))) + print("") + sys.stdout.write("* {} container created\n".format(created_nb)) + if not data: + return + with open("new_containers.csv", 'w+') as f: + w = csv.writer(f) + w.writerows(data) + sys.stdout.write("-> check new containers in \"new_containers.csv\"\n") + diff --git a/archaeological_warehouse/migrations/0102_auto_20200324_1142.py b/archaeological_warehouse/migrations/0102_auto_20200324_1142.py new file mode 100644 index 000000000..4006a9701 --- /dev/null +++ b/archaeological_warehouse/migrations/0102_auto_20200324_1142.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.27 on 2020-03-24 11:42 +from __future__ import unicode_literals + +from django.conf import settings +import django.contrib.gis.db.models.fields +import django.contrib.postgres.fields.jsonb +import django.contrib.postgres.search +from django.db import migrations, models +import django.db.models.deletion +import simple_history.models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('ishtar_common', '0202_auto_20200129_1941'), + ('archaeological_warehouse', '0101_squashed'), + ] + + operations = [ + migrations.CreateModel( + name='HistoricalWarehouse', + fields=[ + ('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('data', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default={})), + ('search_vector', django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector')), + ('qrcode', models.TextField(blank=True, max_length=255, null=True)), + ('x', models.FloatField(blank=True, null=True, verbose_name='X')), + ('y', models.FloatField(blank=True, null=True, verbose_name='Y')), + ('z', models.FloatField(blank=True, null=True, verbose_name='Z')), + ('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', django.contrib.gis.db.models.fields.PointField(blank=True, dim=3, null=True, srid=4326, verbose_name='Point')), + ('point_2d', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Point (2D)')), + ('point_source', models.CharField(blank=True, choices=[('T', 'Town'), ('P', 'Precise'), ('M', 'Polygon')], max_length=1, null=True, verbose_name='Point source')), + ('point_source_item', models.CharField(blank=True, max_length=100, null=True, verbose_name='Point source item')), + ('multi_polygon', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=4326, verbose_name='Multi polygon')), + ('multi_polygon_source', models.CharField(blank=True, choices=[('T', 'Town'), ('P', 'Precise'), ('M', 'Polygon')], max_length=1, null=True, verbose_name='Multi-polygon source')), + ('multi_polygon_source_item', models.CharField(blank=True, max_length=100, null=True, verbose_name='Multi polygon source item')), + ('last_modified', models.DateTimeField(blank=True, editable=False)), + ('history_m2m', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default={})), + ('need_update', models.BooleanField(default=False, verbose_name='Need update')), + ('locked', models.BooleanField(default=False, verbose_name='Item locked for edition')), + ('address', models.TextField(blank=True, null=True, verbose_name='Address')), + ('address_complement', models.TextField(blank=True, null=True, verbose_name='Address complement')), + ('postal_code', models.CharField(blank=True, max_length=10, null=True, verbose_name='Postal code')), + ('town', models.CharField(blank=True, max_length=150, null=True, verbose_name='Town (freeform)')), + ('country', models.CharField(blank=True, max_length=30, null=True, verbose_name='Country')), + ('alt_address', models.TextField(blank=True, null=True, verbose_name='Other address: address')), + ('alt_address_complement', models.TextField(blank=True, null=True, verbose_name='Other address: address complement')), + ('alt_postal_code', models.CharField(blank=True, max_length=10, null=True, verbose_name='Other address: postal code')), + ('alt_town', models.CharField(blank=True, max_length=70, null=True, verbose_name='Other address: town')), + ('alt_country', models.CharField(blank=True, max_length=30, null=True, verbose_name='Other address: country')), + ('phone', models.CharField(blank=True, max_length=18, null=True, verbose_name='Phone')), + ('phone_desc', models.CharField(blank=True, max_length=300, null=True, verbose_name='Phone description')), + ('phone2', models.CharField(blank=True, max_length=18, null=True, verbose_name='Phone description 2')), + ('phone_desc2', models.CharField(blank=True, max_length=300, null=True, verbose_name='Phone description 2')), + ('phone3', models.CharField(blank=True, max_length=18, null=True, verbose_name='Phone 3')), + ('phone_desc3', models.CharField(blank=True, max_length=300, null=True, verbose_name='Phone description 3')), + ('raw_phone', models.TextField(blank=True, null=True, verbose_name='Raw phone')), + ('mobile_phone', models.CharField(blank=True, max_length=18, null=True, verbose_name='Mobile phone')), + ('email', models.EmailField(blank=True, max_length=300, null=True, verbose_name='Email')), + ('alt_address_is_prefered', models.BooleanField(default=False, verbose_name='Alternative address is prefered')), + ('uuid', models.UUIDField(default=uuid.uuid4)), + ('name', models.CharField(max_length=200, verbose_name='Name')), + ('comment', models.TextField(blank=True, null=True, verbose_name='Comment')), + ('external_id', models.TextField(blank=True, null=True, verbose_name='External ID')), + ('auto_external_id', models.BooleanField(default=False, verbose_name='External ID is set automatically')), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_creator', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Creator')), + ('history_modifier', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Last editor')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('lock_user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Locked by')), + ('main_image', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='ishtar_common.Document', verbose_name='Main image')), + ('organization', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='ishtar_common.Organization', verbose_name='Organization')), + ('person_in_charge', 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='Person in charge')), + ('precise_town', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='ishtar_common.Town', verbose_name='Town (precise)')), + ('spatial_reference_system', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='ishtar_common.SpatialReferenceSystem', verbose_name='Spatial Reference System')), + ('warehouse_type', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='archaeological_warehouse.WarehouseType', verbose_name='Warehouse type')), + ], + options={ + 'verbose_name': 'historical Warehouse', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.AddField( + model_name='container', + name='parent', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='archaeological_warehouse.Container', verbose_name='Parent container'), + ), + ] diff --git a/archaeological_warehouse/models.py b/archaeological_warehouse/models.py index 7b98a385a..839ca1a6e 100644 --- a/archaeological_warehouse/models.py +++ b/archaeological_warehouse/models.py @@ -653,6 +653,9 @@ class Container(DocumentItem, LightHistorizedItem, QRCodeItem, GeoItem, null=True, blank=True, db_index=True) cached_division = models.TextField(_(u"Cached division"), null=True, blank=True, db_index=True) + parent = models.ForeignKey("Container", verbose_name=_("Parent container"), + on_delete=models.SET_NULL, + blank=True, null=True) index = models.IntegerField(u"Container ID", default=0) old_reference = models.TextField(_(u"Old reference"), blank=True, null=True) external_id = models.TextField(_(u"External ID"), blank=True, null=True) @@ -695,16 +698,24 @@ class Container(DocumentItem, LightHistorizedItem, QRCodeItem, GeoItem, def _generate_cached_location(self): items = [self.location.name, str(self.index)] - cached_label = u" - ".join(items) + cached_label = " - ".join(items) return cached_label def _generate_cached_division(self): + parents = [] + parent = self.parent + c_ids = [] + while parent: + if parent.id in c_ids: # prevent cyclic + break + c_ids.append(parent.id) + parents.append(parent) + parent = parent.parent locas = [ - u"{} {}".format(loca.division.division, loca.reference) - for loca in ContainerLocalisation.objects.filter( - container=self) + "{} {}".format(loca.container_type.name, loca.reference) + for loca in reversed(parents) ] - return u" | ".join(locas) + return " | ".join(locas) def _get_base_image_path(self): return self.responsible._get_base_image_path() + u"/" + self.external_id diff --git a/ishtar_common/models.py b/ishtar_common/models.py index a22a9307f..4b4753fe3 100644 --- a/ishtar_common/models.py +++ b/ishtar_common/models.py @@ -4358,7 +4358,6 @@ class Organization(Address, Merge, OwnPerms, ValueGetter, MainItem): url = models.URLField(verbose_name=_("Web address"), blank=True, null=True) cached_label = models.TextField(_("Cached name"), null=True, blank=True, db_index=True) - history = HistoricalRecords() DOWN_MODEL_UPDATE = ['members'] @@ -4563,7 +4562,6 @@ class Person(Address, Merge, OwnPerms, ValueGetter, MainItem): verbose_name=_("Is attached to"), blank=True, null=True) cached_label = models.TextField(_("Cached name"), null=True, blank=True, db_index=True) - history = HistoricalRecords() DOWN_MODEL_UPDATE = ["author"] class Meta: diff --git a/requirements.txt b/requirements.txt index 680b2986a..45ae30150 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ six>=1.9 -psycopg2==2.7.7 +psycopg2-binary==2.7.7 django-registration==2.2 django==1.11.28 Pillow==5.4.1 |