diff options
Diffstat (limited to 'archaeological_finds')
17 files changed, 922 insertions, 70 deletions
diff --git a/archaeological_finds/admin.py b/archaeological_finds/admin.py index 555ef8821..c764cf66d 100644 --- a/archaeological_finds/admin.py +++ b/archaeological_finds/admin.py @@ -20,7 +20,9 @@ from django.contrib import admin from ishtar_common.apps import admin_site -from ishtar_common.admin import HistorizedObjectAdmin, GeneralTypeAdmin, MainGeoDataItem +from ishtar_common.admin import GeneralTypeAdmin, HistorizedObjectAdmin, MainGeoDataItem +from archaeological_operations.admin import RecordRelationsAdmin, RelationTypeAdmin + from . import models @@ -242,6 +244,10 @@ class TreatmentStatusAdmin(GeneralTypeAdmin): extra_list_display = ["order"] +admin_site.register(models.FindRelationType, RelationTypeAdmin) +admin_site.register(models.FindRecordRelations, RecordRelationsAdmin) + + @admin.register(models.Exhibition, site=admin_site) class ExhibitionAdmin(HistorizedObjectAdmin): list_display = ('name', 'year', 'reference', 'exhibition_type') @@ -269,12 +275,12 @@ class RecommendedTreatmentTypeAdmin(GeneralTypeAdmin): general_models = [ models.AlterationCauseType, models.AlterationType, models.BatchType, - models.CollectionEntryModeType, models.IntegrityType, models.InventoryConformity, - models.InventoryMarkingPresence, models.MarkingType, models.MaterialTypeQualityType, - models.MuseumCollection, models.ObjectTypeQualityType, models.OriginalReproduction, - models.RemarkabilityType, models.TreatmentEmergencyType, models.DiscoveryMethod, - models.ExhibitionType, models.OwnershipStatus, models.FollowUpActionType, - models.StatementConditionType + models.CollectionEntryModeType, models.IconographicPatternType, models.IntegrityType, + models.InventoryConformity, models.InventoryMarkingPresence, models.ListedBuildingProtectionNature, + models.MarkingType, models.MaterialTypeQualityType, models.MuseumCollection, + models.ObjectTypeQualityType, models.OriginalReproduction, models.RemarkabilityType, + models.TreatmentEmergencyType, models.DiscoveryMethod, models.ExhibitionType, + models.OwnershipStatus, models.FollowUpActionType, models.StatementConditionType ] for model in general_models: diff --git a/archaeological_finds/forms.py b/archaeological_finds/forms.py index d4f954d59..6ea406b9e 100644 --- a/archaeological_finds/forms.py +++ b/archaeological_finds/forms.py @@ -35,7 +35,6 @@ from ishtar_common.utils import gettext_lazy as _ from . import models from archaeological_operations.models import CulturalAttributionType, RemainType from archaeological_context_records.models import ( - Dating, DatingType, DatingQuality, ContextRecord, @@ -92,10 +91,15 @@ from ishtar_common.forms import ( GeoItemSelect, ) from ishtar_common.forms_common import get_town_field -from archaeological_context_records.forms import PeriodSelect +from archaeological_operations.forms import ( + RecordRelationsForm, + RecordRelationsFormSetBase, +) +from archaeological_context_records.forms import DatingSelect from ishtar_common.models import ( Area, + Author, BiographicalNote, get_current_profile, IshtarUser, @@ -103,6 +107,7 @@ from ishtar_common.models import ( Organization, Person, person_type_pks_lazy, + QualifiedBiographicalNote, valid_id, valid_ids, ) @@ -202,8 +207,6 @@ class RecordFormSelection(CustomForm, forms.Form): class MuseumForm: - MARK_FIELD = "mark" - def update_museum_fields(self): """ Overload fields labels for museum @@ -222,10 +225,6 @@ class MuseumForm: fields["museum_id"] = self.fields.pop("museum_id") fields.update(self.fields) self.fields = fields - # update label of mark field - if self.MARK_FIELD not in self.fields: - return - self.fields[self.MARK_FIELD].label = _("Marking details") class BasicFindForm(MuseumForm, CustomForm, ManageOldType): @@ -238,6 +237,8 @@ class BasicFindForm(MuseumForm, CustomForm, ManageOldType): form_admin_name = _("Simple find - 020 - General") form_slug = "find-020-simplegeneral" base_models = [ + "actor", + "editor", "object_type", "period", "material_type", @@ -246,6 +247,7 @@ class BasicFindForm(MuseumForm, CustomForm, ManageOldType): "functional_area", "technical_area", "technical_processe", + "iconographic_pattern", "museum_former_collection", "museum_donor", "museum_inventory_marking_presence", @@ -253,6 +255,8 @@ class BasicFindForm(MuseumForm, CustomForm, ManageOldType): "museum_collection", ] associated_models = { + "actor": QualifiedBiographicalNote, + "editor": Author, "material_type": models.MaterialType, "cultural_attribution": CulturalAttributionType, "object_type": models.ObjectType, @@ -264,6 +268,8 @@ class BasicFindForm(MuseumForm, CustomForm, ManageOldType): "material_type_quality": models.MaterialTypeQualityType, "object_type_quality": models.ObjectTypeQualityType, "checked_type": models.CheckedType, + "iconographic_pattern": models.IconographicPatternType, + "listed_building_protection_nature": models.ListedBuildingProtectionNature, "museum_collection_entry_mode": models.CollectionEntryModeType, "museum_inventory_marking_presence": models.InventoryMarkingPresence, "museum_marking_type": models.MarkingType, @@ -283,11 +289,13 @@ class BasicFindForm(MuseumForm, CustomForm, ManageOldType): "denomination", "previous_id", "laboratory_id", + "title", "museum_id", "museum_id_comment", "seal_number", "museum_inventory_marking_presence", "museum_marking_type", + "mark_text", "mark", "museum_owner_institution", "museum_assigned_institution", @@ -308,6 +316,8 @@ class BasicFindForm(MuseumForm, CustomForm, ManageOldType): "museum_original_repro", "museum_allocation_date", "museum_purchase_price", + "iconographic_pattern", + "iconography_notes", "description", "public_description", "get_first_base_find__discovery_method", @@ -333,6 +343,7 @@ class BasicFindForm(MuseumForm, CustomForm, ManageOldType): "cultural_attribution", "period", "dating_comment", + "actor", "length", "width", "height", @@ -349,6 +360,8 @@ class BasicFindForm(MuseumForm, CustomForm, ManageOldType): "check_date", ] extra_form_modals = [ + "author", + "qualifiedbiographicalnote", "biographicalnote", "person", "organization", @@ -388,14 +401,14 @@ class BasicFindForm(MuseumForm, CustomForm, ManageOldType): museum_id_prefix = forms.CharField(label=_("Museum ID prefix"), required=False) museum_id = forms.CharField(label=_("Museum inventory number"), required=False) museum_id_suffix = forms.CharField(label=_("Museum ID suffix"), required=False) - museum_id_comment = forms.CharField(label=_("Comment on museum ID"), widget=forms.Textarea, required=False) + museum_id_comment = forms.CharField(label=_("Comment on museum ID"), + widget=forms.Textarea, required=False) HEADERS["label"] = FormHeader(_("Identification")) - label = forms.CharField( - label=_("Free ID"), validators=[validators.MaxLengthValidator(60)] - ) + label = forms.CharField(label=_("Free ID")) denomination = forms.CharField(label=_("Denomination"), required=False) previous_id = forms.CharField(label=_("Previous ID"), required=False) laboratory_id = forms.CharField(label=_("Laboratory ID"), required=False) + title = forms.CharField(label=_("Title"), required=False) seal_number = forms.CharField(label=_("Seal number"), required=False) museum_inventory_marking_presence = widgets.Select2MultipleField( label=_("Presence of inventory marking"), required=False @@ -403,7 +416,8 @@ class BasicFindForm(MuseumForm, CustomForm, ManageOldType): museum_marking_type = widgets.Select2MultipleField( label=_("Type of marking"), required=False ) - mark = forms.CharField(label=_("Mark"), required=False) + mark_text = forms.CharField(label=_("Transcription of the marking"), required=False) + mark = forms.CharField(label=_("Marking details"), required=False) HEADERS["ownership_status"] = FormHeader(_("Ownership")) ownership_status = forms.ChoiceField( label=_("Ownership status"), required=False, choices=[] @@ -451,6 +465,12 @@ class BasicFindForm(MuseumForm, CustomForm, ManageOldType): ) museum_allocation_date = DateField(label=_("Date of museum allocation"), required=False) museum_purchase_price = forms.CharField(label=_("Purchase price"), required=False) + iconographic_pattern = widgets.Select2MultipleField( + label=_("Iconographic patterns"), required=False, + ) + iconography_notes = forms.CharField( + label=_("Iconography notes"), widget=forms.Textarea, required=False + ) HEADERS["museum_inventory_transcript"] = FormHeader(_("Description")) museum_inventory_transcript = forms.CharField( @@ -528,6 +548,9 @@ class BasicFindForm(MuseumForm, CustomForm, ManageOldType): dating_comment = forms.CharField( label=_("Comment on datings"), required=False, widget=forms.Textarea ) + actor = widgets.Select2MultipleField( + model=QualifiedBiographicalNote, label=_("Actors"), required=False, + remote=True, new=True, remote_filter='qualification_type__F-A') HEADERS["length"] = FormHeader(_("Dimensions")) length = FloatField( @@ -573,9 +596,26 @@ class BasicFindForm(MuseumForm, CustomForm, ManageOldType): label=_("Dimensions comment"), required=False, widget=forms.Textarea ) + HEADERS["listed_building_id"] = FormHeader(_("Listed buildings")) + listed_building_id = forms.CharField(label=_("Listed building ID"), required=False) + listed_building_protection_nature = forms.ChoiceField( + label=_("Nature of listed buildings protection"), choices=[], + required=False + ) + listed_building_date = DateField( + label=_("Date of listing as a listed building"), + required=False) + listed_building_notes = forms.CharField( + label=_("Notes on listed building"), required=False, + widget=forms.Textarea) + HEADERS["checked_type"] = FormHeader(_("Sheet")) checked_type = forms.ChoiceField(label=_("Check"), required=False) check_date = DateField(initial=get_now, label=_("Check date")) + editor = widgets.Select2MultipleField( + label=_("Editors"), required=False, + model=Author, remote=True, new=True + ) TYPES = [ FieldType( @@ -600,8 +640,13 @@ class BasicFindForm(MuseumForm, CustomForm, ManageOldType): FieldType("technical_processe", models.TechnicalProcessType, is_multiple=True), FieldType("communicabilitie", models.CommunicabilityType, is_multiple=True), FieldType("checked_type", models.CheckedType, is_multiple=True), + FieldType("iconographic_pattern", models.IconographicPatternType, + is_multiple=True), + FieldType("listed_building_protection_nature", + models.ListedBuildingProtectionNature), FieldType("museum_collection_entry_mode", models.CollectionEntryModeType), - FieldType("museum_inventory_marking_presence", models.InventoryMarkingPresence, is_multiple=True), + FieldType("museum_inventory_marking_presence", + models.InventoryMarkingPresence, is_multiple=True), FieldType("museum_marking_type", models.MarkingType, is_multiple=True), FieldType("museum_collection", models.MuseumCollection), FieldType("museum_inventory_conformity", models.InventoryConformity), @@ -663,11 +708,13 @@ class FindForm(BasicFindForm): "denomination", "previous_id", "get_first_base_find__excavation_id", + "title", "laboratory_id", "museum_id", "seal_number", "museum_inventory_marking_presence", "museum_marking_type", + "mark_text", "mark", "ownership_status", "owner", @@ -687,8 +734,10 @@ class FindForm(BasicFindForm): "museum_inventory_conformity", "museum_conformity_comment", "museum_original_repro", - "museum_allocation_date", "museum_purchase_price", + "iconographic_pattern", + "iconography_notes", + "museum_allocation_date", "museum_inventory_transcript", "description", "public_description", @@ -719,6 +768,7 @@ class FindForm(BasicFindForm): "comment", "period", "dating_comment", + "actor", "length", "width", "height", @@ -731,8 +781,13 @@ class FindForm(BasicFindForm): "clutter_short_side", "clutter_height", "dimensions_comment", + "listed_building_id", + "listed_building_protection_nature", + "listed_building_date", + "listed_building_notes", "checked_type", "check_date", + "editor", ] HEADERS = BasicFindForm.HEADERS.copy() @@ -925,6 +980,32 @@ class ResultingFindsForm(CustomForm, ManageOldType): return self.cleaned_data +class FindRecordRelationsForm(RecordRelationsForm): + current_model = models.FindRelationType + current_related_model = models.Find + associated_models = { + "right_record": models.Find, + "relation_type": models.FindRelationType, + } + ERROR_MISSING = _("You should select a find and a relation type.") + ERROR_SAME = _("A find cannot be related to himself.") + + right_record = forms.IntegerField( + label=_("Find"), + widget=widgets.JQueryAutoComplete( + reverse_lazy('autocomplete-find'), + associated_model=models.Find), + validators=[valid_id(models.Find)], required=False) + + +FindRecordRelationsFormSet = formset_factory( + FindRecordRelationsForm, can_delete=True, formset=RecordRelationsFormSetBase +) +FindRecordRelationsFormSet.form_label = _("Find - Relations") +FindRecordRelationsFormSet.form_admin_name = _("Find - Relations") +FindRecordRelationsFormSet.form_slug = "find-recordrelations" + + class QAFindFormMulti(MuseumForm, QAForm): form_admin_name = _("Find - Quick action - Modify") form_slug = "find-quickaction-modify" @@ -1069,7 +1150,7 @@ class QAFindFormMulti(MuseumForm, QAForm): qa_museum_id_suffix = forms.CharField(label=_("Museum ID suffix"), required=False) qa_laboratory_id = forms.CharField(label=_("Laboratory ID"), required=False) qa_seal_number = forms.CharField(label=_("Seal number"), required=False) - qa_mark = forms.CharField(label=_("Mark"), required=False) + qa_mark = forms.CharField(label=_("Marking details"), required=False) ## Ownership qa_ownership_status = forms.ChoiceField(label=_("Ownership status"), @@ -1543,6 +1624,10 @@ class PreservationForm(CustomForm, ManageOldType): widget=widgets.Select2Multiple, required=False, ) + conservatory_states_details = forms.CharField( + label=_("Conservatory state details"), required=False, + widget=forms.Textarea + ) alteration = forms.MultipleChoiceField( label=_("Alteration"), choices=[], @@ -1616,7 +1701,7 @@ class DateForm(ManageOldType, forms.Form): ] -class FindSelect(MuseumForm, GeoItemSelect, PeriodSelect): +class FindSelect(MuseumForm, GeoItemSelect, DatingSelect): _model = models.Find form_admin_name = _("Find - 001 - Search") form_slug = "find-001-search" @@ -1714,6 +1799,9 @@ class FindSelect(MuseumForm, GeoItemSelect, PeriodSelect): "museum": [ "cache_complete_museum_id", "museum_id_comment", + "museum_id", + "museum_id_prefix", + "museum_id_suffix", "museum_owner_institution", "museum_assigned_institution", "museum_custodian_institution", @@ -1746,7 +1834,10 @@ class FindSelect(MuseumForm, GeoItemSelect, PeriodSelect): ) label = forms.CharField(label=_("Free ID")) denomination = forms.CharField(label=_("Denomination")) + title = forms.CharField(label=_("Title")) + museum_id_prefix = forms.CharField(label=_("Museum ID prefix")) museum_id = forms.CharField(label=_("Museum inventory number")) + museum_id_suffix = forms.CharField(label=_("Museum ID suffix")) cache_complete_museum_id = forms.CharField(label=_("Complete museum ID")) previous_id = forms.CharField(label=_("Previous ID")) base_finds__excavation_id = forms.CharField(label=_("Excavation ID")) @@ -1911,6 +2002,10 @@ class FindSelect(MuseumForm, GeoItemSelect, PeriodSelect): label=_("Periods"), choices=[], required=False ) dating_comment = forms.CharField(label=_("Comment on datings")) + actors = forms.IntegerField( + label=_("Actors"), required=False, + widget=widgets.JQueryAutoComplete( + reverse_lazy('autocomplete-qualifiedbiographicalnote'))) length = FloatField(label=_("Length (cm)"), widget=widgets.CentimeterMeterWidget) width = FloatField(label=_("Width (cm)"), widget=widgets.CentimeterMeterWidget) @@ -1942,6 +2037,7 @@ class FindSelect(MuseumForm, GeoItemSelect, PeriodSelect): integrities = forms.ChoiceField(label=_("Integrity"), choices=[]) remarkabilities = forms.ChoiceField(label=_("Remarkability"), choices=[]) conservatory_states = forms.ChoiceField(label=_("Conservatory states"), choices=[]) + conservatory_states_details = forms.CharField(label=_("Conservatory state details")) conservatory_comment = forms.CharField(label=_("Conservatory comment")) alterations = forms.ChoiceField(label=_("Alteration"), choices=[]) alteration_causes = forms.ChoiceField(label=_("Alteration cause"), choices=[]) @@ -1989,13 +2085,22 @@ class FindSelect(MuseumForm, GeoItemSelect, PeriodSelect): reverse_lazy('autocomplete-biographicalnote'), associated_model=BiographicalNote), validators=[valid_id(BiographicalNote)]) + iconographic_patterns = forms.IntegerField( + label=_("Iconographic patterns"), + widget=widgets.JQueryAutoComplete( + reverse_lazy("autocomplete-iconographicpattern"), + associated_model=models.IconographicPatternType, + ), + ) + iconography_notes = forms.CharField(label=_("Iconography notes")) museum_inventory_marking_presence = forms.ChoiceField( label=_("Museum - Presence of inventory marking"), choices=[] ) museum_marking_type = forms.ChoiceField( label=_("Museum - Type of marking"), choices=[] ) - mark = forms.CharField(label=_("Mark")) + mark_text = forms.CharField(label=_("Transcription of the marking"), required=False) + mark = forms.CharField(label=_("Marking details")) museum_collections = forms.ChoiceField( label=_("Museum - Collection"), choices=[] ) @@ -2005,19 +2110,29 @@ class FindSelect(MuseumForm, GeoItemSelect, PeriodSelect): reverse_lazy('autocomplete-biographicalnote'), associated_model=BiographicalNote), validators=[valid_id(BiographicalNote)]) - museum_inventory_entry_year= forms.IntegerField(label=_("Museum - Inventory entry year")) + museum_inventory_entry_year= forms.IntegerField( + label=_("Museum - Inventory entry year") + ) museum_inventory_conformity = forms.ChoiceField( label=_("Museum - Conformity with inventory"), choices=[] ) museum_conformity_comment = forms.CharField(label=_("Museum - Comment on conformity")) - museum_inventory_transcript = forms.CharField(label=_("Museum - Inventory transcript")) + museum_inventory_transcript = forms.CharField( + label=_("Museum - Inventory transcript") + ) museum_original_repro = forms.ChoiceField( label=_("Museum - Original/reproduction"), choices=[] ) museum_allocation_date = DateField(label=_("Museum - Date of allocation")) museum_purchase_price = forms.CharField(label=_("Museum - Purchase price")) + listed_building_id = forms.CharField(label=_("Listed building ID")) + listed_building_protection_nature = forms.ChoiceField( + label=_("Nature of listed buildings protection"), choices=[], + ) + listed_building_date = DateField(label=_("Date of listing as a listed building")) + listed_building_notes = forms.CharField(label=_("Notes on listed building")) - TYPES = PeriodSelect.TYPES + [ + TYPES = DatingSelect.TYPES + [ FieldType("periods", Period), FieldType("conservatory_states", models.ConservatoryState), FieldType("base_finds__batch", models.BatchType), @@ -2052,6 +2167,8 @@ class FindSelect(MuseumForm, GeoItemSelect, PeriodSelect): FieldType("museum_collections", models.MuseumCollection), FieldType("museum_inventory_conformity", models.InventoryConformity), FieldType("museum_original_repro", models.OriginalReproduction), + FieldType("listed_building_protection_nature", + models.ListedBuildingProtectionNature), ] + GeoItemSelect.TYPES SITE_KEYS = { "archaeological_sites": "attached-to-operation", diff --git a/archaeological_finds/migrations/0149_add_editors.py b/archaeological_finds/migrations/0149_add_editors.py new file mode 100644 index 000000000..a74739a1d --- /dev/null +++ b/archaeological_finds/migrations/0149_add_editors.py @@ -0,0 +1,64 @@ +# 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_finds', '0148_statementcondition_documents'), + ] + + operations = [ + migrations.AddField( + model_name='basefind', + name='editors', + field=models.ManyToManyField(blank=True, related_name='%(class)s_edited', to='ishtar_common.author', verbose_name='Editors'), + ), + migrations.AddField( + model_name='exhibition', + name='editors', + field=models.ManyToManyField(blank=True, related_name='%(class)s_edited', to='ishtar_common.author', verbose_name='Editors'), + ), + migrations.AddField( + model_name='find', + name='editors', + field=models.ManyToManyField(blank=True, related_name='%(class)s_edited', to='ishtar_common.author', verbose_name='Editors'), + ), + migrations.AddField( + model_name='property', + name='editors', + field=models.ManyToManyField(blank=True, related_name='%(class)s_edited', to='ishtar_common.author', verbose_name='Editors'), + ), + migrations.AddField( + model_name='statementcondition', + name='editors', + field=models.ManyToManyField(blank=True, related_name='%(class)s_edited', to='ishtar_common.author', verbose_name='Editors'), + ), + migrations.AddField( + model_name='treatment', + name='editors', + field=models.ManyToManyField(blank=True, related_name='%(class)s_edited', to='ishtar_common.author', verbose_name='Editors'), + ), + migrations.AddField( + model_name='treatmentfile', + name='editors', + field=models.ManyToManyField(blank=True, related_name='%(class)s_edited', to='ishtar_common.author', verbose_name='Editors'), + ), + migrations.AlterField( + model_name='find', + name='weight', + field=models.FloatField(blank=True, null=True, verbose_name='Weight (g)'), + ), + migrations.AlterField( + model_name='historicalfind', + name='weight', + field=models.FloatField(blank=True, null=True, verbose_name='Weight (g)'), + ), + migrations.AlterField( + model_name='statementcondition', + name='weight', + field=models.FloatField(blank=True, null=True, verbose_name='Weight (g)'), + ), + ] diff --git a/archaeological_finds/migrations/0150_findrelationtype_findrecordrelations.py b/archaeological_finds/migrations/0150_findrelationtype_findrecordrelations.py new file mode 100644 index 000000000..a7be6cc8b --- /dev/null +++ b/archaeological_finds/migrations/0150_findrelationtype_findrecordrelations.py @@ -0,0 +1,58 @@ +# Generated by Django 4.2.21 on 2026-04-06 18:14 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import ishtar_common.models_common +import re + + +class Migration(migrations.Migration): + + dependencies = [ + ('ishtar_common', '0277_data_migration_qualifiedbionotetype'), + ('archaeological_finds', '0149_add_editors'), + ] + + operations = [ + migrations.CreateModel( + name='FindRelationType', + 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_finds.findrelationtype', verbose_name='Inverse relation')), + ], + options={ + 'verbose_name': 'Find relation type', + 'verbose_name_plural': 'Find relation types', + 'ordering': ('order', 'label'), + }, + bases=(ishtar_common.models_common.Cached, models.Model), + ), + migrations.CreateModel( + name='FindRecordRelations', + 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_finds.find')), + ('relation_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='archaeological_finds.findrelationtype')), + ('right_record', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='left_relations', to='archaeological_finds.find')), + ], + options={ + 'verbose_name': 'Find record relation', + 'verbose_name_plural': 'Find record relations', + 'ordering': ('left_record__cached_label', 'relation_type', 'right_record__cached_label'), + 'permissions': [('view_findrelation', 'Can view all Find relations')], + }, + ), + ] diff --git a/archaeological_finds/migrations/0151_data_migration.json b/archaeological_finds/migrations/0151_data_migration.json new file mode 100644 index 000000000..58dd88c85 --- /dev/null +++ b/archaeological_finds/migrations/0151_data_migration.json @@ -0,0 +1,30 @@ +[ + { + "model": "archaeological_finds.findrelationtype", + "fields": { + "label": "Recolle avec", + "txt_idx": "recolle-avec", + "comment": "", + "available": true, + "order": 10, + "symmetrical": true, + "tiny_label": null, + "inverse_relation": null, + "logical_relation": null + } + }, + { + "model": "archaeological_finds.findrelationtype", + "fields": { + "label": "Voisin de", + "txt_idx": "voisin-de", + "comment": "", + "available": true, + "order": 10, + "symmetrical": true, + "tiny_label": null, + "inverse_relation": null, + "logical_relation": null + } + } +] diff --git a/archaeological_finds/migrations/0151_data_migration_find_relation_type.py b/archaeological_finds/migrations/0151_data_migration_find_relation_type.py new file mode 100644 index 000000000..f3a1c1b4e --- /dev/null +++ b/archaeological_finds/migrations/0151_data_migration_find_relation_type.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, __): + FindRelationtypeType = apps.get_model("archaeological_finds", "findrelationtype") + if not FindRelationtypeType.objects.count(): + json_path = os.sep.join(os.path.abspath(__file__).split(os.sep)[:-1] + ["0151_data_migration.json"]) + call_command("loaddata", json_path) + + +class Migration(migrations.Migration): + + dependencies = [ + ('archaeological_finds', '0150_findrelationtype_findrecordrelations'), + ] + + operations = [ + migrations.RunPython(load_data) + ] diff --git a/archaeological_finds/migrations/0152_find_actors_heritage_museum_fields.py b/archaeological_finds/migrations/0152_find_actors_heritage_museum_fields.py new file mode 100644 index 000000000..1d386c3ee --- /dev/null +++ b/archaeological_finds/migrations/0152_find_actors_heritage_museum_fields.py @@ -0,0 +1,154 @@ +# Generated by Django 4.2.21 on 2026-04-16 10:44 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import ishtar_common.models_common +import re + + +class Migration(migrations.Migration): + + dependencies = [ + ('ishtar_common', '0277_data_migration_qualifiedbionotetype'), + ('archaeological_finds', '0151_data_migration_find_relation_type'), + ] + + operations = [ + migrations.AddField( + model_name='find', + name='actors', + field=models.ManyToManyField(blank=True, related_name='finds', to='ishtar_common.qualifiedbiographicalnote', verbose_name='Actors'), + ), + migrations.AddField( + model_name='find', + name='conservatory_states_details', + field=models.TextField(blank=True, default='', verbose_name='Conservatory state details'), + ), + migrations.AddField( + model_name='find', + name='iconography_notes', + field=models.TextField(blank=True, default='', verbose_name='Notes on iconography'), + ), + migrations.AddField( + model_name='find', + name='listed_building_date', + field=models.DateField(blank=True, null=True, verbose_name='Date of listing as a listed building'), + ), + migrations.AddField( + model_name='find', + name='listed_building_id', + field=models.TextField(blank=True, default='', verbose_name='Listed building ID'), + ), + migrations.AddField( + model_name='find', + name='listed_building_notes', + field=models.TextField(blank=True, default='', verbose_name='Notes on listed building'), + ), + migrations.AddField( + model_name='find', + name='mark_text', + field=models.TextField(blank=True, default='', verbose_name='Mark text'), + ), + migrations.AddField( + model_name='find', + name='title', + field=models.TextField(blank=True, default='', verbose_name='Title'), + ), + migrations.AddField( + model_name='historicalfind', + name='conservatory_states_details', + field=models.TextField(blank=True, default='', verbose_name='Conservatory state details'), + ), + migrations.AddField( + model_name='historicalfind', + name='iconography_notes', + field=models.TextField(blank=True, default='', verbose_name='Notes on iconography'), + ), + migrations.AddField( + model_name='historicalfind', + name='listed_building_date', + field=models.DateField(blank=True, null=True, verbose_name='Date of listing as a listed building'), + ), + migrations.AddField( + model_name='historicalfind', + name='listed_building_id', + field=models.TextField(blank=True, default='', verbose_name='Listed building ID'), + ), + migrations.AddField( + model_name='historicalfind', + name='listed_building_notes', + field=models.TextField(blank=True, default='', verbose_name='Notes on listed building'), + ), + migrations.AddField( + model_name='historicalfind', + name='mark_text', + field=models.TextField(blank=True, default='', verbose_name='Mark text'), + ), + migrations.AddField( + model_name='historicalfind', + name='title', + field=models.TextField(blank=True, default='', verbose_name='Title'), + ), + migrations.AlterField( + model_name='find', + name='mark', + field=models.TextField(blank=True, default='', verbose_name='Marking details'), + ), + migrations.AlterField( + model_name='historicalfind', + name='mark', + field=models.TextField(blank=True, default='', verbose_name='Marking details'), + ), + migrations.CreateModel( + name='ListedBuildingProtectionNature', + 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_finds.listedbuildingprotectionnature', verbose_name='Parent')), + ], + options={ + 'verbose_name': 'Listed building protection nature', + 'verbose_name_plural': 'Listed building protection nature', + 'ordering': ('order', 'label'), + }, + bases=(ishtar_common.models_common.Cached, models.Model), + ), + migrations.CreateModel( + name='IconographicPatternType', + 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_finds.iconographicpatterntype', verbose_name='Parent')), + ], + options={ + 'verbose_name': 'Iconographic pattern type', + 'verbose_name_plural': 'Iconographic pattern types', + 'ordering': ('order', 'label'), + }, + bases=(ishtar_common.models_common.Cached, models.Model), + ), + migrations.AddField( + model_name='find', + name='iconographic_patterns', + field=models.ManyToManyField(blank=True, related_name='finds', to='archaeological_finds.iconographicpatterntype', verbose_name='Iconographic patterns'), + ), + migrations.AddField( + model_name='find', + name='listed_building_protection_nature', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='finds', to='archaeological_finds.listedbuildingprotectionnature', verbose_name='Nature of listed buildings protection'), + ), + migrations.AddField( + model_name='historicalfind', + name='listed_building_protection_nature', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='archaeological_finds.listedbuildingprotectionnature', verbose_name='Nature of listed buildings protection'), + ), + ] diff --git a/archaeological_finds/migrations/0153_data_migration.json b/archaeological_finds/migrations/0153_data_migration.json new file mode 100644 index 000000000..2391c6097 --- /dev/null +++ b/archaeological_finds/migrations/0153_data_migration.json @@ -0,0 +1,46 @@ +[ + { + "model": "archaeological_finds.listedbuildingprotectionnature", + "fields": { + "label": "Inscription au titre des monuments historiques", + "txt_idx": "inscription-au-titre-des-monuments-historiques", + "comment": "", + "available": true, + "order": 10, + "parent": null + } + }, + { + "model": "archaeological_finds.listedbuildingprotectionnature", + "fields": { + "label": "Classement au titre des monuments historiques", + "txt_idx": "classement-au-titre-des-monuments-historiques", + "comment": "", + "available": true, + "order": 20, + "parent": null + } + }, + { + "model": "archaeological_finds.iconographicpatterntype", + "fields": { + "label": "Motif iconographique 1", + "txt_idx": "motif-iconographique-1", + "comment": "", + "available": true, + "order": 10, + "parent": null + } + }, + { + "model": "archaeological_finds.iconographicpatterntype", + "fields": { + "label": "Motif iconographique 2", + "txt_idx": "motif-iconographique-2", + "comment": "", + "available": true, + "order": 20, + "parent": null + } + } +] diff --git a/archaeological_finds/migrations/0153_data_migration_find_listed_building_icono_patterns.py b/archaeological_finds/migrations/0153_data_migration_find_listed_building_icono_patterns.py new file mode 100644 index 000000000..571197971 --- /dev/null +++ b/archaeological_finds/migrations/0153_data_migration_find_listed_building_icono_patterns.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, __): + IconographicPatternType = apps.get_model("archaeological_finds", "iconographicpatterntype") + if not IconographicPatternType.objects.count(): + json_path = os.sep.join(os.path.abspath(__file__).split(os.sep)[:-1] + ["0153_data_migration.json"]) + call_command("loaddata", json_path) + + +class Migration(migrations.Migration): + + dependencies = [ + ('archaeological_finds', '0152_find_actors_heritage_museum_fields'), + ] + + operations = [ + migrations.RunPython(load_data) + ] diff --git a/archaeological_finds/models.py b/archaeological_finds/models.py index d79369711..3b02d751e 100644 --- a/archaeological_finds/models.py +++ b/archaeological_finds/models.py @@ -14,12 +14,16 @@ from archaeological_finds.models_finds import ( FindBasket, FindDating, FindInsideContainer, + FindRecordRelations, + FindRelationType, FindTreatment, FirstBaseFindView, FunctionalArea, + IconographicPatternType, IntegrityType, InventoryConformity, InventoryMarkingPresence, + ListedBuildingProtectionNature, MarkingType, MaterialType, MaterialTypeQualityType, @@ -78,14 +82,18 @@ __all__ = [ "FindDownstreamTreatments", "FindInsideContainer", "FindNonModifTreatments", + "FindRecordRelations", + "FindRelationType", "FindTreatment", "FindTreatments", "FindUpstreamTreatments", "FollowUpActionType", "FunctionalArea", + "IconographicPatternType", "IntegrityType", "InventoryConformity", "InventoryMarkingPresence", + "ListedBuildingProtectionNature", "MarkingType", "MaterialType", "MaterialTypeQualityType", diff --git a/archaeological_finds/models_finds.py b/archaeological_finds/models_finds.py index 9b4156a9d..e475318af 100644 --- a/archaeological_finds/models_finds.py +++ b/archaeological_finds/models_finds.py @@ -29,6 +29,7 @@ from django.db.models import Max, Q, F from django.db.models.signals import m2m_changed, post_save, post_delete, pre_delete from django.core.exceptions import ObjectDoesNotExist from django.urls import reverse, reverse_lazy +from django.utils.formats import date_format from ishtar_common.data_importer import post_importer_action, ImporterError from ishtar_common.utils import ( @@ -54,26 +55,31 @@ from ishtar_common.models import ( Document, DocumentItem, document_attached_changed, + GeneralRecordRelations, + GeneralRelationType, GeneralType, GeoItem, + geo_item_pre_delete, get_current_profile, HierarchicalType, HistoryModel, Imported, IshtarSiteProfile, LightHistorizedItem, + QualifiedBiographicalNote, + main_item_pre_delete, MainItem, OrderedHierarchicalType, OrderedType, Organization, OwnPerms, Person, + post_delete_record_relation, post_save_cache, QuickAction, + RecordRelationManager, SearchVectorConfig, ValueGetter, - main_item_pre_delete, - geo_item_pre_delete ) from ishtar_common.models_common import HistoricalRecords, SerializeItem, \ GeoVectorData, geodata_attached_changed @@ -120,9 +126,37 @@ post_save.connect(post_save_cache, sender=MaterialTypeQualityType) post_delete.connect(post_save_cache, sender=MaterialTypeQualityType) -class ConservatoryState(HierarchicalType): - order = models.IntegerField(_("Order"), default=10) +class IconographicPatternType(OrderedHierarchicalType): + class Meta: + verbose_name = _("Iconographic pattern type") + verbose_name_plural = _("Iconographic pattern types") + ordering = ( + "order", + "label", + ) + ADMIN_SECTION = _("Finds") + + +post_save.connect(post_save_cache, sender=IconographicPatternType) +post_delete.connect(post_save_cache, sender=IconographicPatternType) + + +class ListedBuildingProtectionNature(OrderedHierarchicalType): + class Meta: + verbose_name = _("Listed building protection nature") + verbose_name_plural = _("Listed building protection nature") + ordering = ( + "order", + "label", + ) + ADMIN_SECTION = _("Finds") + + +post_save.connect(post_save_cache, sender=ListedBuildingProtectionNature) +post_delete.connect(post_save_cache, sender=ListedBuildingProtectionNature) + +class ConservatoryState(OrderedHierarchicalType): class Meta: verbose_name = _("Conservatory state type") verbose_name_plural = _("Conservatory state types") @@ -207,6 +241,7 @@ class TreatmentType(HierarchicalType): initial=None, force=False, full_hierarchy=False, + limit=None ): types = super(TreatmentType, cls).get_types( dct=dct, @@ -217,6 +252,7 @@ class TreatmentType(HierarchicalType): initial=initial, force=force, full_hierarchy=full_hierarchy, + limit=limit ) if dct and not exclude: rank = 0 @@ -1342,6 +1378,7 @@ class Find( ("datings__period__label", _("Chronological period")), ("material_types__label", _("Material type")), ("object_types__label", _("Object type")), + ("iconographic_patterns__label", _("Iconographic patterns")), ("recommended_treatments__label", _("Recommended treatments")), ("conservatory_states__label", _("Conservatory states")), ("integrities__label", _("Integrity")), @@ -1389,6 +1426,7 @@ class Find( "museum_entry_date", "museum_entry_date_end", "museum_allocation_date", + "listed_building_date" ] NUMBER_FIELDS = [ "base_finds__context_record__operation__year", @@ -1405,15 +1443,13 @@ class Find( "weight", "find_number", "min_number_of_individuals", - "datings__start_date", - "datings__end_date", "clutter_long_side", "clutter_short_side", "clutter_height", "museum_inventory_entry_year", "museum_inventory_quantity", "museum_observed_quantity", - ] + GeographicSubTownItem.NUMBER_FIELDS + ] + GeographicSubTownItem.NUMBER_FIELDS + BaseDating.NUMBER_FIELDS BASE_REQUEST = {"downstream_treatment__isnull": True} EXTRA_REQUEST_KEYS = { "all_base_finds__context_record": "base_finds__context_record__context_record_tree_parent__cr_parent_id", @@ -1436,6 +1472,7 @@ class Find( "documents__image__isnull": "documents__image__isnull", "container__location": "container__location__pk", "container_ref__location": "container_ref__location__pk", + "base_finds__excavation_id": "base_finds__excavation_id" } for table in (TABLE_COLS, TABLE_COLS_FOR_OPE): for key in table: @@ -1455,6 +1492,9 @@ class Find( "label": SearchAltName( pgettext_lazy("key for text search", "free-id"), "label__iexact" ), + "title": SearchAltName( + pgettext_lazy("key for text search", "title"), "title__iexact" + ), "denomination": SearchAltName( pgettext_lazy("key for text search", "denomination"), "denomination__iexact" ), @@ -1526,6 +1566,19 @@ class Find( "material_types__label__iexact", related_name="material_types", ), + "iconographic_patterns": SearchAltName( + pgettext_lazy("key for text search", "iconographic-patterns"), + "iconographic_patterns__label__iexact", + related_name="iconographic_patterns", + ), + "iconography_notes": SearchAltName( + pgettext_lazy("key for text search", "iconography-notes"), + "iconography_notes__iexact", + ), + "actors": SearchAltName( + pgettext_lazy("key for text search", "actors"), + "actors__cached_label__iexact" + ), "object_types": SearchAltName( pgettext_lazy("key for text search", "object-type"), "object_types__label__iexact", @@ -1605,7 +1658,7 @@ class Find( "previous_id": SearchAltName( pgettext_lazy("key for text search", "previous-id"), "previous_id__iexact" ), - #'collection': + # 'collection': # SearchAltName( # pgettext_lazy("key for text search", "collection"), # 'collection__name__iexact'), @@ -1619,8 +1672,17 @@ class Find( "museum_id": SearchAltName( pgettext_lazy("key for text search", "museum-id"), "museum_id__iexact" ), + "museum_id_prefix": SearchAltName( + pgettext_lazy("key for text search", "museum-id-prefix"), + "museum_id_prefix__iexact" + ), + "museum_id_suffix": SearchAltName( + pgettext_lazy("key for text search", "museum-id-suffix"), + "museum_id_suffix__iexact" + ), "cache_complete_museum_id": SearchAltName( - pgettext_lazy("key for text search", "complete-museum-id"), "cache_complete_museum_id__iexact" + pgettext_lazy("key for text search", "complete-museum-id"), + "cache_complete_museum_id__iexact" ), "laboratory_id": SearchAltName( pgettext_lazy("key for text search", "laboratory-id"), @@ -1629,6 +1691,10 @@ class Find( "mark": SearchAltName( pgettext_lazy("key for text search", "mark"), "mark__iexact" ), + "mark_text": SearchAltName( + pgettext_lazy("key for text search", "marking-transcription"), + "mark_text__iexact" + ), "base_finds__discovery_date": SearchAltName( pgettext_lazy("key for text search", "discovery-date"), "base_finds__discovery_date", @@ -1684,6 +1750,10 @@ class Find( pgettext_lazy("key for text search", "conservatory-comment"), "conservatory_comment__iexact", ), + "conservatory_states_details": SearchAltName( + pgettext_lazy("key for text search", "conservatory-states-details"), + "conservatory_states_details__iexact", + ), "length": SearchAltName( pgettext_lazy("key for text search", "length"), "length" ), @@ -1916,12 +1986,28 @@ class Find( pgettext_lazy("key for text search", "museum-observed-quantity"), "museum_observed_quantity" ), + "listed_building_id": SearchAltName( + pgettext_lazy("key for text search", "listed-building-id"), + "listed_building_id__iexact" + ), + "listed_building_protection_nature": SearchAltName( + pgettext_lazy("key for text search", "listed-building-protection-nature"), + "listed_building_protection_nature__label__iexact" + ), + "listed_building_date": SearchAltName( + pgettext_lazy("key for text search", "listed-building-date"), + "listed_building_date" + ), + "listed_building_notes": SearchAltName( + pgettext_lazy("key for text search", "listed-building-notes"), + "listed_building_notes__iexact" + ), } ALT_NAMES.update(BaseHistorizedItem.ALT_NAMES) ALT_NAMES.update(DocumentItem.ALT_NAMES) - ALT_NAMES.update(Dating.ASSOCIATED_ALT_NAMES) ALT_NAMES.update(GeoItem.ALT_NAMES_FOR_FIND()) ALT_NAMES.update(Imported.ALT_NAMES) + ALT_NAMES.update(BaseDating.ALT_NAMES) DEFAULT_SEARCH_FORM = ("archaeological_finds.forms", "FindSelect") @@ -1983,6 +2069,7 @@ class Find( SearchVectorConfig("periods__label", "local"), SearchVectorConfig("integrities__label", "raw"), SearchVectorConfig("material_types__label", "local"), + SearchVectorConfig("iconographic_patterns__label", "local"), SearchVectorConfig("object_types__label", "raw"), SearchVectorConfig("remarkabilities__label", "raw"), SearchVectorConfig("technical_processes__label", "raw"), @@ -2085,6 +2172,7 @@ class Find( ] HISTORICAL_M2M = [ "material_types", + "iconographic_patterns", "technical_processes", "periods", "datings", @@ -2113,6 +2201,7 @@ class Find( "cultural_attributions", "functional_areas", "material_types", + "iconographic_patterns", "integrities", "recommended_treatments", "museum_former_collections", @@ -2171,6 +2260,7 @@ class Find( order = models.IntegerField(_("Order"), default=1) label = models.TextField(_("Free ID")) denomination = models.TextField(_("Denomination"), blank=True, default="") + title = models.TextField(_("Title"), blank=True, default="") # museum module IDs museum_id_prefix = models.TextField(_("Museum ID prefix"), blank=True, default="") museum_id = models.TextField(_("Museum inventory number"), blank=True, default="") @@ -2180,6 +2270,14 @@ class Find( description = models.TextField(_("Description"), blank=True, default="") decoration = models.TextField(_("Decoration"), blank=True, default="") inscription = models.TextField(_("Inscription"), blank=True, default="") + iconographic_patterns = models.ManyToManyField( + IconographicPatternType, + verbose_name=_("Iconographic patterns"), + related_name="finds", + blank=True, + ) + iconography_notes = models.TextField(_("Notes on iconography"), blank=True, + default="") manufacturing_place = models.TextField( _("Manufacturing place"), blank=True, default="" ) @@ -2238,6 +2336,28 @@ class Find( cultural_attributions = models.ManyToManyField( CulturalAttributionType, verbose_name=_("Cultural attribution"), blank=True ) + actors = models.ManyToManyField( + QualifiedBiographicalNote, related_name="finds", verbose_name=_("Actors"), + blank=True + ) + ## listed building + listed_building_id = models.TextField( + _("Listed building ID"), default="", blank=True, + ) + listed_building_protection_nature = models.ForeignKey( + ListedBuildingProtectionNature, + verbose_name=_("Nature of listed buildings protection"), + blank=True, + null=True, + related_name="finds", + on_delete=models.SET_NULL, + ) + listed_building_date = models.DateField( + _("Date of listing as a listed building"), blank=True, null=True) + listed_building_notes = models.TextField( + _("Notes on listed building"), default="", blank=True, + ) + ## containers container = models.ForeignKey( "archaeological_warehouse.Container", verbose_name=_("Container"), @@ -2322,7 +2442,8 @@ class Find( dimensions_comment = models.TextField( _("Dimensions comment"), blank=True, default="" ) - mark = models.TextField(_("Mark"), blank=True, default="") + mark_text = models.TextField(_("Transcription of the marking"), blank=True, default="") + mark = models.TextField(_("Marking details"), blank=True, default="") comment = models.TextField(_("General comment"), blank=True, default="") dating_comment = models.TextField(_("Comment on dating"), blank=True, default="") previous_id = models.TextField(_("Previous ID"), blank=True, default="") @@ -2419,6 +2540,9 @@ class Find( verbose_name=_("Conservatory states"), blank=True, ) + conservatory_states_details = models.TextField( + _("Conservatory state details"), blank=True, default="" + ) conservatory_comment = models.TextField( _("Conservatory comment"), blank=True, default="" ) @@ -2552,16 +2676,55 @@ class Find( ] ) + def _has_section(self, name, attrs): + """ + For sheets: evaluate availability of a section. + Cache is set. + """ + if getattr(self, "_cache_section", None) is None: + self._cache_section = {} + if name in self._cache_section: + return self._cache_section[name] + has_value = False + for attr in attrs: + if getattr(self, attr): + has_value = True + break + self._cache_section[name] = has_value + return self._cache_section[name] + + @property + def has_listed_building_section(self): + attrs = ["listed_building_protection_nature_id", "listed_building_id", + "listed_building_notes", "listed_building_date"] + return self._has_section("has_listed_building_section", attrs) + + @property + def has_preservation_fields(self): + attrs = [ + "integrities_count", "remarkabilities_count", "conservatory_states_count", + "conservatory_comment", "alterations_count", "alteration_causes_count", + "recommended_treatments_count", "appraisal_date", "treatment_emergency", + "insurance_value", "estimated_value", "conservatory_states_details" + ] + return self._has_section("has_preservation_fields", attrs) + @property def has_museum_section(self): - if get_current_profile().museum and self.mark: + if getattr(self, "_has_museum_section", None) is not None: + return self._has_museum_section + if self.mark or self.mark_text: + self._has_museum_section = True return True for field in self._meta.get_fields(): - if not field.name.startswith("museum_"): + if not field.name.startswith("museum_") and \ + not field.name.startswith("iconograph"): continue instanced_field = getattr(self, field.name) if instanced_field and (not field.many_to_many or instanced_field.count()): + self._has_museum_section = True return True + self._has_museum_section = False return False @property @@ -2570,7 +2733,6 @@ class Find( @property def museum_entry_date_label(self): - from django.utils.formats import date_format if not self.museum_entry_date: return if self.museum_entry_date and self.museum_entry_date_end and ( @@ -2579,9 +2741,12 @@ class Find( self.museum_entry_date.day == 1 and self.museum_entry_date_end.day == 31 ): return self.museum_entry_date.year - dates = [date_format(self.museum_entry_date, format='SHORT_DATE_FORMAT', use_l10n=True)] + dates = [date_format(self.museum_entry_date, format='SHORT_DATE_FORMAT', + use_l10n=True)] if self.museum_entry_date_end: - dates.append(date_format(self.museum_entry_date_end, format='SHORT_DATE_FORMAT', use_l10n=True)) + dates.append( + date_format(self.museum_entry_date_end, format='SHORT_DATE_FORMAT', + use_l10n=True)) return " / ".join(dates) @classmethod @@ -2920,7 +3085,7 @@ class Find( # no particular rights: if you can view an item you can add it to your # own basket - actions = super(Find, self).get_extra_actions(request) + actions = super().get_extra_actions(request) is_locked = hasattr(self, "is_locked") and self.is_locked(request.user) profile = get_current_profile() @@ -2928,6 +3093,14 @@ class Find( if can_edit_find and not is_locked: actions += [ ( + reverse("find-relations-modify", args=[self.pk]), + _("Modify finds relations"), + "fa fa-retweet", + _("finds"), + "", + True, + ), + ( reverse("find-dating-add", args=[self.pk]), _("Add dating"), "fa fa-plus", @@ -3022,29 +3195,52 @@ class Find( return "" return "{}-{}".format(bf.context_record.operation.get_reference(), self.index) + def _get_count(self, attr): + """ + For sheets: evaluate count of m2m. + Cache is set. + """ + if getattr(self, "_cache_count", None) is None: + self._cache_count = {} + if attr not in self._cache_count: + self._cache_count[attr] = getattr(self, attr).count() + return self._cache_count[attr] + @property def integrities_count(self): - return self.integrities.count() + return self._get_count("integrities") @property def conservatory_states_count(self): - return self.conservatory_states.count() + return self._get_count("conservatory_states") @property def remarkabilities_count(self): - return self.remarkabilities.count() + return self._get_count("remarkabilities") @property def cultural_attributions_count(self): - return self.cultural_attributions.count() + return self._get_count("cultural_attributions") @property def documents_count(self): - return self.documents.count() + return self._get_count("documents") @property def periods_count(self): - return self.periods.count() + return self._get_count("periods") + + @property + def alterations_count(self): + return self._get_count("alterations") + + @property + def alteration_causes_count(self): + return self._get_count("alteration_causes") + + @property + def recommended_treatments_count(self): + return self._get_count("recommended_treatments") @property def operation(self): @@ -3943,10 +4139,45 @@ def base_find_find_changed(sender, **kwargs): m2m_changed.connect(base_find_find_changed, sender=Find.base_finds.through) - m2m_changed.connect(document_attached_changed, sender=Find.documents.through) +class FindRelationType(GeneralRelationType): + ADMIN_SECTION = _("Finds") + class Meta: + verbose_name = _("Find relation type") + verbose_name_plural = _("Find relation types") + ordering = ("order", "label") + + +class FindRecordRelations(GeneralRecordRelations): + ADMIN_SECTION = _("Finds") + MAIN_ATTR = "left_record" + left_record = models.ForeignKey( + Find, related_name="right_relations", on_delete=models.CASCADE + ) + right_record = models.ForeignKey( + Find, related_name="left_relations", on_delete=models.CASCADE + ) + relation_type = models.ForeignKey(FindRelationType, on_delete=models.PROTECT) + objects = RecordRelationManager() + + class Meta: + verbose_name = _("Find record relation") + verbose_name_plural = _("Find record relations") + ordering = ( + "left_record__cached_label", + "relation_type", + "right_record__cached_label", + ) + permissions = [ + ("view_findrelation", "Can view all Find relations"), + ] + + +post_delete.connect(post_delete_record_relation, sender=FindRecordRelations) + + class FindDating(BaseDating): SERIALIZE_EXCLUDE = ["find"] CURRENT_MODEL = Find diff --git a/archaeological_finds/templates/ishtar/sheet_find.html b/archaeological_finds/templates/ishtar/sheet_find.html index 0ae238d66..b3663bbbc 100644 --- a/archaeological_finds/templates/ishtar/sheet_find.html +++ b/archaeological_finds/templates/ishtar/sheet_find.html @@ -26,6 +26,7 @@ {% with non_modif_treatments_count=item.non_modif_treatments_count %} {% with associated_treatment_files_count=item.associated_treatment_files_count %} +{% 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_view_container=permission_view_own_container|or_:permission_view_container %} {% with display_warehouse_treatments=item.container|or_:item.container_ref|or_:item.upstream_treatment|or_:item.downstream_treatment|or_:non_modif_treatments_count|or_:associated_treatment_files_count %} {% with dating_list=item|m2m_listing:"datings" %} @@ -51,6 +52,15 @@ {% trans "Archaeological context" %} </a> </li> + {% if display_relations %} + <li class="nav-item"> + <a class="nav-link" id="{{window_id}}-relations-tab" + data-toggle="tab" href="#{{window_id}}-relations" role="tab" + aria-controls="{{window_id}}-relations" aria-selected="false"> + {% trans "Relations" %} + </a> + </li> + {% endif %} <li class="nav-item"> <a class="nav-link" id="{{window_id}}-datations-tab" data-toggle="tab" href="#{{window_id}}-datations" role="tab" @@ -114,6 +124,7 @@ <div class='row'> {% field_flex "Denomination" item.denomination %} + {% field_flex _("Title") item.title %} {% field_flex "Complete museum ID" item.cache_complete_museum_id %} {% field_flex "Free ID" item.label %} {% field_flex "Previous ID" item.previous_id %} @@ -129,6 +140,7 @@ {% field_flex_full "Public description" item.public_description "<pre>" "</pre>" %} {% field_flex "Is complete?" item.is_complete %} {% with material=item.get_hierarchical_material_types %}{% if material %} + {% field_flex_detail_multiple _("Actors") item.actors %} {% field_flex "Material types" material %}{% else %} {% field_flex_multiple_obj "Material types" item 'material_types' %} {% endif %}{% endwith %} @@ -152,6 +164,16 @@ {% field_flex_full _("General comment") item.comment "<pre>" "</pre>" %} </div> + {% if item.has_listed_building_section %} + <h3>{% trans "Listed building" %}</h3> + <div class='row'> + {% field_flex _("Nature of listed buildings protection") item.listed_building_protection_nature %} + {% field_flex _("Listed building ID") item.listed_building_id %} + {% field_flex _("Date of listing as a listed building") item.listed_building_date|date:"SHORT_DATE_FORMAT" %} + {% field_flex _("Notes on listed building") item.listed_building_notes %} + </div> + {% endif %} + {% if item.has_ownership_section %} <h3>{% trans "Ownership" %}</h3> <div class='row'> @@ -177,8 +199,11 @@ {% field_flex museum_entry_date_label item.museum_entry_date_label %} {% field_flex "Comment on museum entry date" item.museum_entry_date_comment %} {% field_flex_detail_multiple _("Donors, testators or vendors") item.museum_donors %} + {% field_flex_multiple_obj _("Iconographic patterns") item 'iconographic_patterns' %} + {% field_flex_full _("Notes on iconography") item.iconography_notes "<pre>" "</pre>" %} {% field_flex_multiple_obj "Presence of inventory marking" item "museum_inventory_marking_presence" %} {% field_flex_multiple_obj "Type of marking" item "museum_marking_type" %} + {% field_flex _("Transcription of the marking") item.mark_text %} {% field_flex "Marking details" item.mark "<pre>" "</pre>" %} {% field_flex "Conformity with inventory" item.museum_inventory_conformity %} {% field_flex "Comment on conformity" item.museum_conformity_comment %} @@ -213,7 +238,7 @@ {% 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'> {% trans "Checked" as checked_label %} @@ -270,6 +295,15 @@ </div> </div> + {% if display_relations %} + <div class="tab-pane fade" id="{{window_id}}-relations" + role="tabpanel" aria-labelledby="{{window_id}}-relations-tab"> + {% with relation_url="/show-find/" %} + {% include "ishtar/blocks/sheet_relations.html" %} + {% endwith %} + </div> + {% endif %} + <div class="tab-pane fade" id="{{window_id}}-datations" role="tabpanel" aria-labelledby="{{window_id}}-datations-tab"> <h3>{% trans "Periods / Datings" %}</h3> @@ -326,7 +360,7 @@ {% endif %} </div> -{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %} +{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %} <script type='text/javascript'> $( "#{{window_id}}-tabs" ).on( "tabsactivate", function( event, ui ) { diff --git a/archaeological_finds/templates/ishtar/sheet_find_treatments.html b/archaeological_finds/templates/ishtar/sheet_find_treatments.html index 49e70d5f8..670c1997b 100644 --- a/archaeological_finds/templates/ishtar/sheet_find_treatments.html +++ b/archaeological_finds/templates/ishtar/sheet_find_treatments.html @@ -9,12 +9,13 @@ </div> {% endif %} {% endcomment %} - {% if item.integrities_count or item.remarkabilities_count or item.conservatory_states_count or item.conservatory_comment or item.alterations.count or item.alteration_causes.count or item.recommended_treatments.count or item.appraisal_date or item.treatment_emergency or item.insurance_value or item.estimated_value %} + {% if item.has_preservation_fields %} <h3>{% trans "Preservation" %}</h3> <div class='row'> {% field_flex_multiple_obj _("Integrity") item 'integrities' %} {% field_flex_multiple_obj _("Remarkability") item 'remarkabilities' %} {% field_flex_multiple_obj _("Conservatory states") item 'conservatory_states' %} + {% field_flex_full _("Conservatory state details") item.conservatory_states_details "<pre>" "</pre>" %} {% field_flex_multiple_obj _("Alteration") item 'alterations' %} {% field_flex_multiple_obj _("Alteration cause") item 'alteration_causes' %} {% field_flex_multiple_obj _("Recommended treatments") item 'recommended_treatments' %} diff --git a/archaeological_finds/templates/ishtar/sheet_museum_find.html b/archaeological_finds/templates/ishtar/sheet_museum_find.html index 3acec990e..b97143e15 100644 --- a/archaeological_finds/templates/ishtar/sheet_museum_find.html +++ b/archaeological_finds/templates/ishtar/sheet_museum_find.html @@ -26,6 +26,7 @@ {% with non_modif_treatments_count=item.non_modif_treatments_count %} {% with associated_treatment_files_count=item.associated_treatment_files_count %} +{% 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_view_container=permission_view_own_container|or_:permission_view_container %} {% with display_warehouse_treatments=item.container|or_:item.container_ref|or_:item.upstream_treatment|or_:item.downstream_treatment|or_:non_modif_treatments_count|or_:associated_treatment_files_count %} {% with dating_list=item|m2m_listing:"datings" %} @@ -51,6 +52,15 @@ {% trans "Archaeological context" %} </a> </li> + {% if display_relations %} + <li class="nav-item"> + <a class="nav-link" id="{{window_id}}-relations-tab" + data-toggle="tab" href="#{{window_id}}-relations" role="tab" + aria-controls="{{window_id}}-relations" aria-selected="false"> + {% trans "Relations" %} + </a> + </li> + {% endif %} <li class="nav-item"> <a class="nav-link" id="{{window_id}}-datations-tab" data-toggle="tab" href="#{{window_id}}-datations" role="tab" @@ -114,6 +124,7 @@ <div class='row'> {% field_flex "Denomination" item.denomination %} + {% field_flex _("Title") item.title %} {% field_flex "Complete museum ID" item.cache_complete_museum_id %} {% field_flex "Free ID" item.label %} {% field_flex "Previous ID" item.previous_id %} @@ -129,6 +140,7 @@ {% field_flex_full "Public description" item.public_description "<pre>" "</pre>" %} {% field_flex "Is complete?" item.is_complete %} {% with material=item.get_hierarchical_material_types %}{% if material %} + {% field_flex_detail_multiple _("Actors") item.actors %} {% field_flex "Material types" material %}{% else %} {% field_flex_multiple_obj "Material types" item 'material_types' %} {% endif %}{% endwith %} @@ -152,6 +164,16 @@ {% field_flex_full _("General comment") item.comment "<pre>" "</pre>" %} </div> + {% if item.has_listed_building_section %} + <h3>{% trans "Listed building" %}</h3> + <div class='row'> + {% field_flex _("Nature of listed buildings protection") item.listed_building_protection_nature %} + {% field_flex _("Listed building ID") item.listed_building_id %} + {% field_flex _("Date of listing as a listed building") item.listed_building_date|date:"SHORT_DATE_FORMAT" %} + {% field_flex _("Notes on listed building") item.listed_building_notes %} + </div> + {% endif %} + {% if item.has_ownership_section %} <h3>{% trans "Ownership" %}</h3> <div class='row'> @@ -177,8 +199,11 @@ {% field_flex museum_entry_date_label item.museum_entry_date_label %} {% field_flex "Comment on museum entry date" item.museum_entry_date_comment %} {% field_flex_detail_multiple _("Donors, testators or vendors") item.museum_donors %} + {% field_flex_multiple_obj _("Iconographic patterns") item 'iconographic_patterns' %} + {% field_flex_full _("Notes on iconography") item.iconography_notes "<pre>" "</pre>" %} {% field_flex_multiple_obj "Presence of inventory marking" item "museum_inventory_marking_presence" %} {% field_flex_multiple_obj "Type of marking" item "museum_marking_type" %} + {% field_flex _("Transcription of the marking") item.mark_text %} {% field_flex "Marking details" item.mark "<pre>" "</pre>" %} {% field_flex "Conformity with inventory" item.museum_inventory_conformity %} {% field_flex "Comment on conformity" item.museum_conformity_comment %} @@ -213,7 +238,7 @@ {% 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'> {% trans "Checked" as checked_label %} @@ -270,6 +295,15 @@ </div> </div> + {% if display_relations %} + <div class="tab-pane fade" id="{{window_id}}-relations" + role="tabpanel" aria-labelledby="{{window_id}}-relations-tab"> + {% with relation_url="/show-find/" %} + {% include "ishtar/blocks/sheet_relations.html" %} + {% endwith %} + </div> + {% endif %} + <div class="tab-pane fade" id="{{window_id}}-datations" role="tabpanel" aria-labelledby="{{window_id}}-datations-tab"> <h3>{% trans "Periods / Datings" %}</h3> @@ -326,7 +360,7 @@ {% endif %} </div> -{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %} +{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %}{% endwith %} <script type='text/javascript'> $( "#{{window_id}}-tabs" ).on( "tabsactivate", function( event, ui ) { diff --git a/archaeological_finds/tests.py b/archaeological_finds/tests.py index 87d0a62f2..aaca76d51 100644 --- a/archaeological_finds/tests.py +++ b/archaeological_finds/tests.py @@ -676,7 +676,7 @@ class ImportFindTest(BaseImportFindTest): #base_find.save() base_find = models.BaseFind.objects.get(pk=base_find.pk) - impt.importation() + impt.start_import() if impt.error_file: self.assertIsNone( impt.error_file, @@ -722,7 +722,7 @@ class ImportFindTest(BaseImportFindTest): nb_find = models.Find.objects.count() nb_container = Container.objects.count() nb_docs = Document.objects.count() - impt.importation() + impt.start_import() self.assertEqual(models.BaseFind.objects.count(), nb_base_find + 1) self.assertEqual(models.Find.objects.count(), nb_find + 1) @@ -775,7 +775,7 @@ class ImportFindTest(BaseImportFindTest): glass = models.MaterialType.objects.get(txt_idx="glass").pk self.set_target_key("material_types", "terre-cuite", ceram) self.set_target_key("material_types", "verre", glass) - impt.importation() + impt.start_import() # new finds has now been imported current_nb = models.BaseFind.objects.count() self.assertEqual(current_nb, (old_nb + 4)) @@ -910,7 +910,7 @@ class ImportFindTest(BaseImportFindTest): impt = form.save(self.ishtar_user) impt.initialize() - impt.importation() + impt.start_import() # no new finds has now been imported current_nb = models.BaseFind.objects.count() self.assertEqual(current_nb, old_nb) @@ -976,7 +976,7 @@ class ImportFindTest(BaseImportFindTest): # reimport impt.initialize() - impt.importation() + impt.start_import() # check errors self.assertEqual(len(impt.errors), 2) error_msg = str( @@ -987,7 +987,7 @@ class ImportFindTest(BaseImportFindTest): # test ignore errors impt.importer_type.ignore_errors = error_msg[:-5] impt.importer_type.save() - impt.importation() + impt.start_import() # check errors self.assertEqual(len(impt.errors), 0) @@ -1023,8 +1023,8 @@ class ImportFindTest(BaseImportFindTest): impt.save() impt.initialize() # import 2 times the description field - impt.importation() - impt.importation() + impt.start_import() + impt.start_import() for find in models.Find.objects.filter(imports__pk=impt.pk).all(): sp = find.description.split(" - ") self.assertEqual(len(sp), 2) @@ -1112,7 +1112,7 @@ class ImportFindLiveServerTest(LiveServerTestCase, BaseImportFindTest): self.assertTrue(form.is_valid()) impt = form.save(self.ishtar_user) impt.initialize() - impt.importation() + impt.start_import() # new finds has now been imported current_nb = models.BaseFind.objects.count() self.assertEqual(current_nb, (old_nb + 4)) @@ -1820,7 +1820,7 @@ class FindSearchTest(FindInit, TestCase, SearchText, StatisticsTest): find2.save() material_key = str(pgettext_lazy("key for text search", "material")) - period_key = str(pgettext_lazy("key for text search", "datings-period")) + period_key = str(pgettext_lazy("key for text search", "dating-period")) ope_key = str(pgettext_lazy("key for text search", "code-patriarche")) result = [ (f'{ope_key}="{ope_values[0]}" {period_key}="{neo}" ' diff --git a/archaeological_finds/urls.py b/archaeological_finds/urls.py index d1520d4ba..de96604c6 100644 --- a/archaeological_finds/urls.py +++ b/archaeological_finds/urls.py @@ -99,6 +99,14 @@ urlpatterns = [ ), name="find-dating-delete", ), + path( + "find-relations-modify/<int:pk>)/", + check_permissions( + ["archaeological_finds.change_find", + "archaeological_finds.change_own_find"] + )(views.find_modify_relations), + name="find-relations-modify", + ), re_path(r"get-findbasket/$", views.get_find_basket, name="get-findbasket"), re_path( r"get-findbasket-write/$", @@ -621,6 +629,11 @@ urlpatterns = [ name="autocomplete-materialtype", ), re_path( + r"autocomplete-iconographicpattern/$", + views.autocomplete_iconographicpattern, + name="autocomplete-iconographicpattern", + ), + re_path( r"autocomplete-treatmenttype/$", views.autocomplete_treatmenttype, name="autocomplete-treatmenttype", diff --git a/archaeological_finds/views.py b/archaeological_finds/views.py index d11e820fa..3ce2a466f 100644 --- a/archaeological_finds/views.py +++ b/archaeological_finds/views.py @@ -63,6 +63,7 @@ from ishtar_common.views_item import ( get_autocomplete_queries, get_autocomplete_query ) +from archaeological_operations.views import get_relation_modify from archaeological_operations.wizards import AdministrativeActDeletionWizard from archaeological_finds import wizards @@ -538,13 +539,20 @@ def find_delete(request, pk): return redirect(reverse("find_deletion", kwargs={"step": step})) +autocomplete_functionalarea = get_autocomplete_generic(models.FunctionalArea) +autocomplete_integritytype = get_autocomplete_generic(models.IntegrityType) +autocomplete_iconographicpattern = get_autocomplete_generic( + models.IconographicPatternType) autocomplete_objecttype = get_autocomplete_generic(models.ObjectType) autocomplete_materialtype = get_autocomplete_generic(models.MaterialType) -autocomplete_treatmenttype = get_autocomplete_generic(models.TreatmentType) -autocomplete_integritytype = get_autocomplete_generic(models.IntegrityType) -autocomplete_functionalarea = get_autocomplete_generic(models.FunctionalArea) autocomplete_technicalarea = get_autocomplete_generic(models.TechnicalAreaType) autocomplete_technicalprocess = get_autocomplete_generic(models.TechnicalProcessType) +autocomplete_treatmenttype = get_autocomplete_generic(models.TreatmentType) + +find_modify_relations = get_relation_modify( + models.Find, models.FindRecordRelations, + forms.FindRecordRelationsFormSet, "find-relations-modify" +) class NewFindBasketView(IshtarMixin, LoginRequiredMixin, CreateView): |
