From 28ae853d43338ca9816cdbe1b2887ff48f94d1ca Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Wed, 25 Mar 2026 19:11:01 +0100 Subject: ♻️ move BaseDating to ishtar_common MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- archaeological_context_records/models.py | 246 +------------------------------ archaeological_finds/models_finds.py | 3 +- ishtar_common/models.py | 245 ++++++++++++++++++++++++++++++ 3 files changed, 249 insertions(+), 245 deletions(-) diff --git a/archaeological_context_records/models.py b/archaeological_context_records/models.py index d104b4351..800a6604b 100644 --- a/archaeological_context_records/models.py +++ b/archaeological_context_records/models.py @@ -44,6 +44,7 @@ from ishtar_common.utils import ( ) from ishtar_common.models import ( + BaseDating, Document, Person, GeneralType, @@ -71,7 +72,7 @@ from ishtar_common.models import ( RelationsViews, ) from ishtar_common.models_common import GeoVectorData, HistoricalRecords,\ - SerializeItem, geodata_attached_changed + geodata_attached_changed from archaeological_operations.models import ( add_oa_prefix, ArchaeologicalSite, @@ -105,249 +106,6 @@ post_save.connect(post_save_cache, sender=DatingQuality) post_delete.connect(post_save_cache, sender=DatingQuality) -class BaseDating(models.Model, SerializeItem): - SLUG = "dating" - SERIALIZE_EXCLUDE = ["find", "context_record"] - CURRENT_MODEL = None - CURRENT_MODEL_ATTR = None - uuid = models.UUIDField(default=uuid.uuid4) - reference = models.TextField(_("Reference"), blank=True, default="") - external_id = models.TextField(_("External ID"), blank=True, default="") - period = models.ForeignKey( - Period, verbose_name=_("Chronological period"), on_delete=models.PROTECT, - blank=True, - null=True, - ) - start_date = models.IntegerField(_("Start date"), blank=True, null=True) - end_date = models.IntegerField(_("End date"), blank=True, null=True) - dating_type = models.ForeignKey( - DatingType, - verbose_name=_("Dating type"), - on_delete=models.SET_NULL, - blank=True, - null=True, - ) - quality = models.ForeignKey( - DatingQuality, - verbose_name=_("Quality"), - on_delete=models.SET_NULL, - blank=True, - null=True, - ) - precise_dating = models.TextField(_("Precise on this dating"), blank=True, default="") - objects = UUIDModelManager() - ASSOCIATED_ALT_NAMES = { - "datings__period": SearchAltName( - pgettext_lazy("key for text search", "datings-period"), - "datings__period__label__iexact", - ), - "datings__precise_dating": SearchAltName( - pgettext_lazy("key for text search", "datings-precise"), - "datings__precise_dating__iexact", - ), - "datings__start_date": SearchAltName( - pgettext_lazy("key for text search", "datings-start"), - "datings__start_date", - ), - "datings__end_date": SearchAltName( - pgettext_lazy("key for text search", "datings-end"), - "datings__end_date", - ), - "datings__dating_type": SearchAltName( - pgettext_lazy("key for text search", "datings-type"), - "datings__dating_type__label__iexact", - ), - "datings__quality": SearchAltName( - pgettext_lazy("key for text search", "datings-quality"), - "datings__quality__label__iexact", - ), - } - - class Meta: - abstract = True - - def __str__(self): - if self.precise_dating and self.precise_dating.strip(): - return self.precise_dating.strip() - start_date = self.start_date and str(self.start_date) or "" - end_date = self.end_date and str(self.end_date) or "" - if not start_date and not end_date: - return str(self.period) - return "%s (%s-%s)" % (self.period, start_date, end_date) - - def natural_key(self): - return (self.uuid,) - - def get_values(self, prefix="", no_values=False, filtr=None, **kwargs): - values = {} - if not filtr or prefix + "reference" in filtr: - values[prefix + "reference"] = self.reference - if not filtr or prefix + "period" in filtr: - values[prefix + "period"] = str(self.period) - if not filtr or prefix + "start_date" in filtr: - values[prefix + "start_date"] = self.start_date or "" - if not filtr or prefix + "end_date" in filtr: - values[prefix + "end_date"] = self.end_date or "" - if not filtr or prefix + "dating_type" in filtr: - values[prefix + "dating_type"] = ( - str(self.dating_type) if self.dating_type else "" - ) - if not filtr or prefix + "quality" in filtr: - values[prefix + "quality"] = str(self.quality) if self.quality else "" - if not filtr or prefix + "precise_dating" in filtr: - values[prefix + "precise_dating"] = self.precise_dating - return values - - HISTORY_ATTR = [ - "reference", - "period", - "start_date", - "end_date", - "dating_type", - "quality", - "precise_dating", - ] - - def history_compress(self): - values = {} - attrs = self.HISTORY_ATTR + [self.CURRENT_MODEL_ATTR + "_id"] - for attr in attrs: - val = getattr(self, attr) - if hasattr(val, "history_compress"): - val = val.history_compress() - elif hasattr(val, "isoformat"): - val = val.isoformat() - elif val is None: - val = "" - else: - val = str(val) - values[attr] = val - return values - - @classmethod - def history_decompress(cls, full_value, create=False): - if not full_value: - return [] - full_res = [] - for value in full_value: - res = {} - for key in value: - val = value[key] - if val == "" and key != "precise_dating": - val = None - elif key in ("period", "dating_type", "quality"): - field = cls._meta.get_field(key) - q = field.related_model.objects.filter(txt_idx=val) - if q.count(): - val = q.all()[0] - else: # do not exist anymore in db - val = None - elif key in ("start_date", "end_date"): - val = int(val) - res[key] = val - if create: - res = cls.objects.create(**res) - full_res.append(res) - return full_res - - @classmethod - def is_identical(cls, dating_1, dating_2): - """ - Compare two dating attribute by attribute and return True if all - attribute is identical - """ - for attr in [ - "reference", - "period", - "start_date", - "end_date", - "dating_type", - "quality", - "precise_dating", - ]: - value1 = getattr(dating_1, attr) - value2 = getattr(dating_2, attr) - if attr == "precise_dating": - if value1: - value1 = value1.strip() - if value2: - value2 = value2.strip() - if value1 != value2: - return False - return True - - def context_records_lbl(self): - return " - ".join(cr.cached_label for cr in self.context_records.all()) - - context_records_lbl.short_description = _("Context record") - context_records_lbl.admin_order_field = "context_records__cached_label" - - def finds_lbl(self): - return " - ".join(f.cached_label for f in self.find.all()) - - finds_lbl.short_description = _("Find") - finds_lbl.admin_order_field = "find__cached_label" - - @classmethod - def fix_dating_association(cls, obj): - """ - Fix redundant m2m dating association (usually after imports) - """ - current_datings = [] - for dating in obj.datings.order_by("pk").all(): - key = ( - dating.period.pk, - dating.reference, - dating.start_date, - dating.end_date, - dating.dating_type, - dating.quality, - dating.precise_dating, - ) - if key not in current_datings: - current_datings.append(key) - continue - dating.delete() - - @property - def q_parent(self): - if not self.pk or not self.CURRENT_MODEL: - return - if getattr(self, "__q_parent", None): - return self.__q_parent - q = self.CURRENT_MODEL.objects.filter(datings__pk=self.pk) - if q.count(): - self.__q_parent = q - return q - - @property - def parent_external_id(self): - if not self.pk or not self.q_parent: - return "" - return self.q_parent.all()[0].external_id - - @property - def auto_id(self): - if not self.pk or not self.q_parent: - return 0 - parent_pk = self.q_parent.all()[0].pk - attr = "context_record" if self.CURRENT_MODEL == ContextRecord else "find" - for idx, dating_pk in enumerate( - self.__class__.objects.filter(**{attr: parent_pk}).values_list( - "pk", flat=True).all()).all(): - if dating_pk == self.pk: - return idx + 1 - return 0 - - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - if not self.pk: - return - external_id = get_generated_id("dating_external_id", self) - if external_id != self.external_id: - self.__class__.objects.filter(pk=self.pk).update(external_id=external_id) - - class Dating(BaseDating): class Meta: verbose_name = _("Dating - deprecated") diff --git a/archaeological_finds/models_finds.py b/archaeological_finds/models_finds.py index 330dc2f78..9b4156a9d 100644 --- a/archaeological_finds/models_finds.py +++ b/archaeological_finds/models_finds.py @@ -46,6 +46,7 @@ from ishtar_common.alternative_configs import ALTERNATE_CONFIGS from ishtar_common.model_managers import UUIDModelManager from ishtar_common.models import ( + BaseDating, BaseHistorizedItem, Basket, BiographicalNote, @@ -85,7 +86,7 @@ from archaeological_operations.models import ( Operation, Period, ) -from archaeological_context_records.models import BaseDating, ContextRecord, Dating, \ +from archaeological_context_records.models import ContextRecord, Dating, \ GeographicSubTownItem from archaeological_warehouse.models import Warehouse diff --git a/ishtar_common/models.py b/ishtar_common/models.py index 90e8a8d8d..c8a2f4f45 100644 --- a/ishtar_common/models.py +++ b/ishtar_common/models.py @@ -180,6 +180,7 @@ from ishtar_common.models_common import ( post_save_cache, QuickAction, SearchVectorConfig, + SerializeItem, SpatialReferenceSystem, TemplateItem, ShortMenuItem, @@ -6248,6 +6249,250 @@ post_save.connect(post_save_cache, sender=OperationType) post_delete.connect(post_save_cache, sender=OperationType) +class BaseDating(models.Model, SerializeItem): + SLUG = "dating" + SERIALIZE_EXCLUDE = ["find", "context_record", "archaeological_site"] + CURRENT_MODEL = None + CURRENT_MODEL_ATTR = None + uuid = models.UUIDField(default=uuid.uuid4) + reference = models.TextField(_("Reference"), blank=True, default="") + external_id = models.TextField(_("External ID"), blank=True, default="") + period = models.ForeignKey( + "archaeological_operations.Period", verbose_name=_("Chronological period"), + on_delete=models.PROTECT, + blank=True, + null=True, + ) + start_date = models.IntegerField(_("Start date"), blank=True, null=True) + end_date = models.IntegerField(_("End date"), blank=True, null=True) + dating_type = models.ForeignKey( + "archaeological_context_records.DatingType", + verbose_name=_("Dating type"), + on_delete=models.SET_NULL, + blank=True, + null=True, + ) + quality = models.ForeignKey( + "archaeological_context_records.DatingQuality", + verbose_name=_("Quality"), + on_delete=models.SET_NULL, + blank=True, + null=True, + ) + precise_dating = models.TextField(_("Precise on this dating"), blank=True, default="") + objects = UUIDModelManager() + ASSOCIATED_ALT_NAMES = { + "datings__period": SearchAltName( + pgettext_lazy("key for text search", "datings-period"), + "datings__period__label__iexact", + ), + "datings__precise_dating": SearchAltName( + pgettext_lazy("key for text search", "datings-precise"), + "datings__precise_dating__iexact", + ), + "datings__start_date": SearchAltName( + pgettext_lazy("key for text search", "datings-start"), + "datings__start_date", + ), + "datings__end_date": SearchAltName( + pgettext_lazy("key for text search", "datings-end"), + "datings__end_date", + ), + "datings__dating_type": SearchAltName( + pgettext_lazy("key for text search", "datings-type"), + "datings__dating_type__label__iexact", + ), + "datings__quality": SearchAltName( + pgettext_lazy("key for text search", "datings-quality"), + "datings__quality__label__iexact", + ), + } + + class Meta: + abstract = True + + def __str__(self): + if self.precise_dating and self.precise_dating.strip(): + return self.precise_dating.strip() + start_date = self.start_date and str(self.start_date) or "" + end_date = self.end_date and str(self.end_date) or "" + if not start_date and not end_date: + return str(self.period) + return "%s (%s-%s)" % (self.period, start_date, end_date) + + def natural_key(self): + return (self.uuid,) + + def get_values(self, prefix="", no_values=False, filtr=None, **kwargs): + values = {} + if not filtr or prefix + "reference" in filtr: + values[prefix + "reference"] = self.reference + if not filtr or prefix + "period" in filtr: + values[prefix + "period"] = str(self.period) + if not filtr or prefix + "start_date" in filtr: + values[prefix + "start_date"] = self.start_date or "" + if not filtr or prefix + "end_date" in filtr: + values[prefix + "end_date"] = self.end_date or "" + if not filtr or prefix + "dating_type" in filtr: + values[prefix + "dating_type"] = ( + str(self.dating_type) if self.dating_type else "" + ) + if not filtr or prefix + "quality" in filtr: + values[prefix + "quality"] = str(self.quality) if self.quality else "" + if not filtr or prefix + "precise_dating" in filtr: + values[prefix + "precise_dating"] = self.precise_dating + return values + + HISTORY_ATTR = [ + "reference", + "period", + "start_date", + "end_date", + "dating_type", + "quality", + "precise_dating", + ] + + def history_compress(self): + values = {} + attrs = self.HISTORY_ATTR + [self.CURRENT_MODEL_ATTR + "_id"] + for attr in attrs: + val = getattr(self, attr) + if hasattr(val, "history_compress"): + val = val.history_compress() + elif hasattr(val, "isoformat"): + val = val.isoformat() + elif val is None: + val = "" + else: + val = str(val) + values[attr] = val + return values + + @classmethod + def history_decompress(cls, full_value, create=False): + if not full_value: + return [] + full_res = [] + for value in full_value: + res = {} + for key in value: + val = value[key] + if val == "" and key != "precise_dating": + val = None + elif key in ("period", "dating_type", "quality"): + field = cls._meta.get_field(key) + q = field.related_model.objects.filter(txt_idx=val) + if q.count(): + val = q.all()[0] + else: # do not exist anymore in db + val = None + elif key in ("start_date", "end_date"): + val = int(val) + res[key] = val + if create: + res = cls.objects.create(**res) + full_res.append(res) + return full_res + + @classmethod + def is_identical(cls, dating_1, dating_2): + """ + Compare two dating attribute by attribute and return True if all + attribute is identical + """ + for attr in [ + "reference", + "period", + "start_date", + "end_date", + "dating_type", + "quality", + "precise_dating", + ]: + value1 = getattr(dating_1, attr) + value2 = getattr(dating_2, attr) + if attr == "precise_dating": + if value1: + value1 = value1.strip() + if value2: + value2 = value2.strip() + if value1 != value2: + return False + return True + + def context_records_lbl(self): + return " - ".join(cr.cached_label for cr in self.context_records.all()) + + context_records_lbl.short_description = _("Context record") + context_records_lbl.admin_order_field = "context_records__cached_label" + + def finds_lbl(self): + return " - ".join(f.cached_label for f in self.find.all()) + + finds_lbl.short_description = _("Find") + finds_lbl.admin_order_field = "find__cached_label" + + @classmethod + def fix_dating_association(cls, obj): + """ + Fix redundant m2m dating association (usually after imports) + """ + current_datings = [] + for dating in obj.datings.order_by("pk").all(): + key = ( + dating.period.pk, + dating.reference, + dating.start_date, + dating.end_date, + dating.dating_type, + dating.quality, + dating.precise_dating, + ) + if key not in current_datings: + current_datings.append(key) + continue + dating.delete() + + @property + def q_parent(self): + if not self.pk or not self.CURRENT_MODEL: + return + if getattr(self, "__q_parent", None): + return self.__q_parent + q = self.CURRENT_MODEL.objects.filter(datings__pk=self.pk) + if q.count(): + self.__q_parent = q + return q + + @property + def parent_external_id(self): + if not self.pk or not self.q_parent: + return "" + return self.q_parent.all()[0].external_id + + @property + def auto_id(self): + if not self.pk or not self.q_parent: + return 0 + parent_pk = self.q_parent.all()[0].pk + attr = self.CURRENT_MODEL_ATTR + for idx, dating_pk in enumerate( + self.__class__.objects.filter(**{attr: parent_pk}).values_list( + "pk", flat=True).all()).all(): + if dating_pk == self.pk: + return idx + 1 + return 0 + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + if not self.pk: + return + external_id = get_generated_id("dating_external_id", self) + if external_id != self.external_id: + self.__class__.objects.filter(pk=self.pk).update(external_id=external_id) + + class AdministrationScript(models.Model): path = models.CharField(_("Filename"), max_length=30) name = models.TextField(_("Name"), blank=True, default="") -- cgit v1.2.3