#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2012-2017 É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 from django.conf import settings from django.contrib.gis.db import models from django.core.urlresolvers import reverse from django.db import connection from django.db.models import Q from django.db.models.signals import post_delete, post_save, m2m_changed from django.utils.translation import ugettext_lazy as _, pgettext, \ activate, pgettext_lazy, deactivate from django.utils.text import slugify from ishtar_common.utils import cached_label_changed, \ m2m_historization_changed, post_save_geo from ishtar_common.model_managers import ExternalIdManager from ishtar_common.models import Document, GeneralType, \ BaseHistorizedItem, HistoricalRecords, OwnPerms, ShortMenuItem, \ GeneralRelationType, GeneralRecordRelations, post_delete_record_relation,\ post_save_cache, ValueGetter, BulkUpdatedItem, \ RelationItem, Town, get_current_profile, document_attached_changed, \ HistoryModel, SearchAltName, GeoItem, QRCodeItem, SearchVectorConfig, \ DocumentItem from archaeological_operations.models import Operation, Period, Parcel, \ ArchaeologicalSite class DatingType(GeneralType): class Meta: verbose_name = _("Dating type") verbose_name_plural = _("Dating types") ordering = ('label',) post_save.connect(post_save_cache, sender=DatingType) post_delete.connect(post_save_cache, sender=DatingType) class DatingQuality(GeneralType): class Meta: verbose_name = _("Dating quality type") verbose_name_plural = _("Dating quality types") ordering = ('label',) post_save.connect(post_save_cache, sender=DatingQuality) post_delete.connect(post_save_cache, sender=DatingQuality) class Dating(models.Model): period = models.ForeignKey(Period, verbose_name=_("Period")) 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 dating"), blank=True, null=True) class Meta: verbose_name = _("Dating") verbose_name_plural = _("Datings") def __str__(self): 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) HISTORY_ATTR = ["period", "start_date", "end_date", "dating_type", "quality", "precise_dating"] def history_compress(self): values = {} for attr in self.HISTORY_ATTR: 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 == '': 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 ["period", "start_date", "end_date", "dating_type", "quality", "precise_dating"]: value1 = getattr(dating_1, attr) value2 = getattr(dating_2, attr) if attr == "precise_dating": value1 = value1.strip() 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.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() class Unit(GeneralType): order = models.IntegerField(_("Order")) parent = models.ForeignKey( "Unit", verbose_name=_("Parent context record type"), on_delete=models.SET_NULL, blank=True, null=True) class Meta: verbose_name = _("Context record Type") verbose_name_plural = _("Context record Types") ordering = ('order', 'label') def __str__(self): return self.label post_save.connect(post_save_cache, sender=Unit) post_delete.connect(post_save_cache, sender=Unit) class ActivityType(GeneralType): order = models.IntegerField(_("Order")) class Meta: verbose_name = _("Activity Type") verbose_name_plural = _("Activity Types") ordering = ('order',) def __str__(self): return self.label post_save.connect(post_save_cache, sender=ActivityType) post_delete.connect(post_save_cache, sender=ActivityType) class IdentificationType(GeneralType): order = models.IntegerField(_("Order")) class Meta: verbose_name = _("Identification Type") verbose_name_plural = _("Identification Types") ordering = ('order', 'label') def __str__(self): return self.label post_save.connect(post_save_cache, sender=IdentificationType) post_delete.connect(post_save_cache, sender=IdentificationType) class ExcavationTechnicType(GeneralType): class Meta: verbose_name = _("Excavation technique type") verbose_name_plural = _("Excavation technique types") ordering = ('label',) post_save.connect(post_save_cache, sender=ExcavationTechnicType) post_delete.connect(post_save_cache, sender=ExcavationTechnicType) class DocumentationType(GeneralType): class Meta: verbose_name = _("Documentation type") verbose_name_plural = _("Documentation types") ordering = ('label',) post_save.connect(post_save_cache, sender=DocumentationType) post_delete.connect(post_save_cache, sender=DocumentationType) class CRBulkView(object): CREATE_SQL = """ CREATE VIEW context_records_cached_label_bulk_update AS ( SELECT cr.id AS id, ope.code_patriarche AS main_code, ope.year AS year, ope.operation_code AS ope_code, parcel.section AS section, parcel.parcel_number AS number, cr.label AS label FROM archaeological_context_records_contextrecord AS cr INNER JOIN archaeological_operations_operation ope ON ope.id = cr.operation_id INNER JOIN archaeological_operations_parcel parcel ON cr.parcel_id = parcel.id );""" DELETE_SQL = """ DROP VIEW context_records_cached_label_bulk_update; """ class ContextRecord(BulkUpdatedItem, DocumentItem, BaseHistorizedItem, QRCodeItem, GeoItem, OwnPerms, ValueGetter, ShortMenuItem, RelationItem): SLUG = 'contextrecord' APP = "archaeological-context-records" MODEL = "context-record" SHOW_URL = 'show-contextrecord' EXTERNAL_ID_KEY = 'context_record_external_id' EXTERNAL_ID_DEPENDENCIES = ['base_finds'] TABLE_COLS = ['label', 'operation__common_name', 'town__name', 'parcel__cached_label', 'unit__label'] if settings.COUNTRY == 'fr': TABLE_COLS.insert(1, 'operation__code_patriarche') TABLE_COLS_FOR_OPE = ['label', 'parcel', 'unit__label', 'cached_periods', 'description'] NEW_QUERY_ENGINE = True COL_LABELS = { 'cached_periods': _("Periods"), 'datings__period__label': _("Periods"), 'datings__period': _("Datings (period)"), 'detailed_related_context_records': _("Related context records"), "cached_related_context_records": _("Related context records"), 'operation__code_patriarche': _("Operation (Patriarche code)"), 'operation__common_name': _("Operation (name)"), 'parcel__external_id': _("Parcel (external ID)"), 'town__name': _("Town"), 'town': _("Town"), 'parcel__year': _("Parcel (year)"), 'section__parcel_number': _("Parcel"), 'parcel__cached_label': _("Parcel"), 'unit__label': _("Context record type"), } CONTEXTUAL_TABLE_COLS = { 'full': { 'related_context_records': 'cached_related_context_records' } } # statistics STATISTIC_MODALITIES_OPTIONS = OrderedDict([ ("unit__label", _("Context record type")), ("datings__period__label", _("Period")), ("identification__label", _("Identification")), ("activity__label", _("Activity")), ("excavation_technic__label", _("Excavation technique")), ("documents__source_type__label", _("Associated document type")), ]) STATISTIC_MODALITIES = [ key for key, lbl in STATISTIC_MODALITIES_OPTIONS.items()] # search parameters EXTRA_REQUEST_KEYS = { 'town': 'town__pk', 'town__name': 'town__name', 'parcel__cached_label': 'parcel__cached_label', 'operation__year': 'operation__year__contains', 'year': 'operation__year__contains', 'operation__code_patriarche': 'operation__code_patriarche', 'operation__operation_code': 'operation__operation_code', 'operation__common_name': 'operation__common_name', 'datings__period': 'datings__period__pk', 'parcel_0': 'operation__parcels__section', 'parcel_1': 'operation__parcels__parcel_number', 'parcel_2': 'operation__parcels__public_domain', 'label': 'label__icontains', 'archaeological_sites': 'operation__archaeological_sites__pk', 'cached_label': 'cached_label__icontains', 'datings__period__label': 'datings__period__label', 'operation_id': 'operation_id', 'unit__label': "unit__label" } RELATION_TYPES_PREFIX = {'ope_relation_types': 'operation__', 'cr_relation_types': ''} # alternative names of fields for searches ALT_NAMES = { 'label': SearchAltName( pgettext_lazy("key for text search", "id"), 'label__iexact' ), 'town': SearchAltName( pgettext_lazy("key for text search", "town"), 'town__cached_label__iexact' ), 'operation__year': SearchAltName( pgettext_lazy("key for text search", "operation-year"), 'operation__year' ), 'operation__code_patriarche': SearchAltName( pgettext_lazy("key for text search", "patriarche"), 'operation__code_patriarche__iexact' ), 'operation__operation_code': SearchAltName( pgettext_lazy("key for text search", "operation-code"), 'operation__operation_code' ), 'operation__cached_label': SearchAltName( pgettext_lazy("key for text search", "operation"), 'operation__cached_label__icontains' ), 'archaeological_site': SearchAltName( pgettext_lazy("key for text search", "site"), 'archaeological_site__cached_label__icontains' ), 'ope_relation_types': SearchAltName( pgettext_lazy("key for text search", "operation-relation-type"), 'ope_relation_types' ), 'datings__period': SearchAltName( pgettext_lazy("key for text search", "period"), 'datings__period__label__iexact' ), 'unit': SearchAltName( pgettext_lazy("key for text search", "unit-type"), 'unit__label__iexact' ), 'parcel': SearchAltName( pgettext_lazy("key for text search", "parcel"), 'parcel__cached_label__iexact' ), 'cr_relation_types': SearchAltName( pgettext_lazy("key for text search", "record-relation-type"), 'cr_relation_types' ), } ALT_NAMES.update(BaseHistorizedItem.ALT_NAMES) PARENT_ONLY_SEARCH_VECTORS = ["operation", "archaeological_site", "parcel"] BASE_SEARCH_VECTORS = [ SearchVectorConfig("cached_label"), SearchVectorConfig("label"), SearchVectorConfig("location"), SearchVectorConfig("town__name"), SearchVectorConfig("interpretation", 'local'), SearchVectorConfig("filling", 'local'), SearchVectorConfig("datings_comment", 'local'), SearchVectorConfig("identification__label"), SearchVectorConfig("activity__label"), SearchVectorConfig("excavation_technic__label")] M2M_SEARCH_VECTORS = [ SearchVectorConfig("datings__period__label", 'local') ] UP_MODEL_QUERY = { "operation": ( pgettext_lazy("key for text search", "operation"), 'cached_label'), "site": ( pgettext_lazy("key for text search", "site"), 'cached_label'), } MAIN_UP_MODEL_QUERY = "operation" RELATIVE_SESSION_NAMES = [ ('operation', 'operation__pk'), ('site', 'archaeological_site__pk'), ('file', 'operation__associated_file__pk'), ] HISTORICAL_M2M = [ 'datings', 'documentations' ] CACHED_LABELS = ['cached_label', 'cached_periods', "cached_related_context_records"] DOWN_MODEL_UPDATE = ["base_finds"] history = HistoricalRecords(bases=[HistoryModel]) objects = ExternalIdManager() # fields external_id = models.TextField(_("External ID"), blank=True, null=True) auto_external_id = models.BooleanField( _("External ID is set automatically"), default=False) parcel = models.ForeignKey( Parcel, verbose_name=_("Parcel"), related_name='context_record', on_delete=models.SET_NULL, blank=True, null=True) town = models.ForeignKey( Town, verbose_name=_("Town"), related_name='context_record', on_delete=models.SET_NULL, blank=True, null=True) operation = models.ForeignKey(Operation, verbose_name=_("Operation"), related_name='context_record') archaeological_site = models.ForeignKey( ArchaeologicalSite, verbose_name=_("Archaeological site"), on_delete=models.SET_NULL, blank=True, null=True, related_name='context_records') label = models.CharField(_("ID"), max_length=200) description = models.TextField(_("Description"), blank=True, null=True) comment = models.TextField(_("General comment"), blank=True, null=True) opening_date = models.DateField(_("Opening date"), blank=True, null=True) closing_date = models.DateField(_("Closing date"), blank=True, null=True) length = models.FloatField(_("Length (m)"), blank=True, null=True) width = models.FloatField(_("Width (m)"), blank=True, null=True) thickness = models.FloatField(_("Thickness (m)"), blank=True, null=True) diameter = models.FloatField(_("Diameter (m)"), blank=True, null=True) depth = models.FloatField(_("Depth (m)"), blank=True, null=True) depth_of_appearance = models.FloatField( _("Depth of appearance (m)"), blank=True, null=True) location = models.TextField( _("Location"), blank=True, null=True, help_text=_("A short description of the location of the context " "record")) datings = models.ManyToManyField(Dating, related_name='context_records') documentations = models.ManyToManyField(DocumentationType, blank=True) datings_comment = models.TextField(_("Comment on datings"), blank=True, null=True) unit = models.ForeignKey(Unit, verbose_name=_("Context record type"), on_delete=models.SET_NULL, related_name='+', blank=True, null=True) filling = models.TextField(_("Filling"), blank=True, null=True) interpretation = models.TextField(_("Interpretation"), blank=True, null=True) taq = models.IntegerField( _("TAQ"), blank=True, null=True, help_text=_("\"Terminus Ante Quem\" the context record can't have " "been created after this date")) taq_estimated = models.IntegerField( _("Estimated TAQ"), blank=True, null=True, help_text=_("Estimation of a \"Terminus Ante Quem\"")) tpq = models.IntegerField( _("TPQ"), blank=True, null=True, help_text=_("\"Terminus Post Quem\" the context record can't have " "been created before this date")) tpq_estimated = models.IntegerField( _("Estimated TPQ"), blank=True, null=True, help_text=_("Estimation of a \"Terminus Post Quem\"")) identification = models.ForeignKey( IdentificationType, blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_("Identification"),) activity = models.ForeignKey(ActivityType, blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_("Activity"),) excavation_technic = models.ForeignKey( ExcavationTechnicType, blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_("Excavation technique")) related_context_records = models.ManyToManyField( 'ContextRecord', through='RecordRelations', blank=True) documents = models.ManyToManyField( Document, related_name='context_records', verbose_name=_("Documents"), blank=True) main_image = models.ForeignKey( Document, related_name='main_image_context_records', on_delete=models.SET_NULL, verbose_name=_("Main image"), blank=True, null=True) cached_label = models.TextField(_("Cached name"), null=True, blank=True, db_index=True) cached_periods = models.TextField( _("Cached periods label"), blank=True, null=True, help_text=_("Generated automatically - do not edit") ) cached_related_context_records = models.TextField( _("Cached related context records"), blank=True, null=True, help_text=_("Generated automatically - do not edit") ) class Meta: verbose_name = _("Context Record") verbose_name_plural = _("Context Record") permissions = ( ("view_contextrecord", "Can view all Context Records"), ("view_own_contextrecord", "Can view own Context Record"), ("add_own_contextrecord", "Can add own Context Record"), ("change_own_contextrecord", "Can change own Context Record"), ("delete_own_contextrecord", "Can delete own Context Record"), ) ordering = ('cached_label',) def natural_key(self): return (self.external_id, ) @property def name(self): return self.label or "" @property def short_class_name(self): return pgettext("short", "Context record") def __str__(self): return self.short_label or "" def get_values(self, prefix='', no_values=False, no_base_finds=True): values = super(ContextRecord, self).get_values(prefix=prefix, no_values=no_values) if prefix and no_base_finds: return values values[prefix + 'base_finds'] = [ bf.get_values(prefix=prefix, no_values=True) for bf in self.base_finds.distinct().all() ] return values def get_town_centroid(self): if self.town: return self.town.center, self._meta.verbose_name if self.archaeological_site: centroid = self.archaeological_site.get_town_centroid() if centroid: return centroid return self.operation.get_town_centroid() def get_town_polygons(self): if self.town: return self.town.limit, self._meta.verbose_name if self.archaeological_site: polys = self.archaeological_site.get_town_polygons() if polys: return polys return self.operation.get_town_polygons() def get_precise_points(self): precise_points = super(ContextRecord, self).get_precise_points() if precise_points: return precise_points if self.archaeological_site: precise_points = self.archaeological_site.get_precise_points() if precise_points: return precise_points return self.operation.get_precise_points() def get_precise_polygons(self): precise_poly = super(ContextRecord, self).get_precise_polygons() if precise_poly: return precise_poly if self.archaeological_site: precise_poly = self.archaeological_site.get_precise_polygons() if precise_poly: return precise_poly return self.operation.get_precise_polygons() @classmethod def cached_label_bulk_update(cls, operation_id=None, parcel_id=None, transaction_id=None): transaction_id, is_recursion = cls.bulk_recursion( transaction_id, [operation_id, parcel_id]) if is_recursion: return if operation_id: where = "operation_id = %s" args = [int(operation_id)] kwargs = {'operation_id': operation_id} elif parcel_id: where = "parcel_id = %s" args = [int(parcel_id)] kwargs = {'parcel_id': parcel_id} else: return kwargs['transaction_id'] = transaction_id profile = get_current_profile() sql = """ UPDATE "archaeological_context_records_contextrecord" AS cr SET cached_label = CASE WHEN context_records_cached_label_bulk_update.main_code IS NULL THEN CASE WHEN context_records_cached_label_bulk_update.year IS NOT NULL AND context_records_cached_label_bulk_update.ope_code IS NOT NULL THEN '{ope_prefix}' || context_records_cached_label_bulk_update.year || '-' || context_records_cached_label_bulk_update.ope_code ELSE '' END ELSE '{main_ope_prefix}' || context_records_cached_label_bulk_update.main_code END || '{join}' || context_records_cached_label_bulk_update.section || '{join}' || context_records_cached_label_bulk_update.number || '{join}' || context_records_cached_label_bulk_update.label FROM context_records_cached_label_bulk_update WHERE cr.id = context_records_cached_label_bulk_update.id AND cr.id IN ( SELECT id FROM archaeological_context_records_contextrecord WHERE {where} ); """.format(main_ope_prefix=profile.operation_prefix, ope_prefix=profile.default_operation_prefix, join=settings.JOINT, where=where) with connection.cursor() as c: c.execute(sql, args) cls._meta.get_field( 'base_finds').related_model.cached_label_bulk_update(**kwargs) @property def short_label(self): return settings.JOINT.join([str(item) for item in [ self.operation.get_reference(), self.parcel, self.label] if item]) @property def relation_label(self): return self.label @property def show_url(self): return reverse('show-contextrecord', args=[self.pk, '']) @classmethod def get_query_owns(cls, ishtaruser): q = cls._construct_query_own( 'operation__', Operation._get_query_owns_dicts(ishtaruser) ) | cls._construct_query_own( 'base_finds__find__basket__', [{"shared_with": ishtaruser, "shared_write_with": ishtaruser}] ) | cls._construct_query_own('', [ {'history_creator': ishtaruser.user_ptr}, {'operation__end_date__isnull': True} ]) return q @classmethod def get_owns(cls, user, menu_filtr=None, limit=None, values=None, get_short_menu_class=None): replace_query = None if menu_filtr and 'operation' in menu_filtr: replace_query = Q(operation=menu_filtr['operation']) owns = super(ContextRecord, cls).get_owns( user, replace_query=replace_query, limit=limit, values=values, get_short_menu_class=get_short_menu_class) return cls._return_get_owns(owns, values, get_short_menu_class) def full_label(self): return str(self) def _real_label(self): if not self.operation.code_patriarche: return return settings.JOINT.join((self.operation.code_patriarche, self.label)) def _temp_label(self): if self.operation.code_patriarche: return return settings.JOINT.join([str(lbl) for lbl in [ self.operation.year, self.operation.operation_code, self.label] if lbl]) def _generate_cached_label(self): return self.full_label() def _generate_cached_periods(self): return " & ".join([dating.period.label for dating in self.datings.all()]) def _generate_cached_related_context_records(self): return self.detailed_related_context_records() def _get_associated_cached_labels(self): from archaeological_finds.models import Find, BaseFind return list(Find.objects.filter(base_finds__context_record=self).all())\ + list(BaseFind.objects.filter(context_record=self).all()) def _cached_labels_bulk_update(self): self.base_finds.model.cached_label_bulk_update( context_record_id=self.pk) return True def _get_base_image_path(self): return self.operation._get_base_image_path() + \ "/{}/{}".format(self.SLUG, slugify(self.label or "00")) @property def archaeological_site_reference(self): if self.archaeological_site: return self.archaeological_site.reference if self.operation.archaeological_sites.count(): return "-".join( [a.reference for a in self.operation.archaeological_sites.all()] ) return "" @property def reference(self): if not self.operation: return "00" return self.full_label() def get_department(self): if not self.operation: return "00" return self.operation.get_department() def get_town_label(self): if not self.operation: return "00" return self.operation.get_town_label() @classmethod def get_periods(cls, slice='year', fltr={}): q = cls.objects if fltr: q = q.filter(**fltr) if slice == 'year': years = set() for res in list(q.values('operation__start_date')): if res['operation__start_date']: yr = res['operation__start_date'].year years.add(yr) return list(years) return [] @classmethod def get_by_year(cls, year, fltr={}): q = cls.objects if fltr: q = q.filter(**fltr) return q.filter(operation__start_date__year=year) @classmethod def get_operations(cls): return [dct['operation__pk'] for dct in cls.objects.values('operation__pk').distinct()] @classmethod def get_by_operation(cls, operation_id): return cls.objects.filter(operation__pk=operation_id) @classmethod def get_total_number(cls, fltr={}): q = cls.objects if fltr: q = q.filter(**fltr) return q.count() def detailed_related_context_records(self): crs = [] for cr in self.right_relations.all(): crs.append("{} ({})".format(cr.right_record, cr.relation_type.get_tiny_label())) return " & ".join(crs) def find_docs_q(self): return Document.objects.filter(finds__base_finds__context_record=self) def fix(self): """ Fix redundant m2m dating association (usually after imports) """ Dating.fix_dating_association(self) def save(self, *args, **kwargs): super(ContextRecord, self).save(*args, **kwargs) if (not self.town and self.parcel) or ( self.parcel and self.parcel.town != self.town): self.town = self.parcel.town self.skip_history_when_saving = True self.save() def context_record_post_save(sender, **kwargs): cached_label_changed(sender=sender, **kwargs) post_save_geo(sender=sender, **kwargs) post_save.connect(context_record_post_save, sender=ContextRecord) m2m_changed.connect(document_attached_changed, sender=ContextRecord.documents.through) for attr in ContextRecord.HISTORICAL_M2M: m2m_changed.connect(m2m_historization_changed, sender=getattr(ContextRecord, attr).through) class RelationType(GeneralRelationType): class Meta: verbose_name = _("Relation type") verbose_name_plural = _("Relation types") ordering = ('order', 'label') class RecordRelations(GeneralRecordRelations, models.Model): MAIN_ATTR = 'left_record' left_record = models.ForeignKey(ContextRecord, related_name='right_relations') right_record = models.ForeignKey(ContextRecord, related_name='left_relations') relation_type = models.ForeignKey(RelationType) TABLE_COLS = [ "left_record__label", "left_record__unit", "left_record__parcel", "relation_type", "right_record__label", "right_record__unit", "right_record__parcel", ] COL_LABELS = { "left_record__label": _("ID (left)"), "left_record__unit": _("Context record type (left)"), "left_record__parcel": _("Parcel (left)"), "left_record__description": _("Description (left)"), "left_record__datings__period": _("Periods (left)"), "relation_type": _("Relation type"), "right_record__label": _("ID (right)"), "right_record__unit": _("Context record type (right)"), "right_record__parcel": _("Parcel (right)"), "right_record__description": _("Description (right)"), "right_record__datings__period": _("Periods (right)") } # search parameters EXTRA_REQUEST_KEYS = { "left_record__operation": "left_record__operation__pk" } class Meta: verbose_name = _("Record relation") verbose_name_plural = _("Record relations") permissions = [ ("view_recordrelation", "Can view all Context record relations"), ] post_delete.connect(post_delete_record_relation, sender=RecordRelations) class RecordRelationView(models.Model): CREATE_SQL = """ CREATE VIEW record_relations AS SELECT DISTINCT right_record_id as id, right_record_id, left_record_id, relation_type_id FROM archaeological_context_records_recordrelations; -- deactivate deletion CREATE RULE record_relations_del AS ON DELETE TO record_relations DO INSTEAD DELETE FROM record_relations where id=NULL; """ DELETE_SQL = """ DROP VIEW record_relations; """ TABLE_COLS = [ "relation_type", "right_record__label", "right_record__unit", "right_record__parcel", "right_record__datings__period", "right_record__description"] COL_LABELS = { "relation_type": _("Relation type"), "right_record__label": _("ID"), "right_record__unit": _("Context record type"), "right_record__parcel": _("Parcel"), "right_record__description": _("Description"), "right_record__datings__period": _("Periods") } # search parameters EXTRA_REQUEST_KEYS = { "left_record_id": "left_record_id", "right_record__unit": "right_record__unit__label" } left_record = models.ForeignKey(ContextRecord, related_name='+', on_delete=models.DO_NOTHING) right_record = models.ForeignKey(ContextRecord, related_name='+', on_delete=models.DO_NOTHING) relation_type = models.ForeignKey(RelationType, related_name='+', on_delete=models.DO_NOTHING) class Meta: managed = False db_table = 'record_relations' unique_together = ('id', 'right_record') permissions = [ ("view_recordrelation", "Can view all record relations - view"), ] @classmethod def general_types(cls): return [] def __str__(self): return "{} \"{}\"".format(self.relation_type, self.right_record)