diff options
Diffstat (limited to 'archaeological_operations')
14 files changed, 1229 insertions, 154 deletions
diff --git a/archaeological_operations/admin.py b/archaeological_operations/admin.py index 429315513..82c5c7f90 100644 --- a/archaeological_operations/admin.py +++ b/archaeological_operations/admin.py @@ -141,6 +141,7 @@ class RecordRelationsAdmin(admin.ModelAdmin): admin_site.register(models.RecordRelations, RecordRelationsAdmin) +admin_site.register(models.SiteRecordRelations, RecordRelationsAdmin) class RelationTypeAdmin(GeneralTypeAdmin): @@ -150,6 +151,7 @@ class RelationTypeAdmin(GeneralTypeAdmin): admin_site.register(models.RelationType, RelationTypeAdmin) +admin_site.register(models.SiteRelationType, RelationTypeAdmin) class RecordQualityTypeAdmin(GeneralTypeAdmin): @@ -210,7 +212,10 @@ admin_site.register(models.CulturalAttributionType, CulturalAttributionTypeAdmin) -general_models = [models.RemainType, models.NatureOfSiteType, models.InterpretationLevelType, - models.SiteCurrentStatusType, models.SiteDiscoveryStatusType, models.SiteType] +general_models = [ + models.RemainType, models.NatureOfSiteType, models.InterpretationLevelType, + models.SiteCurrentStatusType, models.SiteDiscoveryStatusType, models.SiteType, + models.HeritageInterestType, models.HeritageAndEnvironmentalProtectionType +] for model in general_models: admin_site.register(model, GeneralTypeAdmin) diff --git a/archaeological_operations/forms.py b/archaeological_operations/forms.py index ba59f4419..3e94e3963 100644 --- a/archaeological_operations/forms.py +++ b/archaeological_operations/forms.py @@ -31,7 +31,6 @@ from django.core import validators from django.db.models import Max from django.forms.formsets import formset_factory, DELETION_FIELD_NAME, \ TOTAL_FORM_COUNT -from django.utils.functional import lazy from django.utils.safestring import mark_safe from ishtar_common.utils import gettext_lazy as _, pgettext_lazy, parse_parcels @@ -42,13 +41,11 @@ from ishtar_common import widgets from ishtar_common.forms import FinalForm, FormSet, \ reverse_lazy, get_data_from_formset, QAForm, CustomFormSearch,\ ManageOldType, IshtarForm, CustomForm, FieldType, FormHeader, \ - GeoItemSelect, LockForm, MultiSearchForm, DocumentItemSelect + GeoItemSelect, LockForm, MultiSearchForm, DocumentItemSelect, DatingSelect from ishtar_common.forms_common import TownFormSet, get_town_field, TownForm from ishtar_common.models import valid_id, valid_ids, Person, Town, \ DocumentTemplate, Organization, get_current_profile, \ - person_type_pks_lazy, person_type_pk_lazy, organization_type_pks_lazy, \ - organization_type_pk_lazy, SpatialReferenceSystem, Area, \ - get_sra_agent_label, get_sra_agent_head_scientist_label, get_operator_label + SpatialReferenceSystem, Area, QualifiedBiographicalNote from ishtar_common.wizards import MultiValueDict from .widgets import ParcelWidget, SelectParcelWidget, OAWidget @@ -435,6 +432,32 @@ RecordRelationsFormSet.form_admin_name = _("Operation - Relations") RecordRelationsFormSet.form_slug = "operation-relations" +class SiteRecordRelationsForm(RecordRelationsForm): + current_model = models.SiteRelationType + current_related_model = models.ArchaeologicalSite + associated_models = { + "right_record": models.ArchaeologicalSite, + "relation_type": models.SiteRelationType, + } + ERROR_MISSING = _("You should select an archaeological site and a relation type.") + ERROR_SAME = _("An archaeological site cannot be related to himself.") + + right_record = forms.IntegerField( + label=_("Archaeological site"), + widget=widgets.JQueryAutoComplete( + reverse_lazy('autocomplete-archaeologicalsite'), + associated_model=models.ArchaeologicalSite), + validators=[valid_id(models.ArchaeologicalSite)], required=False) + + +SiteRecordRelationsFormSet = formset_factory( + SiteRecordRelationsForm, can_delete=True, formset=RecordRelationsFormSetBase +) +SiteRecordRelationsFormSet.form_label = _("Archaeological site - Relations") +RecordRelationsFormSet.form_admin_name = _("Archaeological site - Relations") +RecordRelationsFormSet.form_slug = "archaeologicalsite-recordrelations" + + class OpeSiteRelationsForm(ManageOldType): associated_models = {'right_record': models.ArchaeologicalSite} @@ -449,6 +472,7 @@ class OpeSiteRelationsForm(ManageOldType): super().__init__(*args, **kwargs) self.fields["right_record"].label = get_current_profile().get_site_label() + OpeSiteRelationsFormSet = formset_factory( OpeSiteRelationsForm, can_delete=True, formset=RecordRelationsFormSetBase, extra=3 @@ -631,6 +655,7 @@ class OperationFormMultiSelection(LockForm, MultiSearchForm): class OperationCodeInput(forms.TextInput): """Manage auto complete when changing year in form""" + def render(self, *args, **kwargs): name, value = args base_name = '-'.join(name.split('-')[:-1]) @@ -1176,7 +1201,7 @@ class OperationDeletionForm(FinalForm): ######### -class SiteSelect(GeoItemSelect): +class SiteSelect(GeoItemSelect, DatingSelect): _model = models.ArchaeologicalSite form_admin_name = _("Archaeological site - 001 - Search") form_slug = "archaeological_site-001-search" @@ -1189,23 +1214,48 @@ class SiteSelect(GeoItemSelect): name = forms.CharField(label=_("Name"), max_length=200, required=False) other_reference = forms.CharField(label=_("Other reference"), max_length=200, required=False) - types = widgets.Select2MultipleField(label=_("Types"), required=False) + types = widgets.Select2SimpleField(label=_("Types"), required=False, + modal="modal-advanced-search") + heritage_interest = forms.ChoiceField(label=_("Heritage interests"), choices=[], + required=False) + actors = forms.IntegerField( + label=_("Actors"), required=False, + widget=widgets.JQueryAutoComplete( + reverse_lazy('autocomplete-qualifiedbiographicalnote'))) + collaborators = forms.IntegerField( + label=_("Collaborators"), required=False, + widget=widgets.JQueryAutoComplete( + reverse_lazy('autocomplete-person'))) + protection_id = forms.CharField(label=_("Protection ID"), required=False) + protection_date = DateField(label=_("Protection date"), required=False) + heritage_environmental_protections = forms.ChoiceField( + label=_("Heritage and environmental protections"), choices=[], + required=False) + details_on_protection = forms.CharField(label=_("Details on protection"), + required=False) discoverer = forms.IntegerField( - widget=widgets.JQueryAutoComplete(reverse_lazy('autocomplete-person'), associated_model=Person), + widget=widgets.JQueryAutoComplete(reverse_lazy('autocomplete-person'), + associated_model=Person), label=_("Discoverer")) - periods = forms.ChoiceField(label=_("Periods"), choices=[], required=False) - remains = forms.ChoiceField(label=_("Remains"), choices=[], required=False) - cultural_attributions = forms.ChoiceField( - label=_("Cultural attribution"), choices=[], required=False) - discovery_status = forms.ChoiceField(label=_("Discovery status"), choices=[], required=False) - current_status = forms.ChoiceField(label=_("Current status"), choices=[], required=False) - nature_of_site = forms.ChoiceField(label=_("Nature of site"), choices=[], required=False) - interpretation_level = forms.ChoiceField(label=_("Interpretation level"), choices=[], required=False) + periods = widgets.Select2SimpleField(label=_("Periods"), required=False, + modal="modal-advanced-search") + remains = widgets.Select2SimpleField(label=_("Remains"), required=False, + modal="modal-advanced-search") + cultural_attributions = widgets.Select2SimpleField( + label=_("Cultural attributions"), required=False, modal="modal-advanced-search") + discovery_status = forms.ChoiceField(label=_("Discovery status"), choices=[], + required=False) + current_states = forms.ChoiceField(label=_("Current states"), required=False) + nature_of_site = forms.ChoiceField(label=_("Nature of site"), choices=[], + required=False) + interpretation_level = forms.ChoiceField(label=_("Interpretation level"), + choices=[], required=False) + towns = get_town_field() towns__areas = forms.ChoiceField(label=_("Areas"), choices=[]) - description = forms.CharField(label=_("Description"), max_length=200, required=False) - public_description = forms.CharField(label=_("Public description"), max_length=200, required=False) - comment = forms.CharField(label=_("Comment"), max_length=200, required=False) + description = forms.CharField(label=_("Description"), required=False) + public_description = forms.CharField(label=_("Public description"), required=False) + comment = forms.CharField(label=_("Comment"), required=False) top_operation = forms.IntegerField( label=_("Top operation"), required=False, widget=widgets.JQueryAutoComplete( @@ -1244,17 +1294,21 @@ class SiteSelect(GeoItemSelect): discovery_area = forms.CharField( label=_("Discovery area"), max_length=200, required=False) + TYPES = [ FieldType('periods', models.Period), FieldType('remains', models.RemainType), - FieldType("types", models.SiteType, is_multiple=True), - FieldType('current_status', models.SiteCurrentStatusType), + FieldType("types", models.SiteType), + FieldType('current_states', models.SiteCurrentStatusType), FieldType('discovery_status', models.SiteDiscoveryStatusType), FieldType('cultural_attributions', models.CulturalAttributionType), FieldType('nature_of_site', models.NatureOfSiteType), FieldType('interpretation_level', models.InterpretationLevelType), FieldType('towns__areas', Area), - ] + GeoItemSelect.TYPES + FieldType('heritage_interest', models.HeritageInterestType), + FieldType('heritage_environmental_protections', + models.HeritageAndEnvironmentalProtectionType), + ] + GeoItemSelect.TYPES + DatingSelect.TYPES def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -1268,6 +1322,7 @@ class SiteSelect(GeoItemSelect): 'affmar_number', 'drassm_number', )) + self._reorder_period_fields("comment") class SiteFormSelection(LockForm, CustomFormSearch): @@ -1312,41 +1367,79 @@ class SiteForm(CustomForm, ManageOldType): form_admin_name = _("Archaeological site - 010 - General") form_slug = "archaeological_site-010-general" associated_models = { - 'period': models.Period, 'remain': models.RemainType, - 'spatial_reference_system': SpatialReferenceSystem, - 'cultural_attribution': models.CulturalAttributionType, - 'collaborator': Person, - 'discoverer': Person, - "nature_of_site": models.NatureOfSiteType, - "interpretation_level": models.InterpretationLevelType, + "actor": QualifiedBiographicalNote, + "collaborator": Person, + "cultural_attribution": models.CulturalAttributionType, + "current_state": models.SiteCurrentStatusType, + "discoverer": Person, "discovery_status": models.SiteDiscoveryStatusType, - "current_status": models.SiteCurrentStatusType, + "editor": models.Author, + "heritage_interest": models.HeritageInterestType, + "heritage_environmental_protection": models.HeritageAndEnvironmentalProtectionType, + "interpretation_level": models.InterpretationLevelType, + "nature_of_site": models.NatureOfSiteType, + "period": models.Period, + "remain": models.RemainType, "town": Town, "type": models.SiteType, } - base_models = ["period", "remain", "collaborator", "cultural_attribution", "town", "type"] - + extra_form_modals = ["author", "qualifiedbiographicalnote", "biographicalnote", + "person", "organization"] + base_models = [ + "actor", "collaborator", "cultural_attribution", "current_state", "editor", + "heritage_interest", "heritage_environmental_protection", "period", "remain", + "town", "type" + ] pk = forms.IntegerField(required=False, widget=forms.HiddenInput) + HEADERS['reference'] = FormHeader(_("General")) reference = forms.CharField(label=_("Reference"), max_length=200) - name = forms.CharField(label=_("Name"), max_length=200, required=False) other_reference = forms.CharField(label=_("Other reference"), required=False) + name = forms.CharField(label=_("Name"), max_length=200, required=False) + heritage_interest = forms.MultipleChoiceField( + label=_("Heritage interest"), choices=[], widget=widgets.Select2Multiple, + required=False) + actor = widgets.Select2MultipleField( + model=QualifiedBiographicalNote, label=_("Actors"), required=False, + remote=True, new=True, remote_filter='qualification_type__S-A') collaborator = widgets.Select2MultipleField( - model=Person, label=_("Collaborators"), required=False, remote=True) + model=Person, label=_("Collaborators"), required=False, + remote=True, new=True) description = forms.CharField(label=_("Description"), widget=forms.Textarea, required=False) - public_description = forms.CharField(label=_("Public description"), widget=forms.Textarea, - required=False) + public_description = forms.CharField( + label=_("Public description"), widget=forms.Textarea, + required=False + ) comment = forms.CharField(label=_("Comment"), widget=forms.Textarea, required=False) + HEADERS['protection_id'] = FormHeader(_("Protection")) + protection_id = forms.CharField(label=_("Protection ID"), required=False) + protection_date = DateField(label=_("Protection date"), required=False) + heritage_environmental_protection = forms.MultipleChoiceField( + label=_("Heritage and environmental protections"), choices=[], + widget=widgets.Select2Multiple, required=False) + details_on_protection = forms.CharField( + label=_("Details on protection"), widget=forms.Textarea, + required=False + ) HEADERS['type'] = FormHeader(_("Scientific")) type = forms.MultipleChoiceField( label=_("Types"), choices=[], widget=widgets.Select2Multiple, required=False) - nature_of_site = forms.ChoiceField(choices=[], label=_("Nature of site"), required=False) - interpretation_level = forms.ChoiceField(choices=[], label=_("Interpretation level"), required=False) - discovery_status = forms.ChoiceField(choices=[], label=_("Discovery status"), required=False) - current_status = forms.ChoiceField(choices=[], label=_("Current status"), required=False) + nature_of_site = forms.ChoiceField( + choices=[], label=_("Nature of site"), required=False + ) + interpretation_level = forms.ChoiceField( + choices=[], label=_("Interpretation level"), required=False + ) + discovery_status = forms.ChoiceField( + choices=[], label=_("Discovery status"), required=False + ) + current_state = forms.MultipleChoiceField( + label=_("Current states"), choices=[], widget=widgets.Select2Multiple, + required=False + ) period = forms.MultipleChoiceField( label=_("Periods"), choices=[], widget=widgets.Select2Multiple, required=False) @@ -1359,7 +1452,8 @@ class SiteForm(CustomForm, ManageOldType): required=False) discoverer = forms.IntegerField( widget=widgets.JQueryAutoComplete( - reverse_lazy('autocomplete-person-permissive'), associated_model=Person), + reverse_lazy('autocomplete-person-permissive'), associated_model=Person, + new=True), label=_("Discoverer"), required=False) HEADERS['town'] = FormHeader(_("Localization")) town = widgets.Select2MultipleField( @@ -1378,22 +1472,32 @@ class SiteForm(CustomForm, ManageOldType): widget=forms.Textarea, required=False ) + HEADERS["editor"] = FormHeader(_("Sheet")) + editor = widgets.Select2MultipleField( + label=_("Editors"), required=False, + model=models.Author, remote=True, new=True + ) + TYPES = [ FieldType('type', models.SiteType, True), FieldType('period', models.Period, True), FieldType('remain', models.RemainType, True), FieldType('cultural_attribution', models.CulturalAttributionType, True), + FieldType('heritage_interest', models.HeritageInterestType, True), + FieldType('heritage_environmental_protection', + models.HeritageAndEnvironmentalProtectionType, True), FieldType('nature_of_site', models.NatureOfSiteType), FieldType('interpretation_level', models.InterpretationLevelType), - FieldType('current_status', models.SiteCurrentStatusType), + FieldType('current_state', models.SiteCurrentStatusType, True), FieldType('discovery_status', models.SiteDiscoveryStatusType), ] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if 'collaborator' in self.fields: - self.fields['collaborator'].widget.attrs['full-width'] = True + OPTIONS_PERMISSIONS = [ + # field name, permission, options + ("actor", ("ishtar_common.add_qualifiedbiographicalnote",), {"new": True}), + ("discoverer", ("ishtar_common.add_person",), {"new": True}), + ("editor", ("ishtar_common.add_author",), {"new": True}), + ] def clean_reference(self): reference = self.cleaned_data['reference'] diff --git a/archaeological_operations/migrations/0125_archaeologicalsite_actors.py b/archaeological_operations/migrations/0125_archaeologicalsite_actors.py new file mode 100644 index 000000000..ac1d91587 --- /dev/null +++ b/archaeological_operations/migrations/0125_archaeologicalsite_actors.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.19 on 2026-03-24 09:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ishtar_common', '0274_qualifiedbiographicalnote'), + ('archaeological_operations', '0124_archaeologicalsite_cached_types'), + ] + + operations = [ + migrations.AddField( + model_name='archaeologicalsite', + name='actors', + field=models.ManyToManyField(blank=True, related_name='sites', to='ishtar_common.qualifiedbiographicalnote', verbose_name='Actors'), + ), + ] diff --git a/archaeological_operations/migrations/0126_archaeologicalsite_heritage_relations_datings.py b/archaeological_operations/migrations/0126_archaeologicalsite_heritage_relations_datings.py new file mode 100644 index 000000000..baa407d6f --- /dev/null +++ b/archaeological_operations/migrations/0126_archaeologicalsite_heritage_relations_datings.py @@ -0,0 +1,207 @@ +# Generated by Django 4.2.19 on 2026-03-27 12:03 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import ishtar_common.models_common +import re +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('ishtar_common', '0275_authortype_parent'), + ('archaeological_context_records', '0126_migrate_periods_and_datings'), + ('archaeological_operations', '0125_archaeologicalsite_actors'), + ] + + operations = [ + migrations.AddField( + model_name='archaeologicalsite', + name='cached_current_states', + field=models.TextField(blank=True, default='', help_text='Generated automatically - do not edit', verbose_name='Cached current states label'), + ), + migrations.AddField( + model_name='archaeologicalsite', + name='cached_heritage_environmental_protections', + field=models.TextField(blank=True, default='', help_text='Generated automatically - do not edit', verbose_name='Cached heritage and environmental protections label'), + ), + migrations.AddField( + model_name='archaeologicalsite', + name='cached_heritage_interests', + field=models.TextField(blank=True, default='', help_text='Generated automatically - do not edit', verbose_name='Cached heritage interests label'), + ), + migrations.AddField( + model_name='archaeologicalsite', + name='current_states', + field=models.ManyToManyField(blank=True, to='archaeological_operations.sitecurrentstatustype', verbose_name='Current status'), + ), + migrations.AddField( + model_name='archaeologicalsite', + name='details_on_protection', + field=models.TextField(blank=True, default='', verbose_name='Details on protection'), + ), + migrations.AddField( + model_name='archaeologicalsite', + name='editors', + field=models.ManyToManyField(blank=True, related_name='sites', to='ishtar_common.author', verbose_name='Editors'), + ), + migrations.AddField( + model_name='archaeologicalsite', + name='protection_date', + field=models.DateField(blank=True, null=True, verbose_name='Protection date'), + ), + migrations.AddField( + model_name='archaeologicalsite', + name='protection_id', + field=models.TextField(blank=True, default='', verbose_name='Protection ID'), + ), + migrations.AddField( + model_name='historicalarchaeologicalsite', + name='cached_current_states', + field=models.TextField(blank=True, default='', help_text='Generated automatically - do not edit', verbose_name='Cached current states label'), + ), + migrations.AddField( + model_name='historicalarchaeologicalsite', + name='cached_heritage_environmental_protections', + field=models.TextField(blank=True, default='', help_text='Generated automatically - do not edit', verbose_name='Cached heritage and environmental protections label'), + ), + migrations.AddField( + model_name='historicalarchaeologicalsite', + name='cached_heritage_interests', + field=models.TextField(blank=True, default='', help_text='Generated automatically - do not edit', verbose_name='Cached heritage interests label'), + ), + migrations.AddField( + model_name='historicalarchaeologicalsite', + name='details_on_protection', + field=models.TextField(blank=True, default='', verbose_name='Details on protection'), + ), + migrations.AddField( + model_name='historicalarchaeologicalsite', + name='protection_date', + field=models.DateField(blank=True, null=True, verbose_name='Protection date'), + ), + migrations.AddField( + model_name='historicalarchaeologicalsite', + name='protection_id', + field=models.TextField(blank=True, default='', verbose_name='Protection ID'), + ), + migrations.AlterField( + model_name='archaeologicalsite', + name='current_status', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sites_deprecated', to='archaeological_operations.sitecurrentstatustype', verbose_name='Current status - deprecated'), + ), + migrations.AlterField( + model_name='historicalarchaeologicalsite', + name='current_status', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='archaeological_operations.sitecurrentstatustype', verbose_name='Current status - deprecated'), + ), + migrations.CreateModel( + name='SiteRelationType', + 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=1, verbose_name='Order')), + ('symmetrical', models.BooleanField(verbose_name='Symmetrical')), + ('tiny_label', models.CharField(blank=True, max_length=50, null=True, verbose_name='Tiny label')), + ('logical_relation', models.CharField(blank=True, choices=[('above', 'Above'), ('below', 'Below'), ('equal', 'Equal'), ('include', 'Include'), ('included', 'Is included')], max_length=10, null=True, verbose_name='Logical relation')), + ('inverse_relation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='archaeological_operations.siterelationtype', verbose_name='Inverse relation')), + ], + options={ + 'verbose_name': 'Site relation type', + 'verbose_name_plural': 'Site relation types', + 'ordering': ('order', 'label'), + }, + bases=(ishtar_common.models_common.Cached, models.Model), + ), + migrations.CreateModel( + name='SiteRecordRelations', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('timestamp_geo', models.IntegerField(blank=True, null=True, verbose_name='Timestamp geo')), + ('timestamp_label', models.IntegerField(blank=True, null=True, verbose_name='Timestamp label')), + ('imports', models.ManyToManyField(blank=True, related_name='imported_%(app_label)s_%(class)s', to='ishtar_common.import', verbose_name='Created by imports')), + ('imports_updated', models.ManyToManyField(blank=True, related_name='import_updated_%(app_label)s_%(class)s', to='ishtar_common.import', verbose_name='Updated by imports')), + ('left_record', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='right_relations', to='archaeological_operations.archaeologicalsite')), + ('relation_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='archaeological_operations.siterelationtype')), + ('right_record', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='left_relations', to='archaeological_operations.archaeologicalsite')), + ], + options={ + 'verbose_name': 'Site record relation', + 'verbose_name_plural': 'Site record relations', + 'ordering': ('left_record__cached_label', 'relation_type', 'right_record__cached_label'), + 'permissions': [('view_siterelation', 'Can view all Site relations')], + }, + ), + migrations.CreateModel( + name='SiteDating', + 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')), + ('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')), + ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='datings', to='archaeological_operations.archaeologicalsite', verbose_name='Site')), + ], + options={ + 'verbose_name': 'Site dating', + 'verbose_name_plural': 'Site datings', + }, + bases=(models.Model, ishtar_common.models_common.SerializeItem), + ), + migrations.CreateModel( + name='HeritageInterestType', + 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='archaeological_operations.heritageinteresttype', verbose_name='Parent')), + ], + options={ + 'verbose_name': 'Heritage interest type', + 'verbose_name_plural': 'Heritage interest types', + 'ordering': ('order', 'label'), + }, + bases=(ishtar_common.models_common.Cached, models.Model), + ), + migrations.CreateModel( + name='HeritageAndEnvironmentalProtectionType', + 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='archaeological_operations.heritageandenvironmentalprotectiontype', verbose_name='Parent')), + ], + options={ + 'verbose_name': 'Heritage and environmental protection type', + 'verbose_name_plural': 'Heritage and environmental protection types', + 'ordering': ('order', 'label'), + }, + bases=(ishtar_common.models_common.Cached, models.Model), + ), + migrations.AddField( + model_name='archaeologicalsite', + name='heritage_environmental_protections', + field=models.ManyToManyField(blank=True, to='archaeological_operations.heritageandenvironmentalprotectiontype', verbose_name='Heritage and environmental protections'), + ), + migrations.AddField( + model_name='archaeologicalsite', + name='heritage_interests', + field=models.ManyToManyField(blank=True, to='archaeological_operations.heritageinteresttype', verbose_name='Heritage interests'), + ), + ] diff --git a/archaeological_operations/migrations/0127_data_migration_current_states.py b/archaeological_operations/migrations/0127_data_migration_current_states.py new file mode 100644 index 000000000..fe1aa5c25 --- /dev/null +++ b/archaeological_operations/migrations/0127_data_migration_current_states.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.19 on 2026-03-27 12:06 + +from django.db import migrations + + +def migrate_current_states(apps, __): + ArchaeologicalSite = apps.get_model("archaeological_operations", "archaeologicalsite") + if not hasattr(ArchaeologicalSite, "current_status"): + return + q = ArchaeologicalSite.objects.filter(current_status__isnull=False) + for site in q.all(): + site.current_states.add(site.current_status) + + +class Migration(migrations.Migration): + + dependencies = [ + ('archaeological_operations', '0126_archaeologicalsite_heritage_relations_datings'), + ] + + operations = [ + migrations.RunPython(migrate_current_states) + ] diff --git a/archaeological_operations/migrations/0128_add_editors.py b/archaeological_operations/migrations/0128_add_editors.py new file mode 100644 index 000000000..d26c47f74 --- /dev/null +++ b/archaeological_operations/migrations/0128_add_editors.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.21 on 2026-04-02 06:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ishtar_common', '0276_add_editors'), + ('archaeological_operations', '0127_data_migration_current_states'), + ] + + operations = [ + migrations.AddField( + model_name='administrativeact', + name='editors', + field=models.ManyToManyField(blank=True, related_name='%(class)s_edited', to='ishtar_common.author', verbose_name='Editors'), + ), + migrations.AddField( + model_name='operation', + name='editors', + field=models.ManyToManyField(blank=True, related_name='%(class)s_edited', to='ishtar_common.author', verbose_name='Editors'), + ), + migrations.AddField( + model_name='parcel', + name='editors', + field=models.ManyToManyField(blank=True, related_name='%(class)s_edited', to='ishtar_common.author', verbose_name='Editors'), + ), + migrations.AddField( + model_name='parcelowner', + name='editors', + field=models.ManyToManyField(blank=True, related_name='%(class)s_edited', to='ishtar_common.author', verbose_name='Editors'), + ), + migrations.AlterField( + model_name='archaeologicalsite', + name='editors', + field=models.ManyToManyField(blank=True, related_name='%(class)s_edited', to='ishtar_common.author', verbose_name='Editors'), + ), + ] diff --git a/archaeological_operations/migrations/0129_data_migration.json b/archaeological_operations/migrations/0129_data_migration.json new file mode 100644 index 000000000..f1e341c88 --- /dev/null +++ b/archaeological_operations/migrations/0129_data_migration.json @@ -0,0 +1,396 @@ +[ + { + "model": "archaeological_operations.heritageandenvironmentalprotectiontype", + "fields": { + "label": "Protections et labels patrimoniaux", + "txt_idx": "protections-et-labels-patrimoniaux", + "comment": "", + "available": true, + "parent": null, + "order": 100 + } + }, + { + "model": "archaeological_operations.heritageandenvironmentalprotectiontype", + "fields": { + "label": "Protections environnementales", + "txt_idx": "protections-environnementales", + "comment": "", + "available": true, + "parent": null, + "order": 300 + } + }, + { + "model": "archaeological_operations.heritageandenvironmentalprotectiontype", + "fields": { + "label": "Inscrit MH", + "txt_idx": "inscrit-mh", + "comment": "", + "available": true, + "parent": [ + "protections-et-labels-patrimoniaux" + ], + "order": 110 + } + }, + { + "model": "archaeological_operations.heritageandenvironmentalprotectiontype", + "fields": { + "label": "Class\u00e9 MH", + "txt_idx": "classe-mh", + "comment": "", + "available": true, + "parent": [ + "protections-et-labels-patrimoniaux" + ], + "order": 120 + } + }, + { + "model": "archaeological_operations.heritageandenvironmentalprotectiontype", + "fields": { + "label": "P\u00e9rim\u00e8tre MH", + "txt_idx": "Perimetre-mh", + "comment": "", + "available": true, + "parent": [ + "protections-et-labels-patrimoniaux" + ], + "order": 130 + } + }, + { + "model": "archaeological_operations.heritageandenvironmentalprotectiontype", + "fields": { + "label": "Site Patrimonial remarquable", + "txt_idx": "site-patrimonial-remarquable", + "comment": "", + "available": true, + "parent": [ + "protections-et-labels-patrimoniaux" + ], + "order": 140 + } + }, + { + "model": "archaeological_operations.heritageandenvironmentalprotectiontype", + "fields": { + "label": "Label ACR ", + "txt_idx": "label-acr", + "comment": "", + "available": true, + "parent": [ + "protections-et-labels-patrimoniaux" + ], + "order": 150 + } + }, + { + "model": "archaeological_operations.heritageandenvironmentalprotectiontype", + "fields": { + "label": "Label PIR", + "txt_idx": "label-pir", + "comment": "", + "available": true, + "parent": [ + "protections-et-labels-patrimoniaux" + ], + "order": 160 + } + }, + { + "model": "archaeological_operations.heritageandenvironmentalprotectiontype", + "fields": { + "label": "Jardin remarquable ", + "txt_idx": "jardin-remarquable", + "comment": "", + "available": true, + "parent": [ + "protections-et-labels-patrimoniaux" + ], + "order": 200 + } + }, + { + "model": "archaeological_operations.heritageandenvironmentalprotectiontype", + "fields": { + "label": "Maison des Illustres ", + "txt_idx": "maison-des-illustres", + "comment": "", + "available": true, + "parent": [ + "protections-et-labels-patrimoniaux" + ], + "order": 220 + } + }, + { + "model": "archaeological_operations.heritageandenvironmentalprotectiontype", + "fields": { + "label": "Natura 2000", + "txt_idx": "natura-2000", + "comment": "", + "available": true, + "parent": [ + "protections-environnementales" + ], + "order": 320 + } + }, + { + "model": "archaeological_operations.heritageandenvironmentalprotectiontype", + "fields": { + "label": "Site inscrit ", + "txt_idx": "site-inscrit", + "comment": "", + "available": true, + "parent": [ + "protections-environnementales" + ], + "order": 340 + } + }, + { + "model": "archaeological_operations.heritageandenvironmentalprotectiontype", + "fields": { + "label": "Site class\u00e9 ", + "txt_idx": "site-classe", + "comment": "", + "available": true, + "parent": [ + "protections-environnementales" + ], + "order": 360 + } + }, + { + "model": "archaeological_operations.heritageandenvironmentalprotectiontype", + "fields": { + "label": "PVAP", + "txt_idx": "pvap", + "comment": "", + "available": true, + "parent": [ + "site-patrimonial-remarquable" + ], + "order": 10 + } + }, + { + "model": "archaeological_operations.heritageandenvironmentalprotectiontype", + "fields": { + "label": "PSMV", + "txt_idx": "psmv", + "comment": "", + "available": true, + "parent": [ + "site-patrimonial-remarquable" + ], + "order": 20 + } + }, + { + "model": "archaeological_operations.heritageinteresttype", + "fields": { + "label": "Peu int\u00e9ressant", + "txt_idx": "peu-interessant", + "comment": "", + "available": true, + "parent": null, + "order": 10 + } + }, + { + "model": "archaeological_operations.heritageinteresttype", + "fields": { + "label": "Int\u00e9ressant", + "txt_idx": "interessant", + "comment": "", + "available": true, + "parent": null, + "order": 20 + } + }, + { + "model": "archaeological_operations.heritageinteresttype", + "fields": { + "label": "Tr\u00e8s int\u00e9ressant", + "txt_idx": "tres-interessant", + "comment": "", + "available": true, + "parent": null, + "order": 30 + } + }, + { + "model": "archaeological_operations.heritageinteresttype", + "fields": { + "label": "Remarquable", + "txt_idx": "remarquable", + "comment": "", + "available": true, + "parent": null, + "order": 40 + } + }, + { + "model": "archaeological_operations.heritageinteresttype", + "fields": { + "label": "\u00c0 prot\u00e9ger", + "txt_idx": "a-proteger", + "comment": "", + "available": true, + "parent": null, + "order": 50 + } + }, + { + "model": "archaeological_operations.siterelationtype", + "fields": { + "label": "Voisin de", + "txt_idx": "voisin-de", + "comment": "", + "available": true, + "order": 30, + "symmetrical": true, + "tiny_label": null, + "inverse_relation": null, + "logical_relation": null + } + }, + { + "model": "archaeological_operations.siterelationtype", + "fields": { + "label": "Relation indirecte", + "txt_idx": "relation-indirecte", + "comment": "", + "available": true, + "order": 40, + "symmetrical": true, + "tiny_label": null, + "inverse_relation": null, + "logical_relation": null + } + }, + { + "model": "archaeological_operations.siterelationtype", + "fields": { + "label": "Parent de", + "txt_idx": "parent-de", + "comment": "", + "available": true, + "order": 10, + "symmetrical": false, + "tiny_label": null, + "inverse_relation": null, + "logical_relation": null + } + }, + { + "model": "archaeological_operations.siterelationtype", + "fields": { + "label": "Enfant de", + "txt_idx": "enfant-de", + "comment": "", + "available": true, + "order": 20, + "symmetrical": false, + "tiny_label": null, + "inverse_relation": null, + "logical_relation": null + } + }, + { + "model": "archaeological_operations.siterelationtype", + "fields": { + "label": "Ant\u00e9rieure \u00e0", + "txt_idx": "anterieure-a", + "comment": "", + "available": true, + "order": 50, + "symmetrical": false, + "tiny_label": null, + "inverse_relation": null, + "logical_relation": null + } + }, + { + "model": "archaeological_operations.siterelationtype", + "fields": { + "label": "Post\u00e9rieure \u00e0", + "txt_idx": "posterieure-a", + "comment": "", + "available": true, + "order": 60, + "symmetrical": false, + "tiny_label": null, + "inverse_relation": null, + "logical_relation": null + } + }, + { + "model": "archaeological_operations.siterelationtype", + "fields": { + "label": "Parent de", + "txt_idx": "parent-de", + "comment": "", + "available": true, + "order": 10, + "symmetrical": false, + "tiny_label": null, + "inverse_relation": [ + "enfant-de" + ], + "logical_relation": null + } + }, + { + "model": "archaeological_operations.siterelationtype", + "fields": { + "label": "Enfant de", + "txt_idx": "enfant-de", + "comment": "", + "available": true, + "order": 20, + "symmetrical": false, + "tiny_label": null, + "inverse_relation": [ + "parent-de" + ], + "logical_relation": null + } + }, + { + "model": "archaeological_operations.siterelationtype", + "fields": { + "label": "Ant\u00e9rieure \u00e0", + "txt_idx": "anterieure-a", + "comment": "", + "available": true, + "order": 50, + "symmetrical": false, + "tiny_label": null, + "inverse_relation": [ + "posterieure-a" + ], + "logical_relation": null + } + }, + { + "model": "archaeological_operations.siterelationtype", + "fields": { + "label": "Post\u00e9rieure \u00e0", + "txt_idx": "posterieure-a", + "comment": "", + "available": true, + "order": 60, + "symmetrical": false, + "tiny_label": null, + "inverse_relation": [ + "anterieure-a" + ], + "logical_relation": null + } + } +] diff --git a/archaeological_operations/migrations/0129_data_migration_heritageinterest_heritageenvprotection_relsite.py b/archaeological_operations/migrations/0129_data_migration_heritageinterest_heritageenvprotection_relsite.py new file mode 100644 index 000000000..59a884794 --- /dev/null +++ b/archaeological_operations/migrations/0129_data_migration_heritageinterest_heritageenvprotection_relsite.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.24 on 2024-02-10 12:07 + +import os + +from django.db import migrations +from django.core.management import call_command + + +def load_data(apps, __): + QualifiedBiographicalNoteType = apps.get_model("archaeological_operations", "HeritageInterestType") + if not QualifiedBiographicalNoteType.objects.count(): + json_path = os.sep.join(os.path.abspath(__file__).split(os.sep)[:-1] + ["0129_data_migration.json"]) + call_command("loaddata", json_path) + + +class Migration(migrations.Migration): + + dependencies = [ + ('archaeological_operations', '0128_add_editors'), + ] + + operations = [ + migrations.RunPython(load_data) + ] diff --git a/archaeological_operations/models.py b/archaeological_operations/models.py index 609f7b229..fc3a8a097 100644 --- a/archaeological_operations/models.py +++ b/archaeological_operations/models.py @@ -36,10 +36,15 @@ from django.db.models.signals import m2m_changed, post_save, post_delete, pre_de from django.forms import ValidationError from django.urls import reverse, reverse_lazy from ishtar_common.data_importer import post_importer_action -from ishtar_common.utils import gettext_lazy as _, pgettext_lazy, get_generated_id +from ishtar_common.utils import ( + gettext_lazy as _, pgettext_lazy, get_generated_id, + related_historization_changed +) from ishtar_common.models import ( Area, + Author, + BaseDating, BaseHistorizedItem, DashboardFormItem, Document, @@ -59,6 +64,8 @@ from ishtar_common.models import ( PersonType, post_delete_record_relation, post_save_cache, + QualifiedBiographicalNote, + RecordRelationManager, RelationItem, SourceType, Town, @@ -209,6 +216,32 @@ post_save.connect(post_save_cache, sender=CulturalAttributionType) post_delete.connect(post_save_cache, sender=CulturalAttributionType) +class HeritageInterestType(HierarchicalType): + order = models.IntegerField(_("Order"), default=10) + + class Meta: + verbose_name = _("Heritage interest type") + verbose_name_plural = _("Heritage interest types") + ordering = ("order", "label") + + +post_save.connect(post_save_cache, sender=HeritageInterestType) +post_delete.connect(post_save_cache, sender=HeritageInterestType) + + +class HeritageAndEnvironmentalProtectionType(HierarchicalType): + order = models.IntegerField(_("Order"), default=10) + + class Meta: + verbose_name = _("Heritage and environmental protection type") + verbose_name_plural = _("Heritage and environmental protection types") + ordering = ("order", "label") + + +post_save.connect(post_save_cache, sender=HeritageAndEnvironmentalProtectionType) +post_delete.connect(post_save_cache, sender=HeritageAndEnvironmentalProtectionType) + + class SiteType(OrderedHierarchicalType): class Meta: verbose_name = _("Site type") @@ -393,9 +426,12 @@ class ArchaeologicalSite( ("towns__areas__label", _("Area")), ("towns__areas__parent__label", _("Extended area")), ("discovery_status__label", _("Discovery status")), - ("current_status__label", _("Current status")), + ("current_states__label", _("Current states")), ("nature_of_site__label", _("Nature of site")), ("interpretation_level__label", _("Interpretation level")), + ("heritage_interests__label", _("Heritage interest")), + ("heritage_environmental_protections__label", + _("Heritage and environmental protections")), ("documents__source_type__label", _("Associated document type")), ("last_modified__FILTERyear", _("Modification (year)")), ] @@ -434,11 +470,13 @@ class ArchaeologicalSite( "cultural_attributions", "towns", "collaborators", - "types" + "types", + "heritage_interests", + "heritage_environmental_protections" ] - DATED_FIELDS = BaseHistorizedItem.DATED_FIELDS + ["sinking_date"] - NUMBER_FIELDS = GeographicTownItem.NUMBER_FIELDS[:] + DATED_FIELDS = BaseHistorizedItem.DATED_FIELDS + ["sinking_date", "protection_date"] + NUMBER_FIELDS = GeographicTownItem.NUMBER_FIELDS[:] + BaseDating.NUMBER_FIELDS EXTRA_REQUEST_KEYS = { "towns_label": "towns", @@ -448,6 +486,7 @@ class ArchaeologicalSite( "collaborators__pk": "collaborators__pk", # dynamic_table_documents "discoverer_id": "discoverer_id", # dynamic_table_documents "types__label": "types__label", + "editors__person_id": "editors__person_id", # dynamic_table_documents } # alternative names of fields for searches @@ -478,9 +517,37 @@ class ArchaeologicalSite( pgettext_lazy("key for text search", "discovery-status"), "discovery_status__label__iexact" ), - "current_status": SearchAltName( + "current_states": SearchAltName( pgettext_lazy("key for text search", "current-status"), - "current_status__label__iexact" + "current_states__label__iexact" + ), + "actors": SearchAltName( + pgettext_lazy("key for text search", "actors"), + "actors__cached_label__iexact" + ), + "collaborators": SearchAltName( + pgettext_lazy("key for text search", "collaborators"), + "collaborators__cached_label__iexact" + ), + "protection_id": SearchAltName( + pgettext_lazy("key for text search", "protection-id"), + "protection_id__iexact" + ), + "protection_date": SearchAltName( + pgettext_lazy("key for text search", "protection-date"), + "protection_date" + ), + "details_on_protection": SearchAltName( + pgettext_lazy("key for text search", "protection-details"), + "details_on_protection__iexact" + ), + "heritage_interest": SearchAltName( + pgettext_lazy("key for text search", "heritage-interest"), + "heritage_interests__label__iexact" + ), + "heritage_environmental_protections": SearchAltName( + pgettext_lazy("key for text search", "heritage-environmental-protection"), + "heritage_environmental_protections__label__iexact" ), "nature_of_site": SearchAltName( pgettext_lazy("key for text search", "nature"), @@ -568,6 +635,7 @@ class ArchaeologicalSite( ALT_NAMES.update(DocumentItem.ALT_NAMES) ALT_NAMES.update(GeoItem.ALT_NAMES) ALT_NAMES.update(Imported.ALT_NAMES) + ALT_NAMES.update(BaseDating.ALT_NAMES) DEFAULT_SEARCH_FORM = ("archaeological_operations.forms", "SiteSelect") @@ -580,13 +648,19 @@ class ArchaeologicalSite( RELATIVE_SESSION_NAMES = [ ("operation", "operations__pk"), ] - HISTORICAL_M2M = ["periods", "remains", "towns", "cultural_attributions", "types"] + HISTORICAL_M2M = [ + "datings", "periods", "remains", "towns", "cultural_attributions", "types", + "heritage_interests", "heritage_environmental_protections" + ] CACHED_LABELS = [ "cached_label", "cached_towns_label", "cached_periods", "cached_remains", "cached_types", + "cached_current_states", + "cached_heritage_interests", + "cached_heritage_environmental_protections" ] DOWN_MODEL_UPDATE = ["context_records"] @@ -653,17 +727,25 @@ class ArchaeologicalSite( Town, verbose_name=_("Towns"), related_name="sites", blank=True ) current_status = models.ForeignKey( + SiteCurrentStatusType, verbose_name=_("Current status - deprecated"), + on_delete=models.SET_NULL, blank=True, null=True, related_name="sites_deprecated" + ) + current_states = models.ManyToManyField( SiteCurrentStatusType, verbose_name=_("Current status"), - on_delete=models.SET_NULL, blank=True, null=True + blank=True ) discovery_status = models.ForeignKey( SiteDiscoveryStatusType, verbose_name=_("Discovery status"), on_delete=models.SET_NULL, blank=True, null=True ) - discoverer = models.ForeignKey(Person, verbose_name=_("Discoverer"), on_delete=models.SET_NULL, - blank=True, null=True, related_name="site_discovered") - nature_of_site = models.ForeignKey(NatureOfSiteType, verbose_name=_("Nature of site"), - on_delete=models.SET_NULL, blank=True, null=True) + discoverer = models.ForeignKey( + Person, verbose_name=_("Discoverer"), on_delete=models.SET_NULL, + blank=True, null=True, related_name="site_discovered" + ) + nature_of_site = models.ForeignKey( + NatureOfSiteType, verbose_name=_("Nature of site"), + on_delete=models.SET_NULL, blank=True, null=True + ) interpretation_level = models.ForeignKey( InterpretationLevelType, verbose_name=_("Interpretation level"), on_delete=models.SET_NULL, blank=True, null=True @@ -684,6 +766,18 @@ class ArchaeologicalSite( verbose_name=_("Collaborators"), related_name="site_collaborator", ) + # heritage + heritage_interests = models.ManyToManyField( + HeritageInterestType, blank=True, verbose_name=_("Heritage interests") + ) + protection_id = models.TextField(_("Protection ID"), blank=True, default="") + heritage_environmental_protections = models.ManyToManyField( + HeritageAndEnvironmentalProtectionType, blank=True, + verbose_name=_("Heritage and environmental protections") + ) + details_on_protection = models.TextField(_("Details on protection"), blank=True, + default="") + protection_date = models.DateField(_("Protection date"), null=True, blank=True) # underwater shipwreck_name = models.TextField(_("Shipwreck name"), blank=True, default="") oceanographic_service_localisation = models.TextField( @@ -699,6 +793,10 @@ class ArchaeologicalSite( _("DRASSM number"), max_length=100, null=True, blank=True ) + actors = models.ManyToManyField( + QualifiedBiographicalNote, related_name="sites", verbose_name=_("Actors"), + blank=True + ) documents = models.ManyToManyField( Document, related_name="sites", verbose_name=_("Documents"), blank=True ) @@ -734,6 +832,24 @@ class ArchaeologicalSite( default="", help_text=_("Generated automatically - do not edit"), ) + cached_current_states = models.TextField( + _("Cached current states label"), + blank=True, + default="", + help_text=_("Generated automatically - do not edit"), + ) + cached_heritage_interests = models.TextField( + _("Cached heritage interests label"), + blank=True, + default="", + help_text=_("Generated automatically - do not edit"), + ) + cached_heritage_environmental_protections = models.TextField( + _("Cached heritage and environmental protections label"), + blank=True, + default="", + help_text=_("Generated automatically - do not edit"), + ) history = HistoricalRecords(bases=[HistoryModel]) @@ -831,6 +947,26 @@ class ArchaeologicalSite( if can_edit_site and not is_locked: actions += [ ( + reverse("site-dating-add", args=[self.pk]), + _("Add dating"), + "fa fa-plus", + _("dating"), + "", + True, + ), + ] + actions += [ + ( + reverse("site-relations-modify", args=[self.pk]), + _("Modify sites relations"), + "fa fa-retweet", + _("sites"), + "", + True, + ), + ] + actions += [ + ( reverse("site-operation-relations-modify", args=[self.pk, window_id]), _("Modify site-operation relations"), "fa fa-retweet", @@ -953,22 +1089,23 @@ class ArchaeologicalSite( return self.towns_label() or "-" def _generate_cached_remains(self): - q = self.remains - if not self.remains.exists(): - return "-" - return " & ".join(list(q.values_list("label", flat=True))) + return self._regenerate_many_to_many_for_cache("remains") def _generate_cached_periods(self): - q = self.periods - if not self.periods.exists(): - return "-" - return " & ".join(list(q.values_list("label", flat=True))) + return self._regenerate_many_to_many_for_cache("periods") def _generate_cached_types(self): - q = self.types - if not self.types.exists(): - return "-" - return " & ".join(list(q.values_list("label", flat=True))) + return self._regenerate_many_to_many_for_cache("types") + + def _generate_cached_current_states(self): + return self._regenerate_many_to_many_for_cache("current_states") + + def _generate_cached_heritage_interests(self): + return self._regenerate_many_to_many_for_cache("heritage_interests") + + def _generate_cached_heritage_environmental_protections(self): + return self._regenerate_many_to_many_for_cache( + "heritage_environmental_protections") def natural_key(self): return (self.reference,) @@ -1067,10 +1204,66 @@ m2m_changed.connect(geodata_attached_changed, sender=ArchaeologicalSite.geodata. m2m_changed.connect(geotown_attached_changed, sender=ArchaeologicalSite.towns.through) +class SiteDating(BaseDating): + SERIALIZE_EXCLUDE = ["site"] + CURRENT_MODEL = ArchaeologicalSite + CURRENT_MODEL_ATTR = "site" + + site = models.ForeignKey( + ArchaeologicalSite, + verbose_name=_("Site"), + related_name="datings", + on_delete=models.CASCADE, + ) + + class Meta: + verbose_name = _("Site dating") + verbose_name_plural = _("Site datings") + + for attr in ArchaeologicalSite.HISTORICAL_M2M: - m2m_changed.connect( - m2m_historization_changed, sender=getattr(ArchaeologicalSite, attr).through + if attr == "datings": + model = SiteDating + post_save.connect(related_historization_changed, sender=SiteDating) + post_delete.connect(related_historization_changed, sender=SiteDating) + else: + m2m_changed.connect( + m2m_historization_changed, sender=getattr(ArchaeologicalSite, attr).through + ) + + +class SiteRelationType(GeneralRelationType): + class Meta: + verbose_name = _("Site relation type") + verbose_name_plural = _("Site relation types") + ordering = ("order", "label") + + +class SiteRecordRelations(GeneralRecordRelations): + MAIN_ATTR = "left_record" + left_record = models.ForeignKey( + ArchaeologicalSite, related_name="right_relations", on_delete=models.CASCADE + ) + right_record = models.ForeignKey( + ArchaeologicalSite, related_name="left_relations", on_delete=models.CASCADE ) + relation_type = models.ForeignKey(SiteRelationType, on_delete=models.PROTECT) + objects = RecordRelationManager() + + class Meta: + verbose_name = _("Site record relation") + verbose_name_plural = _("Site record relations") + ordering = ( + "left_record__cached_label", + "relation_type", + "right_record__cached_label", + ) + permissions = [ + ("view_siterelation", "Can view all Site relations"), + ] + + +post_delete.connect(post_delete_record_relation, sender=SiteRecordRelations) def get_values_town_related(item, prefix, values, filtr=None): @@ -1271,6 +1464,7 @@ class Operation( "collaborators__pk": "collaborators__pk", # dynamic_table_documents "cira_rapporteur__pk": "cira_rapporteur__pk", # dynamic_table_documents "operator__pk": "operator__pk", # dynamic_table_documents + "editors__person_id": "editors__person_id", # dynamic_table_documents } COL_LABELS = { @@ -2810,15 +3004,6 @@ class RelationType(GeneralRelationType): ordering = ("order", "label") -class OperationRecordRelationManager(models.Manager): - def get_by_natural_key(self, left_record, right_record, relation_type): - return self.get( - left_record__uuid=left_record, - right_record__uuid=right_record, - relation_type__txt_idx=relation_type, - ) - - class RecordRelations(GeneralRecordRelations): MAIN_ATTR = "left_record" left_record = models.ForeignKey( @@ -2828,7 +3013,7 @@ class RecordRelations(GeneralRecordRelations): Operation, related_name="left_relations", on_delete=models.CASCADE ) relation_type = models.ForeignKey(RelationType, on_delete=models.PROTECT) - objects = OperationRecordRelationManager() + objects = RecordRelationManager() class Meta: verbose_name = _("Operation record relation") @@ -2842,13 +3027,6 @@ class RecordRelations(GeneralRecordRelations): ("view_operationrelation", "Can view all Operation relations"), ] - def natural_key(self): - return ( - self.left_record.uuid, - self.right_record.uuid, - self.relation_type.txt_idx, - ) - post_delete.connect(post_delete_record_relation, sender=RecordRelations) @@ -3004,6 +3182,7 @@ class AdministrativeAct(DocumentItem, BaseHistorizedItem, OwnPerms, ValueGetter, ), "signature_date": "signature_date", "year": "signature_date__year", + "editors__person_id": "editors__person_id", # dynamic_table_documents } REVERSED_BOOL_FIELDS = [ "index__isnull", diff --git a/archaeological_operations/templates/ishtar/sheet_operation.html b/archaeological_operations/templates/ishtar/sheet_operation.html index e290ce319..fced5457a 100644 --- a/archaeological_operations/templates/ishtar/sheet_operation.html +++ b/archaeological_operations/templates/ishtar/sheet_operation.html @@ -410,28 +410,9 @@ {% if display_relations %} <div class="tab-pane fade" id="{{window_id}}-relations" role="tabpanel" aria-labelledby="{{window_id}}-relations-tab"> - - {% if item.right_relations.count and not item.right_relations_not_available %} - <h3>{% trans "Relations"%}</h3> - {% for rel in item.right_relations.all %} - {% ifchanged rel.relation_type %} - {% if forloop.counter0 %}</div>{% endif %} - <h4>{{rel.relation_type}}</h4> - <div class="row">{% endifchanged %} - <div class="col-12"> - <a href="#" onclick="load_window('/show-operation/{{rel.right_record.pk|unlocalize}}/');" class="display_details"> - <i class="fa fa-info-circle" aria-hidden="true"></i> - </a> {{rel.right_record}} - </div> - {% if forloop.last %} - </div>{% endif %} - {% endfor %} - {% else %} - <div class="alert alert-info" role="alert"> - <i class="fa fa-info-circle" aria-hidden="true"></i> - {% trans "No relations" %} - </div> - {% endif %} + {% with relation_url="/show-operation/" %} + {% include "ishtar/blocks/sheet_relations.html" %} + {% endwith %} </div> {% endif %} diff --git a/archaeological_operations/templates/ishtar/sheet_site.html b/archaeological_operations/templates/ishtar/sheet_site.html index 2deb54683..b153e5795 100644 --- a/archaeological_operations/templates/ishtar/sheet_site.html +++ b/archaeological_operations/templates/ishtar/sheet_site.html @@ -1,5 +1,5 @@ {% extends "ishtar/sheet.html" %} -{% load i18n ishtar_helpers window_tables window_header window_ope_tables window_field from_dict %} +{% load i18n l10n ishtar_helpers window_tables window_header window_ope_tables window_field from_dict %} {% block head_title %}<strong>{{SITE_LABEL}}</strong> - {{item.cached_label}}{% endblock %} @@ -8,6 +8,12 @@ {% endblock %} {% block content %} +{# trick to set to null non existing variable #} +{% with permission_view_own_archaeologicalsite=permission_view_own_archaeologicalsite %} + +{% with display_relations=item|safe_or:"right_relations.count|left_relations.count"|safe_and_not:"right_relations_not_available"|safe_and_not:"left_relations_not_available" %} +{% with can_change=permission_change_own_archaeologicalsite|or_:permission_change_archaeologicalsite %} +{% with dating_list=item|m2m_listing:"datings" %} {% with permission_change_own_geovectordata=permission_change_own_geovectordata %} {% with permission_change_geovectordata=permission_change_geovectordata %} @@ -40,16 +46,9 @@ <h3>{% trans "General"%}</h3> <div class="row"> - {% field_flex "Other reference" item.other_reference %} - {% field_flex_multiple_obj _("Types") item 'types' %} - {% field_flex _("Discovery status") item.discovery_status %} - {% field_flex _("Current status") item.current_status %} - {% field_flex _("Nature of site") item.nature_of_site %} - {% field_flex _("Interpretation level") item.interpretation_level %} - {% field_flex_multiple_obj "Periods" item 'periods' %} - {% field_flex_multiple_obj "Remains" item 'remains' %} - {% field_flex_multiple_obj "Cultural attributions" item 'cultural_attributions' %} - {% field_flex_detail _("Discoverer") item.discoverer %} + {% field_flex _("Other reference") item.other_reference %} + {% field_flex_multiple_obj _("Heritage interest") item 'heritage_interests' %} + {% field_flex_detail_multiple _("Actors") item.actors %} {% field_flex_detail_multiple _("Collaborators") item.collaborators %} {% if item.description == item.public_description %} {% field_flex_full _("Description/Public description") item.description "<pre>" "</pre>" %} @@ -57,8 +56,38 @@ {% field_flex_full _("Description") item.description "<pre>" "</pre>" %} {% field_flex_full _("Public description") item.public_description "<pre>" "</pre>" %} {% endif %} - {% field_flex_full "Comment" item.comment "<pre>" "</pre>" %} + {% field_flex_full _("Comment") item.comment "<pre>" "</pre>" %} +</div> +{% if item.cached_periods or dating_list %} + <h3>{% trans "Periods / Datings" %}</h3> + {% field_flex_multiple_obj _("Periods") item 'periods' %} + {% with url_dating="site-dating" %} + {% include "ishtar/blocks/sheet_dating_list.html" %} + {% endwith %} +{% endif %} +{% if item.cached_types or item.nature_of_site or item.interpretation_level or item.discovery_status or item.cached_current_states or item.cached_periods or item.cached_remains or item.cultural_attributions.count or item.discoverer %} +<h3>{% trans "Scientific" %}</h3> +<div class="row"> + {% field_flex_multiple_obj _("Types") item 'types' %} + {% field_flex _("Nature of site") item.nature_of_site %} + {% field_flex _("Interpretation level") item.interpretation_level %} + {% field_flex _("Discovery status") item.discovery_status %} + {% field_flex_multiple_obj _("Current states") item 'current_states' %} + {% field_flex_multiple_obj _("Periods") item 'periods' %} + {% field_flex_multiple_obj _("Remains") item 'remains' %} + {% field_flex_multiple_obj _("Cultural attributions") item 'cultural_attributions' %} + {% field_flex_detail _("Discoverer") item.discoverer %} </div> +{% endif %} +{% if item.cached_heritage_environmental_protections or item.details_on_protection or item.protection_id or item.protection_date %} +<h3>{% trans "Protection" %}</h3> +<div class="row"> + {% field_flex _("Protection ID") item.protection_id %} + {% field_flex _("Protection date") item.protection_date|date:"DATE_FORMAT" %} + {% field_flex_multiple_obj _("Heritage and environmental protections") item 'heritage_environmental_protections' %} + {% field_flex_full _("Details on protection") item.details_on_protection "<pre>" "</pre>" %} +</div> +{% endif %} {% if item.affmar_number or item.drassm_number or item.oceanographic_service_localisation or item.shipwreck_code or item.sinking_date or item.discovery_area or item.shipwreck_name %} <h3>{% trans "Underwater"%}</h3> @@ -122,13 +151,19 @@ {% endif %} {% endif %} +{% if display_relations %} + {% with relation_url="/show-site/" %} + {% include "ishtar/blocks/sheet_relations.html" %} + {% endwith %} +{% endif %} + {% if not is_external and SHOW_GEO %} <h3>{% trans "Geographic data" %}</h3> {% with geo_item=item %}{% include "ishtar/blocks/sheet_geographic.html" %}{% endwith %} {% endif %} {% if not is_external %} -{% if item.history_creator or item.last_edition_date or item.created %} +{% if item.history_creator or item.last_edition_date or item.created or item.editors.count %} <h3>{% trans "Sheet"%}</h3> <div class="row"> {% include "ishtar/blocks/sheet_creation_section.html" %} @@ -136,5 +171,5 @@ {% endif %} {% endif %} -{% endwith %} {% endwith %} {% endwith %} +{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %} {% endblock %} diff --git a/archaeological_operations/tests.py b/archaeological_operations/tests.py index b239b789d..136f710f1 100644 --- a/archaeological_operations/tests.py +++ b/archaeological_operations/tests.py @@ -127,7 +127,7 @@ class FileInit: def create_file(self): self.extra_models, self.model_list = {}, [] self.user, created = User.objects.get_or_create( - username="username", is_superuser=True + username="username", is_superuser=True, is_staff=True ) self.user.set_password("tralala") self.user.save() @@ -298,7 +298,7 @@ class ImportTest(BaseImportTest): impt = form.save(self.ishtar_user) impt.initialize() self.init_ope_targetkey(imp=impt) - impt.importation() + impt.start_import() def init_parcel_import(self): self.init_ope() @@ -327,7 +327,7 @@ class ImportTest(BaseImportTest): importer, form = self.init_parcel_import() impt = form.save(self.ishtar_user) impt.initialize() - impt.importation() + impt.start_import() def init_context_record_import(self): self.init_parcel() @@ -364,7 +364,7 @@ class ImportTest(BaseImportTest): impt = form.save(self.ishtar_user) impt.initialize() self.init_cr_targetkey(impt) - impt.importation() + impt.start_import() class ImportOperationTest(ImportTest, TestCase): @@ -385,14 +385,14 @@ class ImportOperationTest(ImportTest, TestCase): self.assertTrue(TargetKey.objects.count() > target_key_nb) # first try to import - impt.importation() + impt.start_import() current_ope_nb = models.Operation.objects.count() # no new operation imported because of a missing connection for # operation_type value self.assertEqual(current_ope_nb, first_ope_nb) self.init_ope_targetkey(imp=impt) - impt.importation() + impt.start_import() # new operations have now been imported current_ope_nb = models.Operation.objects.count() self.assertEqual(current_ope_nb, first_ope_nb + 2) @@ -413,7 +413,7 @@ class ImportOperationTest(ImportTest, TestCase): # a second importation will be not possible: no two same patriarche # code - impt.importation() + impt.start_import() self.assertEqual(last_ope, models.Operation.objects.order_by("-pk").all()[0]) def test_import_bad_encoding(self): @@ -428,7 +428,7 @@ class ImportOperationTest(ImportTest, TestCase): impt = form.save(self.ishtar_user) impt.initialize() self.init_ope_targetkey(imp=impt) - impt.importation() + impt.start_import() # new operations have now been imported current_ope_nb = models.Operation.objects.count() self.assertEqual(current_ope_nb, first_ope_nb + 2) @@ -457,7 +457,7 @@ class ImportOperationTest(ImportTest, TestCase): impt = form.save(self.ishtar_user) impt.initialize() self.init_ope_targetkey(imp=impt) - impt.importation() + impt.start_import() current_ope_nb = models.Operation.objects.count() self.assertEqual(current_ope_nb, first_ope_nb + 2) @@ -481,7 +481,7 @@ class ImportOperationTest(ImportTest, TestCase): impt = form.save(self.ishtar_user) impt.initialize() self.init_ope_targetkey(imp=impt) - impt.importation() + impt.start_import() self.assertEqual( models.Operation.objects.filter( code_patriarche="4201", @@ -523,7 +523,7 @@ class ImportOperationTest(ImportTest, TestCase): impt = form.save(self.ishtar_user) impt.initialize() self.init_ope_targetkey(imp=impt) - impt.importation() + impt.start_import() self.assertEqual( models.Operation.objects.filter( code_patriarche="4201", @@ -542,7 +542,7 @@ class ImportOperationTest(ImportTest, TestCase): impt = form.save(self.ishtar_user) impt.initialize() self.init_ope_targetkey(imp=impt) - impt.importation() + impt.start_import() self.assertEqual( models.Operation.objects.filter(code_patriarche="oa-4201").count(), 1 ) @@ -559,7 +559,7 @@ class ImportOperationTest(ImportTest, TestCase): impt = form.save(self.ishtar_user) impt.initialize() self.init_ope_targetkey(imp=impt) - impt.importation() + impt.start_import() self.assertEqual( models.Operation.objects.filter(code_patriarche="oa-42014201").count(), 1 ) @@ -595,7 +595,7 @@ class ImportOperationTest(ImportTest, TestCase): tg.associated_import = other_imp tg.save() - impt.importation() + impt.start_import() current_ope_nb = models.Operation.objects.count() # no new operation self.assertEqual(current_ope_nb, init_ope_number) @@ -615,7 +615,7 @@ class ImportOperationTest(ImportTest, TestCase): impt = form.save(self.ishtar_user) impt.initialize() self.init_ope_targetkey(imp=impt) - impt.importation() + impt.start_import() self.assertEqual(len(impt.errors), 2) self.assertTrue("cody" in impt.errors[0]["error"]) self.assertTrue( @@ -632,7 +632,7 @@ class ImportOperationTest(ImportTest, TestCase): # no model defined in created_models: normal import init_ope_number = models.Operation.objects.count() - impt.importation() + impt.start_import() current_ope_nb = models.Operation.objects.count() self.assertEqual(current_ope_nb, init_ope_number + 2) @@ -650,7 +650,7 @@ class ImportOperationTest(ImportTest, TestCase): self.init_ope_targetkey(imp=impt) # no imports - impt.importation() + impt.start_import() current_ope_nb = models.Operation.objects.count() self.assertEqual(current_ope_nb, init_ope_number) @@ -667,7 +667,7 @@ class ImportOperationTest(ImportTest, TestCase): self.init_ope_targetkey(imp=impt) # import of operations - impt.importation() + impt.start_import() current_ope_nb = models.Operation.objects.count() self.assertEqual(current_ope_nb, init_ope_number + 2) @@ -703,7 +703,7 @@ class ImportOperationTest(ImportTest, TestCase): mcc_parcel, form = self.init_parcel_import() impt = form.save(self.ishtar_user) impt.initialize() - impt.importation() + impt.start_import() # new parcels has now been imported current_nb = models.Parcel.objects.count() self.assertEqual(current_nb, old_nb + 3) @@ -752,7 +752,7 @@ class ImportOperationTest(ImportTest, TestCase): impt = form.save(self.ishtar_user) impt.initialize() self.init_ope_targetkey(imp=impt) - impt.importation() + impt.start_import() ope1 = models.Operation.objects.get(code_patriarche="4200") self.assertEqual(ope1.data, {"autre_refs": {"arbitraire": 789}}) ope2 = models.Operation.objects.get(code_patriarche="4201") @@ -767,7 +767,7 @@ class ImportOperationTest(ImportTest, TestCase): impt = form.save(self.ishtar_user) impt.initialize() self.init_ope_targetkey(imp=impt) - impt.importation() + impt.start_import() ope1 = models.Operation.objects.get(code_patriarche="4200") self.assertEqual(ope1.data, {"autre_refs": {"arbitraire": 789}, "autre": 666}) ope2 = models.Operation.objects.get(code_patriarche="4201") @@ -790,7 +790,7 @@ class ImportOperationTest(ImportTest, TestCase): impt = form.save(self.ishtar_user) impt.initialize() self.init_ope_targetkey(imp=impt) - impt.importation() + impt.start_import() ope1 = models.Operation.objects.get(code_patriarche="4200") self.assertEqual(ope1.data, {"code_insee": "45123"}) ope2 = models.Operation.objects.get(code_patriarche="4201") @@ -858,7 +858,7 @@ class ImportDocumentTest(ImportTest, TestCase): importer, form = self.init_doc_import() self.assertTrue(form.is_valid()) impt = form.save(self.ishtar_user) - impt.importation() + impt.start_import() self.assertEqual(doc_nb + 2, Document.objects.count()) self.assertEqual(current_index + 2, Document.get_next_index() - 1) diff --git a/archaeological_operations/urls.py b/archaeological_operations/urls.py index 3082418d9..31930fbd9 100644 --- a/archaeological_operations/urls.py +++ b/archaeological_operations/urls.py @@ -352,6 +352,14 @@ urlpatterns = [ name="operation-site-relations-modify", ), path( + "site-relations-modify/<int:pk>)/", + check_permissions( + ["archaeological_operations.change_archaeologicalsite", + "archaeological_operations.change_own_archaeologicalsite"] + )(views.site_modify_relations), + name="site-relations-modify", + ), + path( "site-operation-relations-modify/<int:pk>/", check_permissions( ["archaeological_operations.change_operation", @@ -424,6 +432,33 @@ urlpatterns = [ name="site-qa-link", kwargs={"model": models.ArchaeologicalSite, "url": "site-qa-link"}, ), + path( + "site-dating/<int:pk>/", + check_permissions( + ["archaeological_sites.change_archaeologicalsite", + "archaeological_sites.change_own_archaeologicalsite"])( + views.site_dating_add + ), + name="site-dating-add", + ), + path( + "site-dating/<int:pk>/<int:dating_pk>/", + check_permissions( + ["archaeological_sites.change_archaeologicalsite", + "archaeological_sites.change_own_archaeologicalsite"])( + views.site_dating_modify + ), + name="site-dating-modify", + ), + path( + "site-dating-delete/<int:dating_pk>/", + check_permissions( + ["archaeological_sites.change_archaeologicalsite", + "archaeological_sites.change_own_archaeologicalsite"])( + views.site_dating_delete + ), + name="site-dating-delete", + ), re_path( r"^site-qa-bulk-update/(?P<pks>[0-9-]+)?/$", check_permissions( diff --git a/archaeological_operations/views.py b/archaeological_operations/views.py index ecba76e05..f2117c747 100644 --- a/archaeological_operations/views.py +++ b/archaeological_operations/views.py @@ -43,7 +43,10 @@ from ishtar_common.models import ( ) from archaeological_context_records.models import ContextRecord from ishtar_common.utils import check_permissions_condition + from ishtar_common.views import ( + get_dating_delete, + get_dating_form, gen_generate_doc, QAItemEditForm, QABaseLockView, @@ -590,6 +593,12 @@ operation_modify_relations = get_relation_modify( ) +site_modify_relations = get_relation_modify( + models.ArchaeologicalSite, models.SiteRecordRelations, + forms.SiteRecordRelationsFormSet, "site-relations-modify" +) + + RELATION_LIMIT = 50 @@ -662,8 +671,27 @@ def operation_site_modify(model, related_model, related_key, formset_class, url_ return view -operation_site_modify_relations = operation_site_modify(models.Operation, models.ArchaeologicalSite, "archaeological_sites", forms.OpeSiteRelationsFormSet, "operation-site-relations-modify") -site_operation_modify_relations = operation_site_modify(models.ArchaeologicalSite, models.Operation, "operations", forms.SiteOpeRelationsFormSet, "site-operation-relations-modify") +operation_site_modify_relations = operation_site_modify( + models.Operation, models.ArchaeologicalSite, "archaeological_sites", + forms.OpeSiteRelationsFormSet, "operation-site-relations-modify") +site_operation_modify_relations = operation_site_modify( + models.ArchaeologicalSite, models.Operation, "operations", + forms.SiteOpeRelationsFormSet, "site-operation-relations-modify") + + +site_dating_add = get_dating_form( + models.ArchaeologicalSite, models.SiteDating, "site-dating-add" +) + + +site_dating_modify = get_dating_form( + models.ArchaeologicalSite, models.SiteDating, "site-dating-modify" +) + + +site_dating_delete = get_dating_delete( + models.ArchaeologicalSite, models.SiteDating, "site-dating-delete" +) # archaeological sites |
