diff options
Diffstat (limited to 'archaeological_context_records')
10 files changed, 237 insertions, 77 deletions
diff --git a/archaeological_context_records/admin.py b/archaeological_context_records/admin.py index 62f4c12e2..8a0e254ae 100644 --- a/archaeological_context_records/admin.py +++ b/archaeological_context_records/admin.py @@ -54,7 +54,6 @@ class ContextRecordAdmin(HistorizedObjectAdmin, MainGeoDataItem): model = models.ContextRecord readonly_fields = HistorizedObjectAdmin.readonly_fields + [ "cached_label", - "datings", ] exclude = ["documents", "main_image"] diff --git a/archaeological_context_records/forms.py b/archaeological_context_records/forms.py index 4ecb40386..df074c768 100644 --- a/archaeological_context_records/forms.py +++ b/archaeological_context_records/forms.py @@ -179,6 +179,9 @@ class RecordSelect(GeoItemSelect, PeriodSelect): filling = forms.CharField(label=_("Filling")) interpretation = forms.CharField(label=_("Interpretation")) parcel = forms.CharField(label=_("Parcel")) + periods = forms.ChoiceField( + label=_("Periods"), choices=[], required=False + ) has_finds = forms.NullBooleanField(label=_("Has finds")) cr_relation_types = forms.ChoiceField( label=_("Search within relations"), choices=[] @@ -192,6 +195,7 @@ class RecordSelect(GeoItemSelect, PeriodSelect): TYPES = PeriodSelect.TYPES + [ FieldType('area', Area), + FieldType("periods", Period), FieldType('cultural_attributions', models.CulturalAttributionType), FieldType("unit", models.Unit), FieldType("cr_relation_types", models.RelationType), @@ -302,7 +306,10 @@ class RecordFormGeneral(CustomForm, ManageOldType): form_admin_name = _("Context record - 020 - General") form_slug = "contextrecord-020-general" file_upload = True - base_models = ["documentation", "excavation_technic", "structure", "texture", "color", "inclusion"] + base_models = [ + "documentation", "excavation_technic", "structure", "texture", "color", + "inclusion" + ] associated_models = { "archaeological_site": ArchaeologicalSite, "parcel": Parcel, @@ -527,23 +534,18 @@ class DatingForm(ManageOldType, forms.Form): ] -DatingFormSet = formset_factory(DatingForm, can_delete=True, formset=FormSet) -DatingFormSet.form_label = _("Dating") -DatingFormSet.form_admin_name = _("Context record - 030 - Dating") -DatingFormSet.form_slug = "contextrecord-030-datings" - - class RecordFormInterpretation(CustomForm, ManageOldType): HEADERS = {} form_label = _("Interpretation") form_admin_name = _("Context record - 040 - Interpretation") form_slug = "contextrecord-040-interpretation" - base_models = ["cultural_attribution", "identification"] + base_models = ["cultural_attribution", "identification", "period"] associated_models = { "activity": models.ActivityType, "identification": models.IdentificationType, 'cultural_attribution': models.CulturalAttributionType, + "period": Period, } interpretation = forms.CharField( label=_("Interpretation"), widget=forms.Textarea, required=False @@ -559,6 +561,10 @@ class RecordFormInterpretation(CustomForm, ManageOldType): taq_estimated = forms.IntegerField(label=_("Estimated TAQ"), required=False) tpq = forms.IntegerField(label=_("TPQ"), required=False) tpq_estimated = forms.IntegerField(label=_("Estimated TPQ"), required=False) + period = widgets.Select2MultipleField( + label=_("Periods"), + required=False, + ) cultural_attribution = forms.MultipleChoiceField( label=_("Cultural attributions"), choices=[], widget=widgets.Select2Multiple, @@ -573,6 +579,7 @@ class RecordFormInterpretation(CustomForm, ManageOldType): FieldType("identification", models.IdentificationType, True), FieldType('cultural_attribution', models.CulturalAttributionType, True), + FieldType("period", Period, is_multiple=True), ] diff --git a/archaeological_context_records/migrations/0124_contextrecord_periods.py b/archaeological_context_records/migrations/0124_contextrecord_periods.py new file mode 100644 index 000000000..72877aa2b --- /dev/null +++ b/archaeological_context_records/migrations/0124_contextrecord_periods.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.19 on 2025-10-28 15:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('archaeological_operations', '0123_add_timezone_django_v4'), + ('archaeological_context_records', '0123_add_timezone_django_v4'), + ] + + operations = [ + migrations.AddField( + model_name='contextrecord', + name='periods', + field=models.ManyToManyField(blank=True, to='archaeological_operations.period', verbose_name='Periods'), + ), + ] diff --git a/archaeological_context_records/migrations/0125_datings_refactoring.py b/archaeological_context_records/migrations/0125_datings_refactoring.py new file mode 100644 index 000000000..585939420 --- /dev/null +++ b/archaeological_context_records/migrations/0125_datings_refactoring.py @@ -0,0 +1,62 @@ +# Generated by Django 4.2.19 on 2025-10-29 10:53 + +from django.db import migrations, models +import django.db.models.deletion +import ishtar_common.models_common +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('archaeological_operations', '0123_add_timezone_django_v4'), + ('archaeological_context_records', '0124_contextrecord_periods'), + ] + + operations = [ + migrations.AlterModelOptions( + name='dating', + options={'verbose_name': 'Dating - deprecated', 'verbose_name_plural': 'Datings - deprecated'}, + ), + migrations.RenameField( + model_name='contextrecord', + old_name='datings', + new_name='datings_old', + ), + migrations.AddField( + model_name='dating', + name='external_id', + field=models.TextField(blank=True, default='', verbose_name='External ID'), + ), + migrations.AddField( + model_name='dating', + name='reference', + field=models.TextField(blank=True, default='', verbose_name='Reference'), + ), + migrations.AlterField( + model_name='dating', + name='period', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='archaeological_operations.period', verbose_name='Chronological period'), + ), + migrations.CreateModel( + name='ContextRecordDating', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4)), + ('reference', models.TextField(blank=True, default='', verbose_name='Reference')), + ('external_id', models.TextField(blank=True, default='', verbose_name='External ID')), + ('start_date', models.IntegerField(blank=True, null=True, verbose_name='Start date')), + ('end_date', models.IntegerField(blank=True, null=True, verbose_name='End date')), + ('precise_dating', models.TextField(blank=True, default='', verbose_name='Precise on this dating')), + ('context_record', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='datings', to='archaeological_context_records.contextrecord', verbose_name='Context record')), + ('dating_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='archaeological_context_records.datingtype', verbose_name='Dating type')), + ('period', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='archaeological_operations.period', verbose_name='Chronological period')), + ('quality', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='archaeological_context_records.datingquality', verbose_name='Quality')), + ], + options={ + 'verbose_name': 'Context record dating', + 'verbose_name_plural': 'Context record datings', + }, + bases=(models.Model, ishtar_common.models_common.SerializeItem), + ), + ] diff --git a/archaeological_context_records/migrations/0126_migrate_periods_and_datings.py b/archaeological_context_records/migrations/0126_migrate_periods_and_datings.py new file mode 100644 index 000000000..3b04c7ad1 --- /dev/null +++ b/archaeological_context_records/migrations/0126_migrate_periods_and_datings.py @@ -0,0 +1,19 @@ +from django.db import migrations +from ishtar_common.utils_migrations import migrate_dating_periods + + +def _migrate_datings_periods(apps, __): + model_dating = apps.get_model("archaeological_context_records", "contextrecorddating") + model = apps.get_model("archaeological_context_records", "contextrecord") + migrate_dating_periods(apps, model_dating, model, "context_record") + + +class Migration(migrations.Migration): + + dependencies = [ + ('archaeological_context_records', '0125_datings_refactoring'), + ] + + operations = [ + migrations.RunPython(_migrate_datings_periods) + ] diff --git a/archaeological_context_records/models.py b/archaeological_context_records/models.py index 29deae5ee..0c8a18f43 100644 --- a/archaeological_context_records/models.py +++ b/archaeological_context_records/models.py @@ -30,12 +30,15 @@ from django.db.models import Q from django.db.models.signals import post_delete, post_save, m2m_changed from django.urls import reverse, reverse_lazy -from ishtar_common.utils import gettext_lazy as _, pgettext_lazy, pgettext +from ishtar_common.utils import get_generated_id, gettext_lazy as _, pgettext_lazy, \ + pgettext + from django.utils.text import slugify from ishtar_common.utils import ( cached_label_changed, m2m_historization_changed, + related_historization_changed, post_save_geo, SearchAltName, ) @@ -101,12 +104,18 @@ post_save.connect(post_save_cache, sender=DatingQuality) post_delete.connect(post_save_cache, sender=DatingQuality) -class Dating(models.Model, SerializeItem): +class BaseDating(models.Model, SerializeItem): SLUG = "dating" SERIALIZE_EXCLUDE = ["find", "context_record"] + CURRENT_MODEL = None + CURRENT_MODEL_ATTR = None uuid = models.UUIDField(default=uuid.uuid4) + reference = models.TextField(_("Reference"), blank=True, default="") + external_id = models.TextField(_("External ID"), blank=True, default="") period = models.ForeignKey( - Period, verbose_name=_("Chronological period"), on_delete=models.PROTECT + Period, verbose_name=_("Chronological period"), on_delete=models.PROTECT, + blank=True, + null=True, ) start_date = models.IntegerField(_("Start date"), blank=True, null=True) end_date = models.IntegerField(_("End date"), blank=True, null=True) @@ -154,8 +163,7 @@ class Dating(models.Model, SerializeItem): } class Meta: - verbose_name = _("Dating") - verbose_name_plural = _("Datings") + abstract = True def __str__(self): if self.precise_dating and self.precise_dating.strip(): @@ -171,6 +179,8 @@ class Dating(models.Model, SerializeItem): def get_values(self, prefix="", no_values=False, filtr=None, **kwargs): values = {} + if not filtr or prefix + "reference" in filtr: + values[prefix + "reference"] = self.reference if not filtr or prefix + "period" in filtr: values[prefix + "period"] = str(self.period) if not filtr or prefix + "start_date" in filtr: @@ -188,6 +198,7 @@ class Dating(models.Model, SerializeItem): return values HISTORY_ATTR = [ + "reference", "period", "start_date", "end_date", @@ -198,7 +209,8 @@ class Dating(models.Model, SerializeItem): def history_compress(self): values = {} - for attr in self.HISTORY_ATTR: + attrs = self.HISTORY_ATTR + [self.CURRENT_MODEL_ATTR + "_id"] + for attr in attrs: val = getattr(self, attr) if hasattr(val, "history_compress"): val = val.history_compress() @@ -244,6 +256,7 @@ class Dating(models.Model, SerializeItem): attribute is identical """ for attr in [ + "reference", "period", "start_date", "end_date", @@ -283,6 +296,7 @@ class Dating(models.Model, SerializeItem): for dating in obj.datings.order_by("pk").all(): key = ( dating.period.pk, + dating.reference, dating.start_date, dating.end_date, dating.dating_type, @@ -294,6 +308,50 @@ class Dating(models.Model, SerializeItem): continue dating.delete() + @property + def q_parent(self): + if not self.pk or not self.CURRENT_MODEL: + return + if getattr(self, "__q_parent", None): + return self.__q_parent + q = self.CURRENT_MODEL.objects.filter(datings__pk=self.pk) + if q.count(): + self.__q_parent = q + return q + + @property + def parent_external_id(self): + if not self.pk or not self.q_parent: + return "" + return self.q_parent.all()[0].external_id + + @property + def auto_id(self): + if not self.pk or not self.q_parent: + return 0 + parent_pk = self.q_parent.all()[0].pk + attr = "context_record" if self.CURRENT_MODEL == ContextRecord else "find" + for idx, dating_pk in enumerate( + self.__class__.objects.filter(**{attr: parent_pk}).values_list( + "pk", flat=True).all()).all(): + if dating_pk == self.pk: + return idx + 1 + return 0 + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + if not self.pk: + return + external_id = get_generated_id("dating_external_id", self) + if external_id != self.external_id: + self.__class__.objects.filter(pk=self.pk).update(external_id=external_id) + + +class Dating(BaseDating): + class Meta: + verbose_name = _("Dating - deprecated") + verbose_name_plural = _("Datings - deprecated") + class Unit(GeneralType): order = models.IntegerField(_("Order")) @@ -693,6 +751,10 @@ class ContextRecord( pgettext_lazy("key for text search", "excavation-technique"), "excavation_technics__label__iexact", ), + "periods": SearchAltName( + pgettext_lazy("key for text search", "period"), + "periods__label__iexact", + ), "cultural_attributions": SearchAltName( pgettext_lazy("key for text search", "cultural-attribution"), "cultural_attributions__label__iexact", @@ -790,7 +852,8 @@ class ContextRecord( ("site", "archaeological_site__pk"), ("file", "operation__associated_file__pk"), ] - HISTORICAL_M2M = ["datings", "documentations", "excavation_technics", "identifications"] + HISTORICAL_M2M = ["periods", "datings", "documentations", "excavation_technics", + "identifications"] CACHED_LABELS = ["cached_label", "cached_periods", "cached_related_context_records"] DOWN_MODEL_UPDATE = ["base_finds"] GET_VALUES_EXTRA = ValueGetter.GET_VALUES_EXTRA + ["context_record"] @@ -917,7 +980,8 @@ class ContextRecord( default="", help_text=_("A short description of the location of the context record"), ) - datings = models.ManyToManyField(Dating, related_name="context_records") + datings_old = models.ManyToManyField(Dating, related_name="context_records") + periods = models.ManyToManyField(Period, verbose_name=_("Periods"), blank=True) documentations = models.ManyToManyField(DocumentationType, blank=True) structures = models.ManyToManyField(StructureType, blank=True) textures = models.ManyToManyField(TextureType, blank=True) @@ -1461,11 +1525,35 @@ post_save.connect(context_record_post_save, sender=ContextRecord) m2m_changed.connect(document_attached_changed, sender=ContextRecord.documents.through) m2m_changed.connect(geodata_attached_changed, sender=ContextRecord.geodata.through) -for attr in ContextRecord.HISTORICAL_M2M: - m2m_changed.connect( - m2m_historization_changed, sender=getattr(ContextRecord, attr).through + +class ContextRecordDating(BaseDating): + SERIALIZE_EXCLUDE = ["context_record"] + CURRENT_MODEL = ContextRecord + CURRENT_MODEL_ATTR = "context_record" + + context_record = models.ForeignKey( + ContextRecord, + verbose_name=_("Context record"), + related_name="datings", + on_delete=models.CASCADE, ) + class Meta: + verbose_name = _("Context record dating") + verbose_name_plural = _("Context record datings") + + +for attr in ContextRecord.HISTORICAL_M2M: + if attr == "datings": + model = ContextRecordDating + post_save.connect(related_historization_changed, sender=model) + post_delete.connect(related_historization_changed, sender=model) + else: + model = getattr(ContextRecord, attr).through + m2m_changed.connect( + m2m_historization_changed, sender=model + ) + class RelationType(GeneralRelationType): class Meta: diff --git a/archaeological_context_records/serializers.py b/archaeological_context_records/serializers.py index 2bccc63f7..0314de918 100644 --- a/archaeological_context_records/serializers.py +++ b/archaeological_context_records/serializers.py @@ -8,7 +8,7 @@ from archaeological_finds.serializers import ( generate_warehouse_queryset as finds_generate_warehouse_queryset, ) -CR_MODEL_LIST = [models.Dating, models.ContextRecord, models.RecordRelations] +CR_MODEL_LIST = [models.ContextRecord, models.ContextRecordDating, models.RecordRelations] # TODO: associated documents @@ -136,9 +136,8 @@ def cr_serialization( cr_ids = list( result_queryset[models.ContextRecord.__name__].values_list("id", flat=True) ) - result_queryset[models.Dating.__name__] = models.Dating.objects.filter( - Q(context_records__id__in=cr_ids) | Q(find__id__in=list(find_ids)) - ) + result_queryset[models.ContextRecordDating.__name__] = \ + models.ContextRecordDating.objects.filter(context_record_id__in=cr_ids) if get_queryset: return result_queryset diff --git a/archaeological_context_records/templates/ishtar/sheet_contextrecord.html b/archaeological_context_records/templates/ishtar/sheet_contextrecord.html index 4bd770fd0..869d6edd5 100644 --- a/archaeological_context_records/templates/ishtar/sheet_contextrecord.html +++ b/archaeological_context_records/templates/ishtar/sheet_contextrecord.html @@ -55,7 +55,7 @@ <a class="nav-link" id="{{window_id}}-datations-tab" data-toggle="tab" href="#{{window_id}}-datations" role="tab" aria-controls="{{window_id}}-datations" aria-selected="false"> - {% trans "Datations" %} + {% trans "Periods / Datings" %} </a> </li> {% endif %} @@ -214,10 +214,12 @@ {% if display_datations %} <div class="tab-pane fade" id="{{window_id}}-datations" role="tabpanel" aria-labelledby="{{window_id}}-datations-tab"> - {% if dating_list %} - <h3>{% trans "Datations" %}</h3> + {% if dating_list or item.periods_count %} + <h3>{% trans "Periods / Datings" %}</h3> {% endif %} + {% field_flex_multiple_obj _("Periods") item 'periods' %} {% include "ishtar/blocks/sheet_dating_list.html" %} + {% if item.cultural_attributions_count or item.taq or item.taq_estimated or item.tpq or item.tpq_estimated or datings_comment %} <h3>{% trans "Dating complements" %}</h3> <div class='row'> {% field_flex_multiple_obj "Cultural attributions" item 'cultural_attributions' %} @@ -227,6 +229,7 @@ {% field_flex "Estimated TPQ" item.tpq_estimated %} {% field_flex_full "Comment on datings" item.datings_comment "<pre>" "</pre>" has_image %} </div> + {% endif %} </div> {% endif %} diff --git a/archaeological_context_records/tests.py b/archaeological_context_records/tests.py index 852cd3aa7..ea29ce938 100644 --- a/archaeological_context_records/tests.py +++ b/archaeological_context_records/tests.py @@ -162,30 +162,14 @@ class ImportContextRecordTest(ImportTest, TestCase): impt = form.save(self.ishtar_user) impt.initialize() self.init_cr_targetkey(impt) - # Dating is not in models that can be created but force new is - # set for a column that references Dating impt.importation() - self.assertEqual(len(impt.errors), 5) + + self.assertEqual(len(impt.errors), 4) self.assertTrue( "doesn't exist in the database." in impt.errors[0]["error"] or "n'existe pas dans la base" in impt.errors[0]["error"] ) - # retry with only Dating (no context record) - for cr in models.ContextRecord.objects.all(): - cr.delete() - mcc, form = self.init_context_record_import() - mcc.created_models.clear() - dat_model, c = ImporterModel.objects.get_or_create( - klass="archaeological_context_records.models.Dating", - defaults={"name": "Dating"}, - ) - mcc.created_models.add(dat_model) - impt = form.save(self.ishtar_user) - impt.initialize() - self.init_cr_targetkey(impt) - impt.importation() - current_nb = models.ContextRecord.objects.count() self.assertEqual(current_nb, 0) @@ -199,30 +183,12 @@ class ImportContextRecordTest(ImportTest, TestCase): klass="archaeological_context_records.models.ContextRecord" ) ) - mcc.created_models.add(dat_model) impt = form.save(self.ishtar_user) impt.initialize() self.init_cr_targetkey(impt) impt.importation() current_nb = models.ContextRecord.objects.count() self.assertEqual(current_nb, 4) - """ - - # add a context record model - for cr in models.ContextRecord.objects.all(): - cr.delete() - mcc, form = self.init_context_record_import() - mcc.created_models.clear() - mcc.created_models.add(ImporterModel.objects.get( - klass='archaeological_context_records.models.ContextRecord' - )) - impt = form.save(self.ishtar_user) - impt.initialize() - self.init_cr_targetkey(impt) - impt.importation() - current_nb = models.ContextRecord.objects.count() - self.assertEqual(current_nb, 4) - """ class ContextRecordInit(OperationInitTest): @@ -282,10 +248,10 @@ class SerializationTest(GenericSerializationTest, ContextRecordInit, TestCase): ope2 = self.create_operation()[1] cr = self.create_context_record(data={"label": "CR 1", "operation": ope1})[0] cr2 = self.create_context_record(data={"label": "CR 2", "operation": ope2})[1] - dating = models.Dating.objects.create( + models.ContextRecordDating.objects.create( period=models.Period.objects.all()[0], + context_record=cr ) - cr.datings.add(dating) rlt = models.RelationType.objects.create( label="Test", txt_idx="test", symmetrical=False ) @@ -560,16 +526,20 @@ class ContextRecordTest(ContextRecordInit, TestCase): def test_redundant_dating_clean(self): obj = self.context_records[0] - values = {"period": models.Period.objects.all()[0]} + values = { + "period": models.Period.objects.all()[0], + "context_record": obj + } values_2 = { "period": models.Period.objects.all()[0], "quality": models.DatingQuality.objects.all()[0], + "context_record": obj } - obj.datings.add(models.Dating.objects.create(**values)) - obj.datings.add(models.Dating.objects.create(**values)) - obj.datings.add(models.Dating.objects.create(**values_2)) - obj.datings.add(models.Dating.objects.create(**values_2)) + models.ContextRecordDating.objects.create(**values) + models.ContextRecordDating.objects.create(**values) + models.ContextRecordDating.objects.create(**values_2) + models.ContextRecordDating.objects.create(**values_2) self.assertEqual(obj.datings.count(), 4) obj.fix() self.assertEqual(obj.datings.count(), 2) @@ -898,8 +868,7 @@ class ContextRecordSearchTest(ContextRecordInit, TestCase, SearchText): neo = models.Period.objects.get(txt_idx="neolithic") final_neo = models.Period.objects.get(txt_idx="final-neolithic") recent_neo = models.Period.objects.get(txt_idx="recent-neolithic") - dating = models.Dating.objects.create(period=final_neo) - cr.datings.add(dating) + models.ContextRecordDating.objects.create(period=final_neo, context_record=cr) search = {"datings__period": final_neo.pk} @@ -1664,9 +1633,6 @@ class ContextRecordWizardCreationTest(WizardTest, ContextRecordInit, TestCase): def post_wizard(self): self.assertEqual(models.ContextRecord.objects.count(), self.cr_nb + 2) - # identical datings, only one should be finaly save - cr = models.ContextRecord.objects.order_by("-pk")[0] - self.assertEqual(cr.datings.count(), 1) class ContextRecordRelationTest(ContextRecordInit, TestCase): diff --git a/archaeological_context_records/views.py b/archaeological_context_records/views.py index 437a2824e..69f995932 100644 --- a/archaeological_context_records/views.py +++ b/archaeological_context_records/views.py @@ -115,7 +115,6 @@ record_search_wizard = wizards.RecordSearch.as_view( record_creation_steps = [ ("selec-record_creation", forms.OperationRecordFormSelection), ("general-record_creation", forms.RecordFormGeneral), - ("datings-record_creation", forms.DatingFormSet), ("interpretation-record_creation", forms.RecordFormInterpretation), ("final-record_creation", forms.FinalForm), ] @@ -130,7 +129,6 @@ record_modification_steps = [ ("selec-record_modification", forms.RecordFormSelection), ("operation-record_modification", forms.OperationFormSelection), ("general-record_modification", forms.RecordFormGeneral), - ("datings-record_modification", forms.DatingFormSet), ("interpretation-record_modification", forms.RecordFormInterpretation), ("final-record_modification", forms.FinalForm), ] |
