#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (C) 2012-2025 Étienne Loks # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # See the file COPYING for details. from collections import OrderedDict import datetime import uuid from django.apps import apps from django.conf import settings from django.contrib.gis.db import models from django.contrib.postgres.indexes import GinIndex 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 ishtar_common.data_importer import post_importer_action, ImporterError from ishtar_common.utils import ( cached_label_changed, get_generated_id, m2m_historization_changed, pgettext_lazy, post_save_geo, SearchAltName, ugettext_lazy as _ ) from ishtar_common.alternative_configs import ALTERNATE_CONFIGS from ishtar_common.model_managers import UUIDModelManager from ishtar_common.models import ( BaseHistorizedItem, Basket, BiographicalNote, CompleteIdentifierItem, Document, DocumentItem, document_attached_changed, GeneralType, GeoItem, get_current_profile, HierarchicalType, HistoryModel, Imported, IshtarSiteProfile, LightHistorizedItem, MainItem, OrderedHierarchicalType, OrderedType, Organization, OwnPerms, Person, post_save_cache, QuickAction, SearchVectorConfig, ValueGetter, ) from ishtar_common.models_common import HistoricalRecords, Imported, SerializeItem, \ GeoVectorData, geodata_attached_changed from ishtar_common.utils import PRIVATE_FIELDS from archaeological_operations.models import ( AdministrativeAct, Operation, CulturalAttributionType, ) from archaeological_context_records.models import ContextRecord, Dating, \ GeographicSubTownItem from archaeological_warehouse.models import Warehouse class MaterialType(HierarchicalType): code = models.CharField(_("Code"), max_length=100, blank=True, null=True) recommendation = models.TextField(_("Recommendation"), blank=True, default="") class Meta: verbose_name = _("Material type") verbose_name_plural = _("Material types") ordering = ("label",) ADMIN_SECTION = _("Finds") post_save.connect(post_save_cache, sender=MaterialType) post_delete.connect(post_save_cache, sender=MaterialType) class MaterialTypeQualityType(GeneralType): order = models.IntegerField(_("Order"), default=10) class Meta: verbose_name = _("Material type quality type") verbose_name_plural = _("Material type quality types") ordering = ("order", "label") ADMIN_SECTION = _("Finds") 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 Meta: verbose_name = _("Conservatory state type") verbose_name_plural = _("Conservatory state types") ordering = ( "order", "label", ) ADMIN_SECTION = _("Finds") post_save.connect(post_save_cache, sender=ConservatoryState) post_delete.connect(post_save_cache, sender=ConservatoryState) class TreatmentType(HierarchicalType): order = models.IntegerField(_("Order"), default=10) virtual = models.BooleanField(_("Virtual")) destructive = models.BooleanField(_("Destructive"), default=False) create_new_find = models.BooleanField( _("Create a new find"), default=False, help_text=_( "If True when this treatment is applied a new version " "of the object will be created." ), ) upstream_is_many = models.BooleanField( _("Upstream is many"), default=False, help_text=_( "Check this if for this treatment from many finds you'll get " "one." ), ) downstream_is_many = models.BooleanField( _("Downstream is many"), default=False, help_text=_( "Check this if for this treatment from one find you'll get " "many." ), ) change_reference_location = models.BooleanField( _("Change reference location"), default=False, help_text=_("The treatment change the reference location."), ) change_current_location = models.BooleanField( _("Change current location"), default=False, help_text=_("The treatment change the current location."), ) restore_reference_location = models.BooleanField( _("Restore the reference location"), default=False, help_text=_( "The treatment change restore reference location to the " "current location." ), ) class Meta: verbose_name = _("Treatment type") verbose_name_plural = _("Treatment types") ordering = ( "order", "label", ) ADMIN_SECTION = _("Treatments") @classmethod def get_types( cls, dct=None, instances=False, exclude=None, empty_first=True, default=None, initial=None, force=False, full_hierarchy=False, ): types = super(TreatmentType, cls).get_types( dct=dct, instances=instances, exclude=exclude, empty_first=empty_first, default=default, initial=initial, force=force, full_hierarchy=full_hierarchy, ) if dct and not exclude: rank = 0 if instances: type_list = [ty.pk for ty in types] if types: rank = types[-1].rank else: type_list = [idx for idx, __ in types] dct["available"] = True q = cls.objects.filter(**dct).exclude(pk__in=type_list) for t in q.all(): if instances: rank += 1 t.rank = rank types.append(t) else: types.append((t.pk, str(t))) return types post_save.connect(post_save_cache, sender=TreatmentType) post_delete.connect(post_save_cache, sender=TreatmentType) class IntegrityType(GeneralType): class Meta: verbose_name = _("Integrity type") verbose_name_plural = _("Integrity types") ordering = ("label",) ADMIN_SECTION = _("Finds") post_save.connect(post_save_cache, sender=IntegrityType) post_delete.connect(post_save_cache, sender=IntegrityType) class RemarkabilityType(GeneralType): class Meta: verbose_name = _("Remarkability type") verbose_name_plural = _("Remarkability types") ordering = ("label",) ADMIN_SECTION = _("Finds") post_save.connect(post_save_cache, sender=RemarkabilityType) post_delete.connect(post_save_cache, sender=RemarkabilityType) class BatchType(GeneralType): order = models.IntegerField(_("Order"), default=10) class Meta: verbose_name = _("Batch type") verbose_name_plural = _("Batch types") ordering = ("order", "label") ADMIN_SECTION = _("Finds") post_save.connect(post_save_cache, sender=BatchType) post_delete.connect(post_save_cache, sender=BatchType) class ObjectType(HierarchicalType): class Meta: verbose_name = _("Object type") verbose_name_plural = _("Object types") ordering = ( "parent__label", "label", ) ADMIN_SECTION = _("Finds") post_save.connect(post_save_cache, sender=ObjectType) post_delete.connect(post_save_cache, sender=ObjectType) class FunctionalArea(HierarchicalType): class Meta: verbose_name = _("Functional area type") verbose_name_plural = _("Functional area types") ordering = ( "parent__label", "label", ) ADMIN_SECTION = _("Finds") post_save.connect(post_save_cache, sender=FunctionalArea) post_delete.connect(post_save_cache, sender=FunctionalArea) class TechnicalAreaType(OrderedHierarchicalType): class Meta: verbose_name = _("Technical area type") verbose_name_plural = _("Technical area types") ordering = ( "order", "parent__label", "label", ) ADMIN_SECTION = _("Finds") post_save.connect(post_save_cache, sender=TechnicalAreaType) post_delete.connect(post_save_cache, sender=TechnicalAreaType) class TechnicalProcessType(OrderedHierarchicalType): class Meta: verbose_name = _("Technical process type") verbose_name_plural = _("Technical process types") ordering = ( "order", "parent__label", "label", ) ADMIN_SECTION = _("Finds") post_save.connect(post_save_cache, sender=TechnicalProcessType) post_delete.connect(post_save_cache, sender=TechnicalProcessType) class ObjectTypeQualityType(GeneralType): order = models.IntegerField(_("Order"), default=10) class Meta: verbose_name = _("Object type quality type") verbose_name_plural = _("Object type quality types") ordering = ("order", "label") ADMIN_SECTION = _("Finds") post_save.connect(post_save_cache, sender=ObjectTypeQualityType) post_delete.connect(post_save_cache, sender=ObjectTypeQualityType) class AlterationType(HierarchicalType): class Meta: verbose_name = _("Alteration type") verbose_name_plural = _("Alteration types") ordering = ( "parent__label", "label", ) ADMIN_SECTION = _("Finds") post_save.connect(post_save_cache, sender=AlterationType) post_delete.connect(post_save_cache, sender=AlterationType) class AlterationCauseType(HierarchicalType): class Meta: verbose_name = _("Alteration cause type") verbose_name_plural = _("Alteration cause types") ordering = ( "parent__label", "label", ) ADMIN_SECTION = _("Finds") post_save.connect(post_save_cache, sender=AlterationCauseType) post_delete.connect(post_save_cache, sender=AlterationCauseType) class TreatmentEmergencyType(GeneralType): class Meta: verbose_name = _("Treatment emergency type") verbose_name_plural = _("Treatment emergency types") ordering = ("label",) ADMIN_SECTION = _("Finds") post_save.connect(post_save_cache, sender=TreatmentEmergencyType) post_delete.connect(post_save_cache, sender=TreatmentEmergencyType) class CommunicabilityType(HierarchicalType): class Meta: verbose_name = _("Communicability type") verbose_name_plural = _("Communicability types") ordering = ( "parent__label", "label", ) ADMIN_SECTION = _("Finds") post_save.connect(post_save_cache, sender=CommunicabilityType) post_delete.connect(post_save_cache, sender=CommunicabilityType) class CheckedType(GeneralType): order = models.IntegerField(_("Order"), default=10) class Meta: verbose_name = _("Checked type") verbose_name_plural = _("Checked types") ordering = ("order", "label") ADMIN_SECTION = _("Finds") post_save.connect(post_save_cache, sender=CheckedType) post_delete.connect(post_save_cache, sender=CheckedType) class DiscoveryMethod(OrderedHierarchicalType): class Meta: verbose_name = _("Discovery method type") verbose_name_plural = _("Discovery method types") ordering = ( "order", "label", ) ADMIN_SECTION = _("Finds") post_save.connect(post_save_cache, sender=DiscoveryMethod) post_delete.connect(post_save_cache, sender=DiscoveryMethod) class CollectionEntryModeType(OrderedHierarchicalType): class Meta: verbose_name = _("Collection entry mode type") verbose_name_plural = _("Collection entry mode types") ordering = ( "order", "parent__label", "label", ) ADMIN_SECTION = _("Museum") post_save.connect(post_save_cache, sender=CollectionEntryModeType) post_delete.connect(post_save_cache, sender=CollectionEntryModeType) class InventoryMarkingPresence(OrderedType): class Meta: verbose_name = _("Presence of inventory marking type") verbose_name_plural = _("Presence of inventory marking types") ordering = ( "order", "label", ) ADMIN_SECTION = _("Museum") post_save.connect(post_save_cache, sender=InventoryMarkingPresence) post_delete.connect(post_save_cache, sender=InventoryMarkingPresence) class MarkingType(OrderedType): class Meta: verbose_name = _("Marking type") verbose_name_plural = _("Marking types") ordering = ( "order", "label", ) ADMIN_SECTION = _("Museum") post_save.connect(post_save_cache, sender=MarkingType) post_delete.connect(post_save_cache, sender=MarkingType) class MuseumCollection(OrderedType): class Meta: verbose_name = _("Museum collection type") verbose_name_plural = _("Museum collection types") ordering = ( "order", "label", ) ADMIN_SECTION = _("Museum") post_save.connect(post_save_cache, sender=MuseumCollection) post_delete.connect(post_save_cache, sender=MuseumCollection) class InventoryConformity(OrderedType): class Meta: verbose_name = _("Inventory conformity type") verbose_name_plural = _("Inventory conformity types") ordering = ( "order", "label", ) ADMIN_SECTION = _("Museum") post_save.connect(post_save_cache, sender=InventoryConformity) post_delete.connect(post_save_cache, sender=InventoryConformity) class OwnerType(OrderedHierarchicalType): class Meta: verbose_name = _("Owner type") verbose_name_plural = _("Owner types") ordering = ( "order", "parent__label", "label", ) ADMIN_SECTION = _("Finds") post_save.connect(post_save_cache, sender=OwnerType) post_delete.connect(post_save_cache, sender=OwnerType) class OwnershipStatus(OrderedHierarchicalType): class Meta: verbose_name = _("Ownership status") verbose_name_plural = _("Ownership status") ordering = ( "order", "parent__label", "label", ) ADMIN_SECTION = _("Finds") post_save.connect(post_save_cache, sender=OwnershipStatus) post_delete.connect(post_save_cache, sender=OwnershipStatus) class OriginalReproduction(OrderedType): class Meta: verbose_name = _("Original/reproduction type") verbose_name_plural = _("Original/reproduction types") ordering = ( "order", "label", ) ADMIN_SECTION = _("Museum") post_save.connect(post_save_cache, sender=OriginalReproduction) post_delete.connect(post_save_cache, sender=OriginalReproduction) class BFBulkView(object): CREATE_SQL = """ CREATE VIEW basefind_cached_bulk_update AS ( SELECT bf.id AS id, ope.code_patriarche AS main_ope_code, ope.year AS year, ope.operation_code AS ope_code, cr.label AS cr_label, bf.index AS index FROM archaeological_finds_basefind bf INNER JOIN archaeological_context_records_contextrecord cr ON cr.id = bf.context_record_id INNER JOIN archaeological_operations_operation ope ON ope.id = cr.operation_id );""" DELETE_SQL = """ DROP VIEW IF EXISTS basefind_cached_bulk_update; """ class BaseFind( BaseHistorizedItem, GeographicSubTownItem, CompleteIdentifierItem, OwnPerms, ValueGetter, SerializeItem, ): EXTERNAL_ID_KEY = "base_find_external_id" EXTERNAL_ID_DEPENDENCIES = ["find"] SLUG = "basefind" SERIALIZE_EXCLUDE = ["find"] SERIALIZE_CALL = {"complete_id": "complete_id", "short_id": "short_id"} UPPER_GEO = ["context_record"] uuid = models.UUIDField(default=uuid.uuid4) label = models.TextField(_("Free ID")) external_id = models.TextField(_("External ID"), blank=True, default="") auto_external_id = models.BooleanField( _("External ID is set automatically"), default=False ) excavation_id = models.TextField(_("Excavation ID"), blank=True, default="") description = models.TextField(_("Description"), blank=True, default="") comment = models.TextField(_("Comment on the circumstances of discovery"), blank=True, default="") special_interest = models.CharField( _("Special interest"), blank=True, default="", max_length=120 ) context_record = models.ForeignKey( ContextRecord, related_name="base_finds", verbose_name=_("Context Record"), on_delete=models.CASCADE, ) discovery_date = models.DateField( _("Discovery date (exact or beginning)"), blank=True, null=True ) discovery_date_taq = models.DateField( _("Discovery date (end)"), blank=True, null=True ) discovery_method = models.ForeignKey( DiscoveryMethod, blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_("Discovery method") ) batch = models.ForeignKey( BatchType, verbose_name=_("Batch/object"), on_delete=models.SET_NULL, blank=True, null=True, ) index = models.IntegerField("Index", default=0) material_index = models.IntegerField(_("Material index"), default=0) topographic_localisation = models.CharField( _("Point of topographic reference"), blank=True, null=True, max_length=120 ) # gis line = models.LineStringField(_("Line"), blank=True, null=True) cache_short_id = models.TextField( _("Short ID - cached"), blank=True, default="", db_index=True, help_text=_("Cached value - do not edit"), ) cache_complete_id = models.TextField( _("Complete ID - cached"), blank=True, default="", db_index=True, help_text=_("Cached value - do not edit"), ) history = HistoricalRecords() RELATED_POST_PROCESS = ["find"] CACHED_LABELS = ["cache_short_id", "cache_complete_id", "cached_label"] CACHED_COMPLETE_ID = "cache_complete_id" PARENT_ONLY_SEARCH_VECTORS = ["context_record"] BASE_SEARCH_VECTORS = [ SearchVectorConfig("label", "raw"), SearchVectorConfig("description", "local"), SearchVectorConfig("comment", "local"), SearchVectorConfig("cache_short_id", "raw"), SearchVectorConfig("cache_complete_id", "raw"), SearchVectorConfig("excavation_id", "raw"), ] DOC_VALUES = [ ("material_type_label", _("Concatenation of associated material types codes")), ] objects = UUIDModelManager() class Meta: verbose_name = _("Base find") verbose_name_plural = _("Base finds") indexes = [ GinIndex(fields=["data"]), ] ADMIN_SECTION = _("Finds") def __str__(self): return self.label def _get_geo_town(self): return self.context_record._get_geo_town() def natural_key(self): return (self.uuid,) def regenerate_external_id(self): self.skip_history_when_saving = True self._no_move = True self.external_id = "" self.auto_external_id = True self.save() @property def operation(self): return self.context_record.operation def public_representation(self): dct = super(BaseFind, self).public_representation() dct.update( { "context-record": self.context_record.public_representation(), "description": self.description, "comment": self.comment, "discovery-date": self.discovery_date and self.discovery_date.strftime("%Y/%m/%d"), "discovery-date-taq": self.discovery_date_taq and self.discovery_date_taq.strftime("%Y/%m/%d"), "discovery-date-tpq": self.discovery_date_taq and self.discovery_date and self.discovery_date.strftime("%Y/%m/%d"), } ) return dct def get_extra_values(self, prefix="", no_values=False, filtr=None, **kwargs): values = {} kwargs["force_no_base_finds"] = True no_find = kwargs.get("no_find", False) if not filtr or prefix + "complete_id" in filtr: values[prefix + "complete_id"] = self.complete_id() exclude = kwargs.get("exclude", []) if (not filtr or prefix + "context_record" in filtr) and \ "context_record" not in exclude: exclude.append("context_record") kwargs["exclude"] = exclude values[prefix + "context_record"] = self.context_record.get_values( filtr=filtr, **kwargs ) if no_find or "finds" in exclude: return values if not filtr or prefix + "finds" in filtr: kwargs["no_base_finds"] = True values[prefix + "finds"] = [ find.get_values(no_values=True, filtr=None, **kwargs) for find in self.find.order_by("pk").all() ] return values def get_last_find(self): # TODO: manage virtuals - property(last_find) ? finds = self.find.filter().order_by("-order").all() return finds and finds[0] def get_main_find(self): """ Get the last find which is not related to many base_find """ for find in self.find.order_by("-pk"): if find.base_finds.count() == 1: return find return def get_town_centroid(self): return self.context_record.get_town_centroid() def get_town_polygons(self): return self.context_record.get_town_polygons() def get_precise_points(self): precise_points = super(BaseFind, self).get_precise_points() if precise_points: return precise_points return self.context_record.get_precise_points() def get_precise_polygons(self): precise_poly = super(BaseFind, self).get_precise_polygons() if precise_poly: return precise_poly return self.context_record.get_precise_polygons() def generate_index(self): """ Generate index based on operation or context record (based on the configuration) :return: True if index has been changed. """ profile = get_current_profile() if profile.find_index == "O": operation = self.context_record.operation q = BaseFind.objects.filter(context_record__operation=operation) elif profile.find_index == "CR": cr = self.context_record q = BaseFind.objects.filter(context_record=cr) else: return False if self.pk: q = q.exclude(pk=self.pk) if q.count(): self.index = q.aggregate(Max("index"))["index__max"] + 1 else: self.index = 1 return True def _ope_code(self): if not self.context_record.operation: return "" profile = get_current_profile() ope = self.context_record.operation c_id = [] if ope.code_patriarche: c_id.append((profile.operation_prefix or "") + ope.code_patriarche) elif ope.year and ope.operation_code: c_id.append( (profile.default_operation_prefix or "") + str(ope.year or "") + "-" + str(ope.operation_code or "") ) else: c_id.append("") return settings.JOINT.join(c_id) def complete_id(self): profile = get_current_profile() if profile.has_overload("basefind_complete_id"): return ALTERNATE_CONFIGS[profile.config].basefind_complete_id(self) # OPE|MAT.CODE|UE|FIND_index c_id = [self._ope_code()] materials = set() for find in self.find.filter(downstream_treatment__isnull=True): for mat in find.material_types.all(): if mat.code: materials.add(mat.code) c_id.append("-".join(sorted(list(materials)))) c_id.append(self.context_record.label) c_id.append( ("{:0" + str(settings.ISHTAR_FINDS_INDEX_ZERO_LEN) + "d}").format( self.index ) ) return settings.JOINT.join(c_id) def _generate_cache_complete_id(self): return self.complete_id() def short_id(self): profile = get_current_profile() if profile.has_overload("basefind_short_id"): return ALTERNATE_CONFIGS[profile.config].basefind_short_id(self) # OPE|FIND_index c_id = [self._ope_code()] c_id.append( ("{:0" + str(settings.ISHTAR_FINDS_INDEX_ZERO_LEN) + "d}").format( self.index ) ) return settings.JOINT.join(c_id) def _generate_cache_short_id(self): return self.short_id() def full_label(self): return self._real_label() or self._temp_label() or "" def material_type_label(self): materials = set() for f in self.find.all(): for m in f.material_types.all(): while m: if m.code: materials.add(m.code) break elif m.parent: m = m.parent else: break return "-".join(materials) def _real_label(self): if ( not self.context_record.parcel or not self.context_record.operation or not self.context_record.operation.code_patriarche ): return find = self.get_last_find() lbl = find.label or self.label return settings.JOINT.join( [ it for it in ( self.context_record.operation.code_patriarche, self.context_record.label, lbl, ) if it ] ) def _temp_label(self): if not self.context_record.parcel: return find = self.get_last_find() lbl = find.label or self.label return settings.JOINT.join( [ str(it) for it in ( self.context_record.parcel.year, self.index, self.context_record.label, lbl, ) if it ] ) @property def name(self): return self.label def get_deleted_data(self) -> dict: """ Return sub object list that will be deleted :return: {"Sub object type": ["Sub object 1", "Sub object 2", ...]} """ if self.find.count() != 1: return {} lbl = str(_("Finds")) data = {lbl: []} for item in self.find.all(): data[lbl].append(str(item)) return data def post_save_basefind(sender, **kwargs): cached_label_changed(sender, **kwargs) post_save_geo(sender, **kwargs) instance = kwargs.get("instance", None) if not instance or not instance.pk: return for f in instance.find.all(): cached_label_changed(Find, instance=f) def pre_delete_basefind(sender, **kwargs): instance = kwargs["instance"] if not instance or not instance.pk: return q = Find.objects.filter(base_finds__pk=instance.pk) for find in q.all(): if find.base_finds.count() == 1: # only associated to the deleted base find find.__base_find_deleted = True # prevent loop find.delete() post_save.connect(post_save_basefind, sender=BaseFind) pre_delete.connect(pre_delete_basefind, sender=BaseFind) m2m_changed.connect(geodata_attached_changed, sender=BaseFind.geodata.through) WEIGHT_UNIT = ( ("g", _("g")), ("kg", _("kg")), ) class FindBasket(Basket, MainItem, ValueGetter): SHOW_URL = "show-findbasket" SLUG = "findbasket" items = models.ManyToManyField("Find", blank=True, related_name="basket", verbose_name=_("Associated finds")) QA_EDIT = QuickAction( url="findbasket-qa-bulk-update", icon_class="fa fa-pencil", text=_("Modify"), target="many", rights=[ "archaeological_finds.view_find", "archaeological_finds.view_own_find" ], ) QUICK_ACTIONS = [ QA_EDIT, QuickAction( url="findbasket-qa-duplicate", icon_class="fa fa-clone", text=_("Duplicate"), target="one", rights=[ "archaeological_finds.view_find", "archaeological_finds.view_own_find" ], ), ] class Meta: verbose_name = _("Basket") ordering = ("label",) permissions = ( ("view_find", "Can view all Finds"), ("view_own_find", "Can view own Find"), ) ADMIN_SECTION = _("Finds") @property def treatments(self): Treatment = apps.get_model("archaeological_finds", "Treatment") return Treatment.objects.filter(associated_basket_id=self.pk) @property def treatments_list(self): return list(self.treatments.all()) @property def treatment_files(self): TreatmentFile = apps.get_model("archaeological_finds", "TreatmentFile") return TreatmentFile.objects.filter(associated_basket_id=self.pk) @property def treatment_files_list(self): return list(self.treatment_files.all()) @property def exhibitions(self): Exhibition = apps.get_model("archaeological_finds", "Exhibition") return Exhibition.objects.filter(associated_basket_id=self.pk) @property def exhibitions_list(self): return list(self.exhibitions.all()) def get_values(self, prefix="", no_values=False, filtr=None, **kwargs): base_exclude = kwargs["exclude"][:] if "exclude" in kwargs else [] base_exclude.append(prefix + "items") kw = kwargs.copy() kw["exclude"] = base_exclude return super().get_values(prefix=prefix, no_values=no_values, filtr=filtr, **kw) def get_extra_values(self, prefix="", no_values=False, filtr=None, **kwargs): values = {} if not filtr or prefix + "items" in filtr: values[prefix + "items"] = [ item.get_values(no_values=True, filtr=filtr, **kwargs) for item in self.items.distinct().all() ] return values def get_extra_actions(self, request): """ For sheet template: return "Manage basket" action """ # url, base_text, icon, extra_text, extra css class, is a quick action if not request.user or not getattr(request.user, "ishtaruser", None): return [] ishtaruser = request.user.ishtaruser actions = [] if self.user == ishtaruser or ishtaruser.pk in [ user.pk for user in self.shared_write_with.all() ]: actions = [ ( reverse("select_itemsinbasket", args=[self.pk]), _("Manage basket"), "fa fa-shopping-basket", "", "", False, ), ] can_edit_find = self.can_do(request, "archaeological_finds.change_find") if can_edit_find: actions += [ ( reverse("findbasket-add-treatment", args=[self.pk]), _("Add treatment"), "fa fa-flask", "", "", False, ), ] if self.can_do(request, "archaeological_finds.add_treatmentfile"): actions += [ ( reverse("findbasket-add-treatmentfile", args=[self.pk]), _("Add treatment file"), "fa fa-file-text-o", "", "", False, ), ] if self.can_do(request, "archaeological_finds.add_exhibition"): actions += [ ( reverse("findbasket-add-exhibition", args=[self.pk]), _("Create exhibition"), "fa fa-users", "", "", False, ), ] if can_edit_find: duplicate = self.get_quick_action_by_url("findbasket-qa-duplicate") actions += [ ( reverse(duplicate.url, args=[self.pk]), duplicate.text, duplicate.icon_class, "", "", True, ), ] return actions post_save.connect(cached_label_changed, sender=FindBasket) class FirstBaseFindView(object): CREATE_SQL = """ CREATE VIEW find_first_base_find AS ( SELECT find_id, min(basefind_id) as basefind_id FROM archaeological_finds_find_base_finds GROUP BY find_id );""" DELETE_SQL = """ DROP VIEW IF EXISTS find_first_base_find; """ class FBulkView(object): CREATE_SQL = """ CREATE VIEW find_cached_bulk_update AS ( SELECT f.id AS id, ope.code_patriarche AS main_ope_code, ope.year AS year, ope.operation_code AS ope_code, f.label AS label, f.index AS index FROM archaeological_finds_find f INNER JOIN find_first_base_find fbf ON fbf.find_id = f.id INNER JOIN archaeological_finds_basefind bf ON fbf.basefind_id = bf.id INNER JOIN archaeological_context_records_contextrecord cr ON cr.id = bf.context_record_id INNER JOIN archaeological_operations_operation ope ON ope.id = cr.operation_id );""" DELETE_SQL = """ DROP VIEW IF EXISTS find_cached_bulk_update; """ def query_loan(is_true=True): """ Query to get loan find :return: (filter, exclude, extra) """ if is_true: return ( Q(container_ref__isnull=False, container__isnull=False), Q(container_ref=F("container")), None, ) else: return ( Q( container_ref__isnull=False, container__isnull=False, container_ref=F("container"), ), None, None, ) class Find( ValueGetter, DocumentItem, BaseHistorizedItem, CompleteIdentifierItem, OwnPerms, MainItem, ): SLUG = "find" APP = "archaeological-finds" MODEL = "find" SHOW_URL = "show-find" DELETE_URL = "delete-find" EXTERNAL_ID_KEY = "find_external_id" TABLE_COLS = [ "external_id", "label", "base_finds__context_record__town__name", "base_finds__context_record__operation__common_name", "base_finds__context_record__label", "cached_materials", "cached_object_types", "cached_periods", "container__cached_label", ] TABLE_COLS_FILTERS = { "container": "archaeological_warehouse.view_container", } if settings.COUNTRY == "fr": TABLE_COLS.insert(3, "base_finds__context_record__operation__code_patriarche") TABLE_COLS_FOR_OPE = [ "base_finds__cache_short_id", "base_finds__cache_complete_id", "previous_id", "label", "cached_materials", "cached_periods", "find_number", "cached_object_types", "container__cached_label", "container_ref__cached_label", "description", "base_finds__context_record__town__name", "base_finds__context_record__parcel", ] TABLE_COLS_FOR_CR = [ "base_finds__cache_short_id", "base_finds__cache_complete_id", "previous_id", "label", "base_finds__context_record__label", "cached_materials", "cached_periods", "find_number", "cached_object_types", "container__cached_label", "container_ref__cached_label", "description", "base_finds__context_record__town__name", "base_finds__context_record__parcel", ] NEW_QUERY_ENGINE = True # if these fields are in the search query columns - add these keys for total count QUERY_DISTINCT_COUNT = { "base_finds__context_record__label": "base_finds__context_record__pk", "base_finds__context_record__operation__code_patriarche": "base_finds__context_record__operation__pk", "base_finds__context_record__operation__common_name": "base_finds__context_record__operation__pk", } COL_LABELS = { "base_finds__context_record__label": _("Context record"), "base_finds__cache_short_id": _("Base find - Short ID"), "base_finds__cache_complete_id": _("Base find - Complete ID"), "base_finds__context_record__operation__code_patriarche": _("Operation (code)"), "base_finds__context_record__town__name": _("Town"), "base_finds__context_record__operation__common_name": _("Operation (name)"), "base_finds__context_record__archaeological_site__name": IshtarSiteProfile.get_default_site_label, "base_finds__context_record__parcel": _("Parcel"), "base_finds__batch": _("Batch"), "base_finds__comment": _("Base find - Comment"), "base_finds__description": _("Base find - Description"), "base_finds__topographic_localisation": _( "Base find - " "Topographic localisation" ), "base_finds__special_interest": _("Base find - Special interest"), "base_finds__discovery_date": _("Base find - Discovery date (exact or beginning)"), "base_finds__discovery_date_taq": _("Base find - Discovery date (end)"), "container__cached_label": _("Current container"), "container_ref__cached_label": _("Reference container"), "datings__period__label": _("Periods"), "cached_periods": _("Periods"), "material_types__label": _("Material types"), "cached_materials": _("Material types"), "object_types__label": _("Object types"), "cached_object_types": _("Object types"), } EXTRA_FULL_FIELDS = [ "datings", "base_finds__cache_short_id", "base_finds__cache_complete_id", "base_finds__comment", "base_finds__description", "base_finds__topographic_localisation", "base_finds__special_interest", "base_finds__discovery_date", "base_finds__discovery_date_taq", ] ATTRS_EQUIV = {"get_first_base_find": "base_finds"} # statistics STATISTIC_MODALITIES_OPTIONS = OrderedDict( [ ( "base_finds__context_record__operation__operation_type__label", _("Operation type"), ), ( "base_finds__context_record__operation__cached_label", _("Operation"), ), ("base_finds__context_record__operation__year", _("Year")), ("base_finds__context_record__operation__towns__areas__label", _("Area")), ( "base_finds__context_record__operation__towns__areas__parent__label", _("Extended area"), ), ("datings__period__label", _("Chronological period")), ("material_types__label", _("Material type")), ("object_types__label", _("Object type")), ("preservation_to_considers__label", _("Recommended treatments")), ("conservatory_states__label", _("Conservatory states")), ("integrities__label", _("Integrity")), ("remarkabilities__label", _("Remarkability")), ("communicabilities__label", _("Communicability")), ("checked_type__label", _("Check")), ("alterations__label", _("Alteration")), ("alteration_causes__label", _("Alteration cause")), ("treatment_emergency__label", _("Treatment emergency")), ("documents__source_type__label", _("Associated document type")), ("last_modified__year", _("Modification (year)")), ] ) STATISTIC_MODALITIES = [key for key, lbl in STATISTIC_MODALITIES_OPTIONS.items()] STATISTIC_SUM_VARIABLE = OrderedDict( ( ("pk", (_("Number"), 1)), ("weight", (_("Weight (kg)"), 0.001)), ("find_number", (_("Number of remains"), 1)), ("estimated_value", (_("Estimated value"), 1)), ("insurance_value", (_("Insurance value"), 1)), ) ) # search parameters REVERSED_BOOL_FIELDS = [ "documents__image__isnull", "documents__associated_url__isnull", "documents__associated_file__isnull", ] BOOL_FIELDS = BaseHistorizedItem.BOOL_FIELDS + ["is_complete"] CALLABLE_BOOL_FIELDS = ["loan"] RELATION_TYPES_PREFIX = { "ope_relation_types": "base_finds__context_record__operation__", "cr_relation_types": "base_finds__context_record__", } DATED_FIELDS = BaseHistorizedItem.DATED_FIELDS + [ "basket__treatment_files__end_date", "treatments__end_date", "base_finds__discovery_date", "base_finds__discovery_date_taq", "check_date", "appraisal_date", "museum_entry_date", "museum_entry_date_end", "museum_allocation_date", ] NUMBER_FIELDS = [ "base_finds__context_record__operation__year", "base_finds__context_record__operation__operation_code", "insurance_value", "estimated_value", "length", "width", "height", "thickness", "diameter", "circumference", "volume", "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 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", "base_finds__context_record": "base_finds__context_record__pk", "base_finds__context_record__archaeological_site": "base_finds__context_record__archaeological_site__pk", "archaeological_sites_context_record": "base_finds__context_record__archaeological_site__pk", "base_finds__context_record__operation__year": "base_finds__context_record__operation__year__contains", "base_finds__context_record__operation": "base_finds__context_record__operation__pk", "base_finds__context_record__operation__operation_type": "base_finds__context_record__operation__operation_type__pk", "archaeological_sites": "base_finds__context_record__operation__archaeological_sites__pk", "base_finds__context_record__operation__code_patriarche": "base_finds__context_record__operation__code_patriarche", "base_finds__context_record__town__areas": "base_finds__context_record__town__areas__pk", "base_finds__context_record__archaeological_site__name": "base_finds__context_record__archaeological_site__name", "datings__period": "datings__period__pk", "description": "description__icontains", "base_finds__batch": "base_finds__batch", "basket_id": "basket__pk", "denomination": "denomination", "cached_label": "cached_label__icontains", "documents__image__isnull": "documents__image__isnull", "container__location": "container__location__pk", "container_ref__location": "container_ref__location__pk", } for table in (TABLE_COLS, TABLE_COLS_FOR_OPE): for key in table: if key not in EXTRA_REQUEST_KEYS.keys(): EXTRA_REQUEST_KEYS[key] = key # alternative names of fields for searches ALT_NAMES = { "base_finds__cache_short_id": SearchAltName( pgettext_lazy("key for text search", "short-id"), "base_finds__cache_short_id__iexact", ), "base_finds__cache_complete_id": SearchAltName( pgettext_lazy("key for text search", "complete-id"), "base_finds__cache_complete_id__iexact", ), "label": SearchAltName( pgettext_lazy("key for text search", "free-id"), "label__iexact" ), "denomination": SearchAltName( pgettext_lazy("key for text search", "denomination"), "denomination__iexact" ), "base_finds__context_record__town": SearchAltName( pgettext_lazy("key for text search", "town"), "base_finds__context_record__town__cached_label__iexact", ), "base_finds__context_record__operation__year": SearchAltName( pgettext_lazy("key for text search", "year"), "base_finds__context_record__operation__year", ), "base_finds__context_record__operation__operation_code": SearchAltName( pgettext_lazy("key for text search", "operation-code"), "base_finds__context_record__operation__operation_code", ), "base_finds__context_record__operation__code_patriarche": SearchAltName( pgettext_lazy("key for text search", "code-patriarche"), "base_finds__context_record__operation__code_patriarche__iexact", ), "base_finds__context_record__operation__operation_type": SearchAltName( pgettext_lazy("key for text search", "operation-type"), "base_finds__context_record__operation__operation_type" "__label__iexact", ), "base_finds__context_record__town__areas": SearchAltName( pgettext_lazy("key for text search", "area"), "base_finds__context_record__town__areas__label__iexact", ), "archaeological_sites": SearchAltName( pgettext_lazy("key for text search", "site"), "base_finds__context_record__operation__archaeological_sites__" "cached_label__iexact", ), "archaeological_sites_name": SearchAltName( pgettext_lazy("key for text search", "site-name"), "base_finds__context_record__operation__archaeological_sites__" "name__iexact", ), "archaeological_sites_context_record": SearchAltName( pgettext_lazy("key for text search", "context-record-site"), "base_finds__context_record__archaeological_site__" "cached_label__iexact", ), "archaeological_sites_context_record_name": SearchAltName( pgettext_lazy("key for text search", "context-record-site-name"), "base_finds__context_record__archaeological_site__name__iexact", ), "base_finds__context_record": SearchAltName( pgettext_lazy("key for text search", "context-record"), "base_finds__context_record__cached_label__iexact", ), "base_finds__context_record__unit": SearchAltName( pgettext_lazy("key for text search", "context-record-type"), "base_finds__context_record__unit__label__iexact", ), "base_finds__comment": SearchAltName( pgettext_lazy("key for text search", "discovery-comment"), "base_finds__comment__iexact", ), "ope_relation_types": SearchAltName( pgettext_lazy("key for text search", "operation-relation-type"), "ope_relation_types", ), "cr_relation_types": SearchAltName( pgettext_lazy("key for text search", "context-record-relation-type"), "cr_relation_types", ), "material_types": SearchAltName( pgettext_lazy("key for text search", "material"), "material_types__label__iexact", related_name="material_types", ), "object_types": SearchAltName( pgettext_lazy("key for text search", "object-type"), "object_types__label__iexact", ), "preservation_to_considers": SearchAltName( pgettext_lazy("key for text search", "recommended-treatments"), "preservation_to_considers__label__iexact", ), "conservatory_states": SearchAltName( pgettext_lazy("key for text search", "conservatory"), "conservatory_states__label__iexact", ), "integrities": SearchAltName( pgettext_lazy("key for text search", "integrity"), "integrities__label__iexact", ), "remarkabilities": SearchAltName( pgettext_lazy("key for text search", "remarkability"), "remarkabilities__label__iexact", ), "description": SearchAltName( pgettext_lazy("key for text search", "description"), "description__iexact" ), "base_finds__batch": SearchAltName( pgettext_lazy("key for text search", "batch"), "base_finds__batch__label__iexact", ), "checked_type": SearchAltName( pgettext_lazy("key for text search", "checked"), "checked_type__label__iexact", ), "container_ref": SearchAltName( pgettext_lazy("key for text search", "container"), "container_ref__cached_label__iexact", ), "container_ref__location": SearchAltName( pgettext_lazy("key for text search", "location"), "container_ref__location__name__iexact", ), "container__location": SearchAltName( pgettext_lazy("key for text search", "current-location"), "container__location__name__iexact", ), "container": SearchAltName( pgettext_lazy("key for text search", "current-container"), "container__cached_label__iexact", ), "basket": SearchAltName( pgettext_lazy("key for text search", "basket"), "basket__label__iexact" ), "base_finds__context_record__operation__cached_label": SearchAltName( pgettext_lazy("key for text search", "operation"), "base_finds__context_record__operation__cached_label__iexact", ), "history_modifier": SearchAltName( pgettext_lazy("key for text search", "last-modified-by"), "history_modifier__ishtaruser__person__cached_label__iexact", ), "history_creator": SearchAltName( pgettext_lazy("key for text search", "created-by"), "history_creator__ishtaruser__person__cached_label__iexact", ), "loan": SearchAltName(pgettext_lazy("key for text search", "loan"), query_loan), "treatments_file_end_date": SearchAltName( pgettext_lazy("key for text search", "treatment-file-end-date"), "basket__treatment_files__end_date", ), "treatments_end_date": SearchAltName( pgettext_lazy("key for text search", "treatment-end-date"), "treatments__end_date", ), "previous_id": SearchAltName( pgettext_lazy("key for text search", "previous-id"), "previous_id__iexact" ), #'collection': # SearchAltName( # pgettext_lazy("key for text search", "collection"), # 'collection__name__iexact'), "seal_number": SearchAltName( pgettext_lazy("key for text search", "seal-number"), "seal_number__iexact" ), "base_finds__excavation_id": SearchAltName( pgettext_lazy("key for text search", "excavation-id"), "base_finds__excavation_id__iexact", ), "museum_id": SearchAltName( pgettext_lazy("key for text search", "museum-id"), "museum_id__iexact" ), "cache_complete_museum_id": SearchAltName( 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"), "laboratory_id__iexact", ), "mark": SearchAltName( pgettext_lazy("key for text search", "mark"), "mark__iexact" ), "base_finds__discovery_date": SearchAltName( pgettext_lazy("key for text search", "discovery-date"), "base_finds__discovery_date", ), "base_finds__discovery_date_taq": SearchAltName( pgettext_lazy("key for text search", "discovery-date-taq"), "base_finds__discovery_date_taq", ), "is_complete": SearchAltName( pgettext_lazy("key for text search", "is-complete"), "is_complete" ), "material_type_quality": SearchAltName( pgettext_lazy("key for text search", "material-type-quality"), "material_type_quality__label__iexact", ), "object_type_quality": SearchAltName( pgettext_lazy("key for text search", "object-type-quality"), "object_type_quality__label__iexact", ), "find_number": SearchAltName( pgettext_lazy("key for text search", "find-number"), "find_number" ), "min_number_of_individuals": SearchAltName( pgettext_lazy("key for text search", "min-number-of-individuals"), "min_number_of_individuals", ), "decoration": SearchAltName( pgettext_lazy("key for text search", "decoration"), "decoration__iexact" ), "inscription": SearchAltName( pgettext_lazy("key for text search", "inscription"), "inscription__iexact" ), "manufacturing_place": SearchAltName( pgettext_lazy("key for text search", "manufacturing-place"), "manufacturing_place__iexact", ), "communicabilities": SearchAltName( pgettext_lazy("key for text search", "communicabilities"), "communicabilities__label__iexact", ), "comment": SearchAltName( pgettext_lazy("key for text search", "comment"), "comment__iexact" ), "material_comment": SearchAltName( pgettext_lazy("key for text search", "material-comment"), "material_comment__iexact", ), "dating_comment": SearchAltName( pgettext_lazy("key for text search", "dating-comment"), "dating_comment__iexact", ), "conservatory_comment": SearchAltName( pgettext_lazy("key for text search", "conservatory-comment"), "conservatory_comment__iexact", ), "length": SearchAltName( pgettext_lazy("key for text search", "length"), "length" ), "width": SearchAltName( pgettext_lazy("key for text search", "width"), "width" ), "height": SearchAltName( pgettext_lazy("key for text search", "height"), "height" ), "thickness": SearchAltName( pgettext_lazy("key for text search", "thickness"), "thickness" ), "diameter": SearchAltName( pgettext_lazy("key for text search", "diameter"), "diameter" ), "circumference": SearchAltName( pgettext_lazy("key for text search", "circumference"), "circumference", ), "volume": SearchAltName( pgettext_lazy("key for text search", "volume"), "volume" ), "weight": SearchAltName( pgettext_lazy("key for text search", "weight"), "weight" ), "clutter_long_side": SearchAltName( pgettext_lazy("key for text search", "clutter-long-side"), "clutter_long_side", ), "clutter_short_side": SearchAltName( pgettext_lazy("key for text search", "clutter-short-side"), "clutter_short_side", ), "clutter_height": SearchAltName( pgettext_lazy("key for text search", "clutter-height"), "clutter_height", ), "dimensions_comment": SearchAltName( pgettext_lazy("key for text search", "dimensions-comment"), "dimensions_comment__iexact", ), "base_finds__discovery_method": SearchAltName( pgettext_lazy("key for text search", "discovery-method"), "base_finds__discovery_method__label__iexact", ), "base_finds__topographic_localisation": SearchAltName( pgettext_lazy("key for text search", "topographic-localisation"), "base_finds__topographic_localisation__iexact", ), "check_date": SearchAltName( pgettext_lazy("key for text search", "check-date"), "check_date" ), "alterations": SearchAltName( pgettext_lazy("key for text search", "alterations"), "alterations__label__iexact", ), "alteration_causes": SearchAltName( pgettext_lazy("key for text search", "alteration-causes"), "alteration_causes__label__iexact", ), "treatment_emergency": SearchAltName( pgettext_lazy("key for text search", "treatment-emergency"), "treatment_emergency__label__iexact", ), "estimated_value": SearchAltName( pgettext_lazy("key for text search", "estimated-value"), "estimated_value", ), "insurance_value": SearchAltName( pgettext_lazy("key for text search", "insurance-value"), "insurance_value", ), "appraisal_date": SearchAltName( pgettext_lazy("key for text search", "appraisal-date"), "appraisal_date", ), "owner": SearchAltName( pgettext_lazy("key for text search", "owner"), "owner__label__iexact", ), "ownership_status": SearchAltName( pgettext_lazy("key for text search", "ownership"), "ownership_status__label__iexact", ), "cultural_attributions": SearchAltName( pgettext_lazy("key for text search", "cultural-attribution"), "cultural_attributions__label__iexact", ), "functional_areas": SearchAltName( pgettext_lazy("key for text search", "functional-area"), "functional_areas__label__iexact", ), "technical_areas": SearchAltName( pgettext_lazy("key for text search", "technical-area"), "technical_areas__label__iexact", ), "technical_processes": SearchAltName( pgettext_lazy("key for text search", "technical-process"), "technical_processes__label__iexact", ), "base_finds__context_record__operation__address": SearchAltName( pgettext_lazy("key for text search", "operation-address"), "base_finds__context_record__operation__address__iexact", ), "base_finds__context_record__operation__in_charge": SearchAltName( pgettext_lazy("key for text search", "in-charge"), "base_finds__context_record__operation__in_charge__cached_label__iexact", ), "base_finds__context_record__operation__scientist": SearchAltName( pgettext_lazy("key for text search", "scientist"), "base_finds__context_record__operation__scientist__cached_label__iexact", ), "base_finds__context_record__operation__operator": SearchAltName( pgettext_lazy("key for text search", "operator"), "base_finds__context_record__operation__operator__cached_label__iexact", ), "base_finds__context_record__operation__common_name": SearchAltName( pgettext_lazy("key for text search", "operation-name"), "base_finds__context_record__operation__common_name__iexact", ), "base_finds__context_record__operation__remains": SearchAltName( pgettext_lazy("key for text search", "operation-remain"), "base_finds__context_record__operation__remains__label__iexact" ), "base_finds__context_record__archaeological_site__remains": SearchAltName( pgettext_lazy("key for text search", "site-remain"), "base_finds__context_record__archaeological_site__remains__label__iexact" ), "museum_id_comment": SearchAltName( pgettext_lazy("key for text search", "museum-id-comment"), "museum_id_comment__iexact" ), "museum_owner_institution": SearchAltName( pgettext_lazy("key for text search", "museum-owner-institution"), "museum_owner_institution__name__iexact", related_name="museum_owner_institution" ), "museum_assigned_institution": SearchAltName( pgettext_lazy("key for text search", "museum-assigned-institution"), "museum_assigned_institution__name__iexact", related_name="museum_assigned_institution" ), "museum_custodian_institution": SearchAltName( pgettext_lazy("key for text search", "museum-custodian-institution"), "museum_custodian_institution__name__iexact", related_name="museum_custodian_institution" ), "museum_depositor_inventory_number": SearchAltName( pgettext_lazy("key for text search", "museum-depositor-inventory-number"), "museum_depositor_inventory_number__iexact" ), "museum_collection_entry_mode": SearchAltName( pgettext_lazy("key for text search", "museum-collection-entry-mode"), "museum_collection_entry_mode__label__iexact" ), "museum_entry_mode_comment": SearchAltName( pgettext_lazy("key for text search", "museum-entry-mode-comment"), "museum_entry_mode_comment__iexact" ), "museum_entry_date": SearchAltName( pgettext_lazy("key for text search", "museum-entry-date"), "museum_entry_date" ), "museum_entry_date_end": SearchAltName( pgettext_lazy("key for text search", "museum-entry-date-end"), "museum_entry_date_end" ), "museum_entry_date_comment": SearchAltName( pgettext_lazy("key for text search", "museum-entry-date-comment"), "museum_entry_date_comment__iexact" ), "museum_donors": SearchAltName( pgettext_lazy("key for text search", "museum-donors"), "museum_donors__denomination__iexact", related_name="museum_donor" ), "museum_inventory_marking_presence": SearchAltName( pgettext_lazy("key for text search", "museum-inventory-marking-presence"), "museum_inventory_marking_presence__label__iexact" ), "museum_marking_type": SearchAltName( pgettext_lazy("key for text search", "museum-marking-type"), "museum_marking_type__label__iexact" ), "museum_collections": SearchAltName( pgettext_lazy("key for text search", "museum-collections"), "museum_collections__label__iexact", related_name="museum_collections" ), "museum_former_collections": SearchAltName( pgettext_lazy("key for text search", "museum-former-collection"), "museum_former_collections__denomination__iexact", related_name="museum_former_collections" ), "museum_inventory_entry_year": SearchAltName( pgettext_lazy("key for text search", "museum-inventory-entry-year"), "museum_inventory_entry_year" ), "museum_inventory_conformity": SearchAltName( pgettext_lazy("key for text search", "museum-inventory-conformity"), "museum_inventory_conformity__label__iexact", related_name="museum_inventory_conformity" ), "museum_conformity_comment": SearchAltName( pgettext_lazy("key for text search", "museum-conformity-comment"), "museum_conformity_comment__iexact" ), "museum_inventory_transcript": SearchAltName( pgettext_lazy("key for text search", "museum-inventory-transcript"), "museum_inventory_transcript__iexact" ), "museum_original_repro": SearchAltName( pgettext_lazy("key for text search", "museum-original-repro"), "museum_original_repro__label__iexact", related_name="museum_original_repro" ), "museum_allocation_date": SearchAltName( pgettext_lazy("key for text search", "museum-allocation-date"), "museum_allocation_date" ), "museum_purchase_price": SearchAltName( pgettext_lazy("key for text search", "museum-purchase-price"), "museum_purchase_price__iexact" ), "museum_inventory_quantity": SearchAltName( pgettext_lazy("key for text search", "museum-inventory-quantity"), "museum_inventory_quantity" ), "museum_observed_quantity": SearchAltName( pgettext_lazy("key for text search", "museum-observed-quantity"), "museum_observed_quantity" ), } 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) DEFAULT_SEARCH_FORM = ("archaeological_finds.forms", "FindSelect") """ # kept as an example DYNAMIC_REQUESTS = { 'current_division': DynamicRequest( label=_("Division current -"), app_name='archaeological_warehouse', model_name='WarehouseDivision', form_key='current_division', search_key=pgettext_lazy("key for text search", 'current-division'), type_query='container__division__division__division__txt_idx', search_query='container__division__reference__iexact' ), 'reference_division': DynamicRequest( label=_("Division reference -"), app_name='archaeological_warehouse', model_name='WarehouseDivision', form_key='reference_division', search_key=pgettext_lazy("key for text search", 'reference-division'), type_query='container_ref__division__division__division__txt_idx', search_query='container_ref__division__reference__iexact' ), } """ PARENT_SEARCH_VECTORS = ["base_finds"] PARENT_ONLY_SEARCH_VECTORS = ["container"] BASE_SEARCH_VECTORS = [ SearchVectorConfig("cached_label", "raw"), SearchVectorConfig("index", "raw"), SearchVectorConfig("cache_complete_museum_id", "raw"), SearchVectorConfig("museum_id", "raw"), SearchVectorConfig("label", "raw"), SearchVectorConfig("description", "local"), SearchVectorConfig("museum_id_comment", "local"), SearchVectorConfig("mark"), SearchVectorConfig("comment", "local"), SearchVectorConfig("dating_comment", "local"), SearchVectorConfig("previous_id", "raw"), SearchVectorConfig("denomination"), SearchVectorConfig("museum_id", "raw"), SearchVectorConfig("laboratory_id", "raw"), SearchVectorConfig("decoration"), SearchVectorConfig("manufacturing_place"), SearchVectorConfig("museum_owner_institution__name", "raw"), SearchVectorConfig("museum_assigned_institution__name", "raw"), SearchVectorConfig("museum_custodian_institution__name", "raw"), SearchVectorConfig("museum_depositor_inventory_number", "raw"), SearchVectorConfig("museum_entry_mode_comment"), SearchVectorConfig("museum_entry_date_comment", "local"), SearchVectorConfig("museum_donors__denomination"), SearchVectorConfig("museum_collections__label"), SearchVectorConfig("museum_former_collections__denomination"), SearchVectorConfig("museum_inventory_transcript", "local"), ] M2M_SEARCH_VECTORS = [ SearchVectorConfig("datings__period__label", "local"), SearchVectorConfig("integrities__label", "raw"), SearchVectorConfig("material_types__label", "local"), SearchVectorConfig("object_types__label", "raw"), SearchVectorConfig("remarkabilities__label", "raw"), SearchVectorConfig("technical_processes__label", "raw"), ] QA_EDIT = QuickAction( url="find-qa-bulk-update", icon_class="fa fa-pencil", text=_("Bulk update"), target="many", rights=["archaeological_finds.change_find", "archaeological_finds.change_own_find"], ) QA_LINK = QuickAction( url="find-qa-link", icon_class="fa fa-link", text=_("Link to account"), target="many", rights=["ishtaradmin"], btn_class="btn-warning" ) QA_LOCK = QuickAction( url="find-qa-lock", icon_class="fa fa-lock", text=_("Lock/Unlock"), target="many", rights=["archaeological_finds.change_find", "archaeological_finds.change_own_find"], btn_class="btn-warning" ) QUICK_ACTIONS = [ QA_EDIT, QuickAction( url="find-qa-duplicate", icon_class="fa fa-clone", text=_("Duplicate"), target="one", rights=[ "archaeological_finds.change_find", "archaeological_finds.change_own_find" ], ), QuickAction( url="find-qa-basket", icon_class="fa fa-shopping-basket", text=_("Basket"), target="many", rights=[ "archaeological_finds.change_find", "archaeological_finds.change_own_find" ], ), QuickAction( url="find-qa-packaging", icon_class="fa fa-gift", text=_("Packaging"), target="many", rights=[ "archaeological_finds.change_find", "archaeological_finds.change_own_find" ], module="warehouse", ), QuickAction( url="treatment-n1-create", icon_class="fa fa-object-group", text=_("Treatment many to one"), target="many", rights=[ "archaeological_finds.change_find", "archaeological_finds.change_own_find" ], is_popup=False, ), QA_LOCK, QA_LINK ] UP_MODEL_QUERY = { "operation": ( pgettext_lazy("key for text search", "operation"), "cached_label", ), "contextrecord": ( pgettext_lazy("key for text search", "context-record"), "cached_label", ), "warehouse": (pgettext_lazy("key for text search", "location"), "name"), "site": ( pgettext_lazy("key for text search", "context-record-site"), "cached_label", ), } RELATIVE_SESSION_NAMES = [ ("contextrecord", "base_finds__context_record__pk"), ("operation", "base_finds__context_record__operation__pk"), ("file", "base_finds__context_record__operation__associated_file__pk"), ("warehouse", "container__location__pk"), ("site", "base_finds__context_record__archaeological_site__pk"), ] HISTORICAL_M2M = [ "material_types", "technical_processes", "datings", "cultural_attributions", "conservatory_states", "object_types", "functional_areas", "technical_areas", "integrities", "remarkabilities", "communicabilities", "museum_inventory_marking_presence", "museum_marking_type", "museum_former_collections", "preservation_to_considers", "alterations", "alteration_causes", ] GET_VALUES_EXTRA = ValueGetter.GET_VALUES_EXTRA + ["complete_id", "context_record_label"] GET_VALUES_M2M = [ "alterations", "alteration_causes", "communicabilities", "conservatory_states", "cultural_attributions", "functional_areas", "material_types", "integrities", "preservation_to_considers", "museum_former_collections", "museum_inventory_marking_presence", "museum_marking_type", "object_types", "preservation_to_considers", "remarkabilities", "technical_areas", "technical_processes", ] CACHED_LABELS = [ "cache_complete_museum_id", "cached_label", "cached_periods", "cached_object_types", "cached_materials", ] SERIALIZE_CALL = { "base_finds_list": "base_finds_list", "documents_list": "documents_list", "m2m_listing_datings": "m2m_listing_datings", } SERIALIZE_PROPERTIES = MainItem.SERIALIZE_PROPERTIES + [ "administrative_index", "integrities_count", "conservatory_states_count", "remarkabilities_count", "cultural_attributions_count", "documents_count", "excavation_ids", "weight_string", ] UPPER_PERMISSIONS = [ (ContextRecord, "base_finds__context_record_id"), (("archaeological_warehouse", "Warehouse"), "container__location_id"), (("archaeological_warehouse", "Warehouse"), "container_ref__responsibility_id"), ] SHEET_ALTERNATIVES = [("museum", "museum_find")] SHEET_EMPTY_KEYS = ["container_ref", "upstream_treatment", "downstream_treatment", "documents_count", "m2m_listing"] DEFAULT_WIZARD = reverse_lazy("find_search", args=["generalwarehouse-find_search"]) objects = UUIDModelManager() # fields uuid = models.UUIDField(default=uuid.uuid4) base_finds = models.ManyToManyField( BaseFind, verbose_name=_("Base find"), related_name="find" ) external_id = models.TextField(_("External ID"), blank=True, default="") auto_external_id = models.BooleanField( _("External ID is set automatically"), default=False ) # judiciary operation seal_number = models.TextField(_("Seal number"), blank=True, default="") order = models.IntegerField(_("Order"), default=1) label = models.TextField(_("Free ID")) denomination = models.TextField(_("Denomination"), 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="") museum_id_suffix = models.TextField(_("Museum ID suffix"), blank=True, default="") museum_id_comment = models.TextField(_("Comment on museum ID"), blank=True, default="") laboratory_id = models.TextField(_("Laboratory ID"), blank=True, default="") description = models.TextField(_("Description"), blank=True, default="") decoration = models.TextField(_("Decoration"), blank=True, default="") inscription = models.TextField(_("Inscription"), blank=True, default="") manufacturing_place = models.TextField( _("Manufacturing place"), blank=True, default="" ) material_types = models.ManyToManyField( MaterialType, verbose_name=_("Material types"), related_name="finds", blank=True ) material_type_quality = models.ForeignKey( MaterialTypeQualityType, verbose_name=_("Material type quality"), related_name="finds", on_delete=models.SET_NULL, blank=True, null=True, ) technical_processes = models.ManyToManyField( TechnicalProcessType, verbose_name=_("Technical processes"), related_name="find", blank=True, ) material_comment = models.TextField( _("Comment on the material"), blank=True, default="" ) volume = models.FloatField(_("Volume (l)"), blank=True, null=True) weight = models.FloatField(_("Weight"), blank=True, null=True) weight_unit = models.CharField( _("Weight unit"), max_length=4, blank=True, null=True, choices=WEIGHT_UNIT ) find_number = models.IntegerField(_("Number of remains"), blank=True, null=True) min_number_of_individuals = models.IntegerField( _("Minimum number of individuals (MNI)"), blank=True, null=True ) quantity_comment = models.TextField( _("Comment on quantity"), blank=True, default="" ) upstream_treatment = models.ForeignKey( "Treatment", blank=True, null=True, related_name="downstream", on_delete=models.SET_NULL, verbose_name=_("Upstream treatment"), ) downstream_treatment = models.ForeignKey( "Treatment", blank=True, null=True, related_name="upstream", verbose_name=_("Downstream treatment"), on_delete=models.SET_NULL, ) datings = models.ManyToManyField( Dating, verbose_name=_("Dating"), related_name="find" ) cultural_attributions = models.ManyToManyField( CulturalAttributionType, verbose_name=_("Cultural attribution"), blank=True ) container = models.ForeignKey( "archaeological_warehouse.Container", verbose_name=_("Container"), blank=True, null=True, related_name="finds", on_delete=models.SET_NULL, ) container_fisrt_full_location = models.TextField( _("Container - first full location"), default="", blank=True, help_text=_("Updated as long as no packaging is attached") ) container_ref = models.ForeignKey( "archaeological_warehouse.Container", verbose_name=_("Reference container"), blank=True, null=True, related_name="finds_ref", on_delete=models.SET_NULL, ) container_ref_fisrt_full_location = models.TextField( _("Reference container - first full location"), default="", blank=True, help_text=_("Updated as long as no packaging is attached") ) is_complete = models.NullBooleanField(_("Is complete?"), blank=True, null=True) object_types = models.ManyToManyField( ObjectType, verbose_name=_("Object types"), related_name="find", blank=True ) object_type_quality = models.ForeignKey( ObjectTypeQualityType, verbose_name=_("Object type quality"), related_name="finds", on_delete=models.SET_NULL, blank=True, null=True, ) functional_areas = models.ManyToManyField( FunctionalArea, verbose_name=_("Functional areas"), related_name="find", blank=True, ) technical_areas = models.ManyToManyField( TechnicalAreaType, verbose_name=_("Technical areas"), related_name="find", blank=True, ) integrities = models.ManyToManyField( IntegrityType, verbose_name=_("Integrity"), related_name="find", blank=True, ) remarkabilities = models.ManyToManyField( RemarkabilityType, verbose_name=_("Remarkability"), related_name="find", blank=True, ) communicabilities = models.ManyToManyField( CommunicabilityType, verbose_name=_("Communicability"), related_name="find", blank=True, ) length = models.FloatField(_("Length (cm)"), blank=True, null=True) width = models.FloatField(_("Width (cm)"), blank=True, null=True) height = models.FloatField(_("Height (cm)"), blank=True, null=True) diameter = models.FloatField(_("Diameter (cm)"), blank=True, null=True) circumference = models.FloatField(_("Circumference (cm)"), blank=True, null=True) thickness = models.FloatField(_("Thickness (cm)"), blank=True, null=True) clutter_long_side = models.FloatField( _("Clutter - long side (cm)"), blank=True, null=True ) clutter_short_side = models.FloatField( _("Clutter - short side (cm)"), blank=True, null=True ) clutter_height = models.FloatField( _("Clutter - height (cm)"), blank=True, null=True ) dimensions_comment = models.TextField( _("Dimensions comment"), blank=True, default="" ) mark = models.TextField(_("Mark"), 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="") index = models.IntegerField("Index", default=0) checked_type = models.ForeignKey( CheckedType, verbose_name=_("Check"), on_delete=models.SET_NULL, blank=True, null=True, ) check_date = models.DateField(_("Check date"), default=datetime.date.today) estimated_value = models.FloatField(_("Estimated value"), blank=True, null=True) ownership_status = models.ForeignKey( OwnershipStatus, blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_("Ownership status"), ) owner = models.ForeignKey( OwnerType, blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_("Owner"), ) # museum module museum_owner_institution = models.ForeignKey( Organization, blank=True, null=True, on_delete=models.SET_NULL, related_name="owns", verbose_name=_("Owner institution"), ) museum_assigned_institution = models.ForeignKey( Organization, blank=True, null=True, on_delete=models.SET_NULL, related_name="assigned", verbose_name=_("Assigned institution"), ) museum_custodian_institution = models.ForeignKey( Organization, blank=True, null=True, on_delete=models.SET_NULL, related_name="deposited", verbose_name=_("Custodian institution"), ) museum_depositor_inventory_number = models.TextField(_("Depositor inventory number"), blank=True, default="") museum_collection_entry_mode = models.ForeignKey( CollectionEntryModeType, blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_("Collections entry mode"), ) museum_entry_mode_comment = models.TextField(_("Comment on museum entry mode"), blank=True, default="") museum_entry_date = models.DateField(_("Museum entry date (exact or start)"), blank=True, null=True) museum_entry_date_end = models.DateField(_("Museum entry date (end)"), blank=True, null=True) museum_entry_date_comment = models.TextField(_("Comment on museum entry date"), blank=True, default="") museum_donors = models.ManyToManyField( BiographicalNote, blank=True, related_name="finds_donors", verbose_name=_("Donors, testators or vendors"), ) museum_inventory_marking_presence = models.ManyToManyField( InventoryMarkingPresence, blank=True, related_name="finds", verbose_name=_("Presence of inventory marking"), ) museum_marking_type = models.ManyToManyField( MarkingType, verbose_name=_("Type of marking"), blank=True, related_name="finds", ) museum_collections = models.ManyToManyField( MuseumCollection, blank=True, verbose_name=_("Collections"), ) museum_former_collections = models.ManyToManyField( BiographicalNote, blank=True, related_name="finds_former_collections", verbose_name=_("Former collection"), ) museum_inventory_entry_year = models.PositiveIntegerField( _("Inventory entry year"), blank=True, null=True ) museum_inventory_conformity = models.ForeignKey( InventoryConformity, blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_("Conformity with inventory"), ) museum_conformity_comment = models.TextField(_("Comment on conformity"), blank=True, default="") museum_inventory_transcript = models.TextField(_("Inventory transcript"), blank=True, default="") museum_original_repro = models.ForeignKey( OriginalReproduction, blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_("Original/reproduction"), ) museum_allocation_date = models.DateField(_("Date of museum allocation"), blank=True, null=True) museum_purchase_price = models.TextField(_("Purchase price"), blank=True, default="") museum_inventory_quantity = models.PositiveSmallIntegerField(_("Inventory quantity"), blank=True, null=True) museum_observed_quantity = models.PositiveSmallIntegerField(_("Observed quantity"), blank=True, null=True) # preservation module conservatory_states = models.ManyToManyField( ConservatoryState, verbose_name=_("Conservatory states"), blank=True, ) conservatory_comment = models.TextField( _("Conservatory comment"), blank=True, default="" ) preservation_to_considers = models.ManyToManyField( TreatmentType, verbose_name=_("Recommended treatments"), related_name="finds_recommended", blank=True, ) alterations = models.ManyToManyField( AlterationType, verbose_name=_("Alteration"), blank=True, related_name="finds" ) alteration_causes = models.ManyToManyField( AlterationCauseType, verbose_name=_("Alteration cause"), blank=True, related_name="finds", ) treatment_emergency = models.ForeignKey( TreatmentEmergencyType, verbose_name=_("Treatment emergency"), on_delete=models.SET_NULL, blank=True, null=True, ) insurance_value = models.FloatField(_("Insurance value"), blank=True, null=True) appraisal_date = models.DateField(_("Appraisal date"), blank=True, null=True) public_description = models.TextField( _("Public description"), blank=True, default="" ) documents = models.ManyToManyField( Document, related_name="finds", verbose_name=_("Documents"), blank=True ) main_image = models.ForeignKey( Document, related_name="main_image_finds", on_delete=models.SET_NULL, verbose_name=_("Main image"), blank=True, null=True, ) treatments = models.ManyToManyField( "Treatment", verbose_name=_("Treatments"), related_name="finds", blank=True, help_text=_("Related treatments when no new find is created"), through="FindTreatment" ) cached_periods = models.TextField( _("Cached periods label"), blank=True, default="", help_text=_("Generated automatically - do not edit"), ) cached_object_types = models.TextField( _("Cached object types label"), blank=True, default="", help_text=_("Generated automatically - do not edit"), ) cached_materials = models.TextField( _("Cached material types label"), blank=True, default="", help_text=_("Generated automatically - do not edit"), ) cache_complete_museum_id = models.TextField( _("Complete museum ID"), blank=True, default="", db_index=True, help_text=_("Cached value - do not edit"), ) history = HistoricalRecords(bases=[HistoryModel]) BASKET_MODEL = FindBasket class Meta: verbose_name = _("Find") verbose_name_plural = _("Finds") permissions = ( ("view_own_find", "Can view own Find"), ("change_own_find", "Can change own Find"), ("delete_own_find", "Can delete own Find"), ) ordering = ("cached_label",) indexes = [ GinIndex(fields=["data"]), ] ADMIN_SECTION = _("Finds") def natural_key(self): return (self.uuid,) def geodata(self): return GeoVectorData.objects.filter( related_items_archaeological_finds_basefind__find__pk=self.pk ) @property def short_class_name(self): return _("FIND") def __str__(self): return self.cached_label or "" @property def short_label(self): return self.reference @property def dating(self): return " ; ".join([str(dating) for dating in self.datings.all()]) @property def excavation_ids(self): return " - ".join( [ base_find["excavation_id"] for base_find in self.base_finds.values("excavation_id") .order_by("pk") .all() if base_find["excavation_id"] ] ) @property def has_museum_section(self): if get_current_profile().museum and self.mark: return True for field in self._meta.get_fields(): if not field.name.startswith("museum_"): continue instanced_field = getattr(self, field.name) if instanced_field and (not field.many_to_many or instanced_field.count()): return True return False @property def has_ownership_section(self): return self.owner or self.ownership_status @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 ( self.museum_entry_date.year == self.museum_entry_date_end.year and self.museum_entry_date.month == 1 and self.museum_entry_date_end.month == 12 and 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)] if self.museum_entry_date_end: dates.append(date_format(self.museum_entry_date_end, format='SHORT_DATE_FORMAT', use_l10n=True)) return " / ".join(dates) @classmethod def hierarchic_fields(cls): return ["container", "container_ref"] @property def materials(self): return " ; ".join([str(material) for material in self.material_types.all()]) def get_first_material_type(self): model = self.__class__.material_types.through q = model.objects.filter(find=self) if not q.count(): return if q.filter(materialtype__parent__isnull=True).count(): q = q.filter(materialtype__parent__isnull=True) return q.order_by("materialtype__label")[0].materialtype @property def show_url(self): return reverse("show-find", args=[self.pk, ""]) @property def has_packaging_for_current_container(self): return FindTreatment.objects.filter(find=self, location_type__in=["B", "C"]) @staticmethod def _get_upstream_count(upstream_id, idx): if not upstream_id: return idx q = Find.objects.filter(downstream_treatment_id=upstream_id) if not q.exists(): # upstream find has been deleted? return idx idx += 1 upstream_ids = q.values_list("upstream_treatment_id", flat=True) return max([Find._get_upstream_count(up_id, idx) for up_id in upstream_ids]) @property def upstream_count(self): """ Count upstream treatments. Get the maximum count. If equal to 0 return None. Used mainly to generate IDs """ if not self.id: return if not hasattr(self, "_cache_upstream_count"): self._cache_upstream_count = Find._get_upstream_count( self.upstream_treatment_id, 0 ) or None return self._cache_upstream_count @property def has_packaging_for_reference_container(self): return FindTreatment.objects.filter(find=self, location_type__in=["B", "R"]) def public_representation(self): dct = super(Find, self).public_representation() dct.update( { "denomination": self.denomination, "free-id": self.label, "description": self.description, "public-description": self.public_description, "materials": [str(mt) for mt in self.material_types.all()], "material-comment": self.material_comment, "object-types": [str(ot) for ot in self.object_types.all()], "find-number": self.find_number, "decoration": self.decoration, "inscription": self.inscription, "manufacturing-place": self.manufacturing_place, "comment": self.comment, "length": self.length, "width": self.width, "height": self.height, "thickness": self.thickness, "diameter": self.diameter, "circumference": self.circumference, "volume": self.volume, "weight": self.weight, "datings": [str(dating) for dating in self.datings.all()], "base-finds": [ bf.public_representation() for bf in self.base_finds.all() ], } ) # images return dct def regenerate_external_id(self): for bf in self.base_finds.all(): if self.base_finds.count() == 1: bf.label = self.label bf.regenerate_external_id() super(Find, self).regenerate_external_id() @property def name(self): return " - ".join(base_find.name for base_find in self.base_finds.all()) @property def full_label(self): lbl = " - ".join( getattr(self, attr) for attr in ("label", "denomination", "administrative_index") if getattr(self, attr) ) base = " - ".join( base_find.complete_id() for base_find in self.base_finds.all() ) if base: lbl += " ({})".format(base) return lbl @property def complete_id(self): """ Return complete id of associated base find """ return " ; ".join( list(self.base_finds.values_list("cache_complete_id", flat=True)) ) @property def context_record_label(self): """ Return label of associated context records """ return " ; ".join( list(self.base_finds.values_list("context_record__label", flat=True)) ) def get_first_base_find(self): if not self.base_finds.count(): return return self.base_finds.order_by("-pk").all()[0] def base_finds_list(self) -> list: lst = [] for bf in self.base_finds.all(): lst.append(bf.full_serialize()) return lst def m2m_listing_datings(self) -> list: return [dating.full_serialize() for dating in self.datings.all()] DOC_VALUES = [ ("base_finds", _("List of associated base finds")), ("material_types_label", _("Material types string")), ("material_types_code", _("Material types code string")), ("material_types_recommendations", _("Recommendations string from material")), ("complete_id", _("Complete ID of associated base finds")), ("context_record_label", _("Label of associated context records")), ] def get_material_types_code(self) -> str: """ Return pipe separated material type code """ materials = set() for material in self.material_types.exclude(code__isnull=True).values_list( "code", flat=True ): materials.add(material) return "|".join(sorted(materials)) def get_material_types(self) -> str: """ Return comma separated string of material types """ materials = set() for material in self.material_types.exclude(label__isnull=True).values_list( "label", flat=True ): materials.add(material) return ", ".join(sorted(materials)) def get_hierarchical_material_types(self) -> str: """ Return comma separated string of material types with full hierarchy """ labels = [] for material in self.material_types.all(): mat = [material.label] while material.parent: mat.append(material.parent.label) material = material.parent labels.append(" > ".join([m for m in reversed(mat)])) return " ; ".join(sorted(labels)) def get_material_types_recommendations(self) -> str: """ Return comma separated string of recommendations from material """ recommendations = set() for level in range(5): excludes = [ ("parent__" * level + "recommendation__isnull", True), ("parent__" * level + "recommendation", ""), ] q = self.material_types if level: q = q.exclude(**{("parent__" * level)[:-2] + "_id": None}) for k, v in excludes: q = q.exclude(**{k: v}) for recommendation in q.values_list( "parent__" * level + "recommendation", flat=True ): recommendations.add(recommendation) return ", ".join(sorted(recommendations)) def get_extra_values(self, prefix="", no_values=False, filtr=None, **kwargs): values = {} no_base_finds = False if "no_base_finds" in kwargs: no_base_finds = kwargs["no_base_finds"] if not filtr or prefix + "material_types_label" in filtr: values[prefix + "material_types_label"] = self.get_material_types() if not filtr or prefix + "material_types_code" in filtr: values[prefix + "material_types_code"] = self.get_material_types_code() if not filtr or prefix + "material_types_recommendations" in filtr: values[ prefix + "material_types_recommendations" ] = self.get_material_types_recommendations() if no_base_finds: return values # by default attach first basefind data bf = self.get_first_base_find() if not bf: return values if not filtr: filtr = [] exclude = kwargs.get("exclude", []) exclude += [ e for e in ("finds", "base_find_finds", "find", "base_find_find") if e not in exclude ] kwargs["exclude"] = exclude kwargs["no_find"] = True alt_filtr = [k[len("base_find_"):] for k in filtr if k.startswith("base_find_")] alt_filtr += filtr v = bf.get_values(prefix=prefix, no_values=True, filtr=alt_filtr, **kwargs) new_values = {} # use simple notation for base finds to simplify templates for k in v: if not hasattr(self, k): # do not overload existings fields new_values[k] = v[k] # "base_find" prefix notation new_values.update(dict((('base_find_' + k, v) for k, v in v.items()))) new_values.update(values) values = new_values # list all base finds if necessary values[prefix + "base_finds"] = [ base_find.get_values(no_values=True, filtr=filtr, **kwargs) for base_find in self.base_finds.distinct().order_by("-pk").all() ] return values def get_values_for_datings(self, prefix=""): return [dating.get_values(prefix=prefix) for dating in self.datings.all()] @property def reference(self): bf = self.get_first_base_find() if not bf: return "00" return bf.short_id() def get_extra_actions(self, request): """ For sheet template: return "Add to basket" action """ # url, base_text, icon, extra_text, extra css class, is a quick action # 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) is_locked = hasattr(self, "is_locked") and self.is_locked(request.user) profile = get_current_profile() can_add_geo = profile.mapping and self.can_do(request, "ishtar_common.add_geovectordata") if can_add_geo: if self.base_finds.count() == 1: actions.append(self.base_finds.all()[0].get_add_geo_action()) can_edit_find = self.can_do(request, "archaeological_finds.change_find") if can_edit_find and not is_locked: actions += [ ( reverse("find-qa-duplicate", args=[self.pk]), _("Duplicate"), "fa fa-clone", "", "", True, ), ( reverse("find-qa-basket", args=[self.pk]), _("Add to basket"), "fa fa-shopping-basket", "", "", True, ), ( reverse("find-add-treatment", args=[self.pk]), _("Simple treatment"), "fa fa-flask", "", "", False, ), ( reverse("find-add-divide-treatment", args=[self.pk]), _("Divide treatment"), "fa fa-scissors", "", "", False, ), ] if get_current_profile().warehouse: actions.append( ( reverse("find-qa-packaging", args=[self.pk]), _("Packaging"), "fa fa-gift", "", "", True, ) ) return actions def _get_base_image_path(self): bf = None if self.id: bf = self.get_first_base_find() if not bf: return "detached/{}".format(self.SLUG) ope = bf.context_record.operation find_idx = "{:0" + str(settings.ISHTAR_FINDS_INDEX_ZERO_LEN) + "d}" return ("{}/{}/" + find_idx).format( ope._get_base_image_path(), self.SLUG, self.index ) @property def administrative_index(self): profile = get_current_profile() if profile.has_overload("find_administrative_index"): return ALTERNATE_CONFIGS[profile.config].find_administrative_index(self) bf = self.get_first_base_find() if not bf or not bf.context_record or not bf.context_record.operation: return "" return "{}-{}".format(bf.context_record.operation.get_reference(), self.index) @property def integrities_count(self): return self.integrities.count() @property def conservatory_states_count(self): return self.conservatory_states.count() @property def remarkabilities_count(self): return self.remarkabilities.count() @property def cultural_attributions_count(self): return self.cultural_attributions.count() @property def documents_count(self): return self.documents.count() @property def operation(self): bf = self.get_first_base_find() if not bf or not bf.context_record or not bf.context_record.operation: return return bf.context_record.operation def context_records_lbl(self): return " - ".join( [bf.context_record.cached_label for bf in self.base_finds.all()] ) context_records_lbl.short_description = _("Context record") context_records_lbl.admin_order_field = "base_finds__context_record__cached_label" def operations_lbl(self): return " - ".join( [bf.context_record.operation.cached_label for bf in self.base_finds.all()] ) operations_lbl.short_description = _("Operation") operations_lbl.admin_order_field = ( "base_finds__context_record__operation__cached_label" ) def _get_treatments(self, model, rel="upstream", limit=None, count=False): treatments, findtreats = [], [] q = model.objects.filter(find_id=self.pk).order_by( "-treatment__year", "-treatment__index", "-treatment__start_date", "-treatment__end_date", ) if count: return q.count() for findtreat in q.distinct().all(): if findtreat.pk in findtreats: continue findtreats.append(findtreat.pk) q = getattr(findtreat.treatment, rel).distinct().order_by("label") if limit: q = q[:limit] treatments.append((q.all(), findtreat.treatment)) return treatments def upstream_treatments(self, limit=None): from archaeological_finds.models_treatments import FindUpstreamTreatments return self._get_treatments(FindUpstreamTreatments, "upstream", limit=limit) def limited_upstream_treatments(self): return self.upstream_treatments(15) def downstream_treatments(self, limit=None): from archaeological_finds.models_treatments import FindDownstreamTreatments return self._get_treatments(FindDownstreamTreatments, "downstream", limit=limit) def limited_downstream_treatments(self): return self.downstream_treatments(15) def all_treatments(self): return self.upstream_treatments() + self.downstream_treatments() def non_modif_treatments(self, limit=None): from archaeological_finds.models_treatments import FindNonModifTreatments return self._get_treatments(FindNonModifTreatments, "finds", limit=limit) def non_modif_treatments_count(self): from archaeological_finds.models_treatments import FindNonModifTreatments return self._get_treatments(FindNonModifTreatments, "finds", count=True) def limited_non_modif_treatments(self): return self.non_modif_treatments(15) def associated_treatment_files(self): TreatmentFile = apps.get_model("archaeological_finds", "TreatmentFile") return TreatmentFile.objects.filter( associated_basket_id__in=FindBasket.objects.filter( items__pk=self.pk ).values_list("pk", flat=True) ).order_by("reception_date", "creation_date", "end_date") def associated_treatment_files_count(self): return self.associated_treatment_files().count() @property def weight_string(self): if not self.weight: return "" return "{} {}".format(self.weight, self.weight_unit or "") def get_department(self): bf = self.get_first_base_find() if not bf: return "00" return bf.context_record.operation.get_department() def get_town_label(self): bf = self.get_first_base_find() if not bf: return "00" return bf.context_record.operation.get_town_label() @classmethod def get_periods(cls, slice="year", fltr=None): if not fltr: fltr = {} q = cls.objects if fltr: q = q.filter(**fltr) if slice == "year": years = set() finds = q.filter(downstream_treatment__isnull=True) for find in finds: bi = find.base_finds.all() if not bi: continue bi = bi[0] if bi.context_record.operation.start_date: yr = bi.context_record.operation.start_date.year years.add(yr) return list(years) @classmethod def get_by_year(cls, year, fltr=None): if not fltr: fltr = {} q = cls.objects if fltr: q = q.filter(**fltr) return q.filter( downstream_treatment__isnull=True, base_finds__context_record__operation__start_date__year=year, ) @classmethod def get_operations(cls): operations = set() finds = cls.objects.filter(downstream_treatment__isnull=True) for find in finds: bi = find.base_finds.all() if not bi: continue bi = bi[0] pk = bi.context_record.operation.pk operations.add(pk) return list(operations) @classmethod def get_by_operation(cls, operation_id): return cls.objects.filter( downstream_treatment__isnull=True, base_finds__context_record__operation__pk=operation_id, ) @classmethod def get_total_number(cls, fltr=None): q = cls.objects if fltr: q = q.filter(**fltr) return q.filter(downstream_treatment__isnull=True).count() def duplicate( self, user, copy_datings=True, duplicate_for_treatment=True, data=None ): model = self.__class__ new = model.objects.get(pk=self.pk) for field in model._meta.fields: # pk is in PRIVATE_FIELDS so: new.pk = None and a new # item will be created on save if field.name in PRIVATE_FIELDS: setattr(new, field.name, None) new.order = self.order if duplicate_for_treatment: new.order += 1 new.history_user = user if data: for k in data: setattr(new, k, data[k]) # remove associated treatments if not duplicate_for_treatment and ( new.upstream_treatment or new.downstream_treatment ): new.upstream_treatment, new.downstream_treatment = None, None new.uuid = uuid.uuid4() new.save() if hasattr(user, "user_ptr"): new.history_creator = user.user_ptr new.history_modifier = user.user_ptr new.save() # m2m fields m2m = [ field.name for field in model._meta.many_to_many if field.name not in PRIVATE_FIELDS ] for field in m2m: if field == "datings" and copy_datings: for dating in self.datings.all(): is_present = False for current_dating in new.datings.all(): if Dating.is_identical(current_dating, dating): is_present = True break if is_present: continue dating.pk = None dating.save() new.datings.add(dating) else: for val in getattr(self, field).all(): if val not in getattr(new, field).all(): getattr(new, field).add(val) if not duplicate_for_treatment: bf = self.get_first_base_find() new.base_finds.clear() if bf: new.base_finds.add( bf.duplicate( user=user, data={"label": new.label, "external_id": ""} ) ) # remove documents for this kind of duplicate (data entry) new.documents.clear() # remove associated treatments new.treatments.clear() return new @classmethod def get_limit_to_area_query(cls, town_ids): return Q(base_finds__context_record__operation__towns__pk__in=town_ids) @classmethod def _get_query_owns(cls, ishtaruser, prefix=""): q = ( cls._construct_query_own( cls, f"{prefix}container__location__", Warehouse._get_query_owns_dicts(ishtaruser) ) | cls._construct_query_own( cls, f"{prefix}container__responsible__", Warehouse._get_query_owns_dicts(ishtaruser) ) | cls._construct_query_own( cls, f"{prefix}base_finds__context_record__operation__", Operation._get_query_owns_dicts(ishtaruser), ) | cls._construct_query_own( cls, f"{prefix}basket__", [{"shared_with": ishtaruser, "shared_write_with": ishtaruser}], ) | cls._construct_query_own( cls, "", [ {f"{prefix}history_creator": ishtaruser.user_ptr}, {f"{prefix}base_finds__context_record__operation__end_date__isnull": True}, ], ) ) return q @classmethod def get_query_owns(cls, ishtaruser): return cls._get_query_owns(ishtaruser) @classmethod def get_owns( cls, user, menu_filtr=None, limit=None, values=None, get_short_menu_class=None, no_auth_check=False, query=False ): replace_query = None if menu_filtr and "contextrecord" in menu_filtr: replace_query = Q(base_finds__context_record=menu_filtr["contextrecord"]) owns = super(Find, cls).get_owns( user, replace_query=replace_query, limit=limit, values=values, get_short_menu_class=get_short_menu_class, no_auth_check=no_auth_check, query=query ) if query: return owns return cls._return_get_owns(owns, values, get_short_menu_class) def _generate_cached_label(self): label = self._profile_generate_cached_label() if label: return label if not self.base_finds.count(): return "-" return self.base_finds.all()[0].cached_label def complete_museum_id(self): return self.cache_complete_museum_id def _generate_cache_complete_museum_id(self): return get_generated_id("museum_complete_identifier", self) or "" def _generate_cached_periods(self): return " & ".join([dating.period.label for dating in self.datings.all()]) def _generate_cached_object_types(self): return " & ".join([str(obj) for obj in self.object_types.all()]) def _generate_cached_materials(self): return " & ".join([str(mat) for mat in self.material_types.all()]) def get_localisation(self, place, is_ref=False): """ Get localisation reference in the warehouse :param place: number of the localisation starting with 0 :param is_ref: if true - reference container else current container :return: reference - empty string if not available """ if is_ref: container = self.container_ref else: container = self.container if not container: return "" ## first localisation is the warehouse locas = list(container.get_localisations())[1:] if len(locas) < (place + 1): return "" return locas[place] @property def reference_localisation_1(self): return self.get_localisation(0, is_ref=True) @property def reference_localisation_2(self): return self.get_localisation(1, is_ref=True) @property def reference_localisation_3(self): return self.get_localisation(2, is_ref=True) @property def reference_localisation_4(self): return self.get_localisation(3, is_ref=True) @property def reference_localisation_5(self): return self.get_localisation(4, is_ref=True) @property def reference_localisation_6(self): return self.get_localisation(5, is_ref=True) @property def reference_localisation_7(self): return self.get_localisation(6, is_ref=True) @property def reference_localisation_8(self): return self.get_localisation(7, is_ref=True) @property def reference_localisation_9(self): return self.get_localisation(8, is_ref=True) @property def localisation_1(self): return self.get_localisation(0) @property def localisation_2(self): return self.get_localisation(1) @property def localisation_3(self): return self.get_localisation(2) @property def localisation_4(self): return self.get_localisation(3) @property def localisation_5(self): return self.get_localisation(4) @property def localisation_6(self): return self.get_localisation(5) @property def localisation_7(self): return self.get_localisation(6) @property def localisation_8(self): return self.get_localisation(7) @property def localisation_9(self): return self.get_localisation(8) def set_localisation(self, place, context, value, is_ref=False, static=False): """ Get localisation reference in the warehouse :param place: number of the localisation starting with 0 :param context: context of the request - not used :param value: localisation value :param is_ref: if true - reference container else current container :param static: if true: do not create new container :return: None """ if is_ref: container = self.container_ref else: container = self.container if not container: if not value: return if is_ref: raise ImporterError( _( "No reference container have been set - the " "localisation cannot be set." ) ) else: raise ImporterError( _("No container have been set - the localisation cannot " "be set.") ) container.no_post_process(history=True) localisation, error = container.set_localisation( place, value, static=static, return_errors=True ) if error: raise ImporterError(error) @post_importer_action def set_reference_localisation_1(self, context, value): return self.set_localisation(0, context, value, is_ref=True) set_reference_localisation_1.post_save = True @post_importer_action def set_reference_localisation_2(self, context, value): return self.set_localisation(1, context, value, is_ref=True) set_reference_localisation_2.post_save = True @post_importer_action def set_reference_localisation_3(self, context, value): return self.set_localisation(2, context, value, is_ref=True) set_reference_localisation_3.post_save = True @post_importer_action def set_reference_localisation_4(self, context, value): return self.set_localisation(3, context, value, is_ref=True) set_reference_localisation_4.post_save = True @post_importer_action def set_reference_localisation_5(self, context, value): return self.set_localisation(4, context, value, is_ref=True) set_reference_localisation_5.post_save = True @post_importer_action def set_reference_localisation_6(self, context, value): return self.set_localisation(5, context, value, is_ref=True) set_reference_localisation_6.post_save = True @post_importer_action def set_reference_localisation_7(self, context, value): return self.set_localisation(6, context, value, is_ref=True) set_reference_localisation_7.post_save = True @post_importer_action def set_reference_localisation_8(self, context, value): return self.set_localisation(7, context, value, is_ref=True) set_reference_localisation_8.post_save = True @post_importer_action def set_reference_localisation_9(self, context, value): return self.set_localisation(8, context, value, is_ref=True) set_reference_localisation_9.post_save = True @post_importer_action def set_reference_static_localisation_1(self, context, value): return self.set_localisation(0, context, value, is_ref=True, static=True) set_reference_static_localisation_1.post_save = True @post_importer_action def set_reference_static_localisation_2(self, context, value): return self.set_localisation(1, context, value, is_ref=True, static=True) set_reference_static_localisation_2.post_save = True @post_importer_action def set_reference_static_localisation_3(self, context, value): return self.set_localisation(2, context, value, is_ref=True, static=True) set_reference_static_localisation_3.post_save = True @post_importer_action def set_reference_static_localisation_4(self, context, value): return self.set_localisation(3, context, value, is_ref=True, static=True) set_reference_static_localisation_4.post_save = True @post_importer_action def set_reference_static_localisation_5(self, context, value): return self.set_localisation(4, context, value, is_ref=True, static=True) set_reference_static_localisation_5.post_save = True @post_importer_action def set_reference_static_localisation_6(self, context, value): return self.set_localisation(5, context, value, is_ref=True, static=True) set_reference_static_localisation_6.post_save = True @post_importer_action def set_reference_static_localisation_7(self, context, value): return self.set_localisation(6, context, value, is_ref=True, static=True) set_reference_static_localisation_7.post_save = True @post_importer_action def set_reference_static_localisation_8(self, context, value): return self.set_localisation(7, context, value, is_ref=True, static=True) set_reference_static_localisation_8.post_save = True @post_importer_action def set_reference_static_localisation_9(self, context, value): return self.set_localisation(8, context, value, is_ref=True, static=True) set_reference_static_localisation_9.post_save = True @post_importer_action def set_localisation_1(self, context, value): return self.set_localisation(0, context, value) set_localisation_1.post_save = True @post_importer_action def set_localisation_2(self, context, value): return self.set_localisation(1, context, value) set_localisation_2.post_save = True @post_importer_action def set_localisation_3(self, context, value): return self.set_localisation(2, context, value) set_localisation_3.post_save = True @post_importer_action def set_localisation_4(self, context, value): return self.set_localisation(3, context, value) set_localisation_4.post_save = True @post_importer_action def set_localisation_5(self, context, value): return self.set_localisation(4, context, value) set_localisation_5.post_save = True @post_importer_action def set_localisation_6(self, context, value): return self.set_localisation(5, context, value) set_localisation_6.post_save = True @post_importer_action def set_localisation_7(self, context, value): return self.set_localisation(6, context, value) set_localisation_7.post_save = True @post_importer_action def set_localisation_8(self, context, value): return self.set_localisation(7, context, value) set_localisation_8.post_save = True @post_importer_action def set_localisation_9(self, context, value): return self.set_localisation(8, context, value) set_localisation_9.post_save = True @post_importer_action def set_static_localisation_1(self, context, value): return self.set_localisation(0, context, value, static=True) set_static_localisation_1.post_save = True @post_importer_action def set_static_localisation_2(self, context, value): return self.set_localisation(1, context, value, static=True) set_static_localisation_2.post_save = True @post_importer_action def set_static_localisation_3(self, context, value): return self.set_localisation(2, context, value, static=True) set_static_localisation_3.post_save = True @post_importer_action def set_static_localisation_4(self, context, value): return self.set_localisation(3, context, value, static=True) set_static_localisation_4.post_save = True @post_importer_action def set_static_localisation_5(self, context, value): return self.set_localisation(4, context, value, static=True) set_static_localisation_5.post_save = True @post_importer_action def set_static_localisation_6(self, context, value): return self.set_localisation(5, context, value, static=True) set_static_localisation_6.post_save = True @post_importer_action def set_static_localisation_7(self, context, value): return self.set_localisation(6, context, value, static=True) set_static_localisation_7.post_save = True @post_importer_action def set_static_localisation_8(self, context, value): return self.set_localisation(7, context, value, static=True) set_static_localisation_8.post_save = True @post_importer_action def set_static_localisation_9(self, context, value): return self.set_localisation(8, context, value, static=True) set_static_localisation_9.post_save = True def update_current_full_location(self, full_location=None): """ If relevant update full location of current container :param full_location: provided if update is triggered from container """ if getattr(self, "_container_fisrt_full_location", False) \ or self.has_packaging_for_current_container: return False self._container_fisrt_full_location = True if self.container: if not full_location: full_location = self.container.generate_full_location() if full_location == self.container_fisrt_full_location: return False self.container_fisrt_full_location = full_location else: if self.container_fisrt_full_location == "": return False self.container_fisrt_full_location = "" return True def update_ref_full_location(self, full_location=None): """ If relevant update full location of reference container :param full_location: provided if update is triggered from container """ if getattr(self, "_container_ref_fisrt_full_location", False) \ or self.has_packaging_for_reference_container: return False self._container_ref_fisrt_full_location = True if self.container_ref: if not full_location: full_location = self.container_ref.generate_full_location() if full_location == self.container_ref_fisrt_full_location: return False self.container_ref_fisrt_full_location = full_location else: if self.container_ref_fisrt_full_location == "": return False self.container_ref_fisrt_full_location = "" return True def update_full_location(self): updated = self.update_current_full_location() updated |= self.update_ref_full_location() return updated def generate_index(self): """ Generate index based on operation or context record (based on the configuration) :return: True if index has been changed. """ bfs = self.base_finds profile = get_current_profile() if profile.find_index == "O": bfs = bfs.filter(context_record__operation__pk__isnull=False).order_by( "-context_record__operation__start_date" ) if not bfs.count(): return False operation = bfs.all()[0].context_record.operation q = Find.objects.filter(base_finds__context_record__operation=operation) elif profile.find_index == "CR": bfs = bfs.filter(context_record__pk__isnull=False).order_by( "context_record__pk" ) if not bfs.count(): return False cr = bfs.all()[0].context_record q = Find.objects.filter(base_finds__context_record=cr) else: return False if self.pk: q = q.exclude(pk=self.pk) if q.count(): self.index = q.aggregate(Max("index"))["index__max"] + 1 else: self.index = 1 return True def save(self, *args, **kwargs): old_container = None # fetch in db if self.pk: old_container = self.__class__.objects.filter(pk=self.pk).values_list( "container_id", flat=True )[0] super().save(*args, **kwargs) self.skip_history_when_saving = True if self.container_ref and not self.container: self.container = self.container_ref if self.container and self.container._calculate_weight(): self.container.save() if (self.container and self.container.pk != old_container) or ( not self.container and old_container): # force recalculation of weight when a find is removed Container = apps.get_model("archaeological_warehouse.Container") try: old_container = Container.objects.get(pk=old_container) if old_container._calculate_weight(): old_container.save() except Container.DoesNotExist: pass if self.update_full_location(): self.save() return True updated = self.update_external_id(save=False) if updated: self._cached_label_checked = False self.save() return q = self.base_finds if not self.index and q.count(): changed = self.generate_index() if changed: self._cached_label_checked = False self.save() for base_find in self.base_finds.filter( context_record__operation__pk__isnull=False ).all(): modified = False if self.label and not base_find.label: base_find.label = self.label modified = True if not base_find.index: modified = base_find.generate_index() short_id = base_find.short_id() if base_find.cache_short_id != short_id: base_find.cache_short_id = short_id modified = True complete_id = base_find.complete_id() if base_find.cache_complete_id != complete_id: base_find.cache_complete_id = complete_id modified = True if base_find.update_external_id(): modified = True if modified: base_find.no_post_process() base_find._cached_label_checked = False base_find.save() # if not base_find.material_index: # idx = BaseFind.objects\ # .filter(context_record=base_find.context_record, # find__material_types=self.material_type)\ # .aggregate(Max('material_index')) # base_find.material_index = \ # idx and idx['material_index__max'] + 1 or 1 def fix(self): """ Fix redundant m2m dating association (usually after imports) """ Dating.fix_dating_association(self) def pre_clean_find(sender, **kwargs): if not kwargs.get("instance"): return instance = kwargs.get("instance") if not getattr(instance, "__base_find_deleted", False): # prevent loop for bf in instance.base_finds.all(): # no other find is associated if not bf.find.exclude(pk=instance.pk).count(): bf.delete() try: if instance.downstream_treatment: # TODO: not managed for now. Raise an error? return except ObjectDoesNotExist: pass try: if not instance.upstream_treatment: return except ObjectDoesNotExist: return instance.upstream_treatment.upstream.clear() instance.upstream_treatment.downstream.clear() instance.upstream_treatment.delete() post_save.connect(cached_label_changed, sender=Find) pre_delete.connect(pre_clean_find, sender=Find) def base_find_find_changed(sender, **kwargs): obj = kwargs.get("instance", None) if not obj: return obj.skip_history_when_saving = True # recalculate cached_label, complete id and external id obj.save() m2m_changed.connect(base_find_find_changed, sender=Find.base_finds.through) m2m_changed.connect(document_attached_changed, sender=Find.documents.through) class FindInsideContainer(models.Model): CREATE_SQL = """ CREATE VIEW find_inside_container AS SELECT fb.id AS find_id, fb.container_id AS container_id FROM archaeological_finds_find fb WHERE fb.downstream_treatment_id IS NULL AND fb.container_id IS NOT NULL UNION SELECT f.id AS find_id, r.container_parent_id AS container_id FROM archaeological_finds_find f INNER JOIN container_tree r ON r.container_id = f.container_id WHERE f.downstream_treatment_id IS NULL; -- deactivate deletion CREATE RULE find_inside_container_del AS ON DELETE TO find_inside_container DO INSTEAD DELETE FROM archaeological_finds_find where id=NULL; """ DELETE_SQL = """ DROP VIEW IF EXISTS find_inside_container; """ TABLE_COLS = ["find__" + t for t in Find.TABLE_COLS] COL_LABELS = {"find__" + k: Find.COL_LABELS[k] for k in Find.COL_LABELS.keys()} EXTRA_REQUEST_KEYS = { "find__" + k: "find__" + Find.EXTRA_REQUEST_KEYS[k] for k in Find.EXTRA_REQUEST_KEYS.keys() } SLUG = "find_inside_container" find = models.OneToOneField( Find, verbose_name=_("Find"), related_name="inside_container", primary_key=True, on_delete=models.DO_NOTHING, ) container = models.ForeignKey( "archaeological_warehouse.Container", verbose_name=_("Container"), related_name="container_content", on_delete=models.DO_NOTHING, ) class Meta: managed = False db_table = "find_inside_container" @classmethod def get_query_owns(cls, ishtaruser): return Find._get_query_owns(ishtaruser, prefix="find__") for attr in Find.HISTORICAL_M2M: m2m_changed.connect(m2m_historization_changed, sender=getattr(Find, attr).through) LOCATION_TYPE = [ ["C", _("Current")], ["R", _("Reference")], ["B", _("Reference/current")], ] LOCATION_TYPE_DICT = dict(LOCATION_TYPE) class FindTreatment(Imported): """ Record all new location for a find. """ find = models.ForeignKey( Find, verbose_name=_("Find"), on_delete=models.CASCADE, ) treatment = models.ForeignKey( "archaeological_finds.Treatment", blank=True, null=True, on_delete=models.CASCADE ) full_location = models.TextField(_("Full location"), default="", blank=True) location_type = models.CharField(_("Location type"), max_length=1, choices=LOCATION_TYPE, default="C") class Meta: verbose_name = _("Find - Treatment") verbose_name_plural = _("Find - Treatments") db_table = 'archaeological_finds_find_treatments' def __str__(self): return f"{self.treatment} / {self.find}" def location_type_label(self): if self.location_type in LOCATION_TYPE_DICT: return LOCATION_TYPE_DICT[self.location_type] def generate_full_location(self): if getattr(self, "_full_location_set", False) or self.full_location or ( not self.treatment.is_current_container_changer and not self.treatment.is_reference_container_changer): return False if self.treatment.is_current_container_changer: if self.treatment.is_reference_container_changer: self.location_type = "B" else: self.location_type = "C" elif self.treatment.is_reference_container_changer: self.location_type = "R" if self.treatment.container: self.full_location = self.treatment.container.generate_full_location() elif self.treatment.is_loan and self.find.container_ref: self.full_location = self.find.container_ref.generate_full_location() self._full_location_set = True self.save() return True class Property(LightHistorizedItem): find = models.ForeignKey(Find, verbose_name=_("Find"), on_delete=models.CASCADE) administrative_act = models.ForeignKey( AdministrativeAct, verbose_name=_("Administrative act"), on_delete=models.CASCADE, ) person = models.ForeignKey( Person, verbose_name=_("Person"), related_name="properties", on_delete=models.CASCADE, ) start_date = models.DateField(_("Start date")) end_date = models.DateField(_("End date")) class Meta: verbose_name = _("Property") verbose_name_plural = _("Properties") indexes = [ GinIndex(fields=["data"]), ] ADMIN_SECTION = _("Finds") def __str__(self): return str(self.person) + settings.JOINT + str(self.find)