diff --git a/arches/app/models/concept.py b/arches/app/models/concept.py
index e90d8bc9a7f..7ab424eb36d 100644
--- a/arches/app/models/concept.py
+++ b/arches/app/models/concept.py
@@ -1,1483 +1,1483 @@
-"""
-ARCHES - a program developed to inventory and manage immovable cultural heritage.
-Copyright (C) 2013 J. Paul Getty Trust and World Monuments Fund
-
-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 .
-"""
-
-import re
-import uuid
-import copy
-from operator import itemgetter
-from operator import methodcaller
-from django.db import transaction, connection
-from django.db.models import Q
-from arches.app.models import models
-from arches.app.models.system_settings import settings
-from arches.app.search.search_engine_factory import SearchEngineInstance as se
-from arches.app.search.elasticsearch_dsl_builder import Term, Query, Bool, Match, Terms
-from arches.app.search.mappings import CONCEPTS_INDEX
-from arches.app.utils.betterJSONSerializer import JSONSerializer, JSONDeserializer
-from arches.app.utils.i18n import rank_label
-from django.utils.translation import get_language, gettext as _
-from django.db import IntegrityError
-from psycopg2.extensions import AsIs
-
-import logging
-
-
-logger = logging.getLogger(__name__)
-
-CORE_CONCEPTS = (
- "00000000-0000-0000-0000-000000000001",
- "00000000-0000-0000-0000-000000000004",
- "00000000-0000-0000-0000-000000000005",
- "00000000-0000-0000-0000-000000000006",
-)
-
-
-class Concept(object):
- def __init__(self, *args, **kwargs):
- self.id = ""
- self.nodetype = ""
- self.legacyoid = ""
- self.relationshiptype = ""
- self.values = []
- self.subconcepts = []
- self.parentconcepts = []
- self.relatedconcepts = []
- self.hassubconcepts = False
-
- if len(args) != 0:
- if isinstance(args[0], str):
- try:
- uuid.UUID(args[0])
- self.get(args[0])
- except ValueError:
- self.load(JSONDeserializer().deserialize(args[0]))
- elif isinstance(args[0], dict):
- self.load(args[0])
- elif isinstance(args[0], object):
- self.load(args[0])
-
- def __unicode__(self):
- return ("%s - %s") % (self.get_preflabel().value, self.id)
-
- def __hash__(self):
- return hash(self.id)
-
- def __eq__(self, x):
- return hash(self) == hash(x)
-
- def __ne__(self, x):
- return hash(self) != hash(x)
-
- def load(self, value):
- if isinstance(value, dict):
- self.id = str(value["id"]) if "id" in value else ""
- self.nodetype = value["nodetype"] if "nodetype" in value else ""
- self.legacyoid = value["legacyoid"] if "legacyoid" in value else ""
- self.relationshiptype = value["relationshiptype"] if "relationshiptype" in value else ""
- if "values" in value:
- for val in value["values"]:
- self.addvalue(val)
- if "subconcepts" in value:
- for subconcept in value["subconcepts"]:
- self.addsubconcept(subconcept)
- if "parentconcepts" in value:
- for parentconcept in value["parentconcepts"]:
- self.addparent(parentconcept)
- if "relatedconcepts" in value:
- for relatedconcept in value["relatedconcepts"]:
- self.addrelatedconcept(relatedconcept)
-
- if isinstance(value, models.Concept):
- self.id = str(value.pk)
- self.nodetype = value.nodetype_id
- self.legacyoid = value.legacyoid
-
- def get(
- self,
- id="",
- legacyoid="",
- include_subconcepts=False,
- include_parentconcepts=False,
- include_relatedconcepts=False,
- exclude=[],
- include=[],
- depth_limit=None,
- up_depth_limit=None,
- lang=settings.LANGUAGE_CODE,
- semantic=True,
- pathway_filter=None,
- **kwargs,
- ):
- if id != "":
- self.load(models.Concept.objects.get(pk=id))
- elif legacyoid != "":
- self.load(models.Concept.objects.get(legacyoid=legacyoid))
-
- _cache = kwargs.pop("_cache", {})
- _cache[self.id] = self.__class__(
- {"id": self.id, "nodetype": self.nodetype, "legacyoid": self.legacyoid, "relationshiptype": self.relationshiptype}
- )
-
- if semantic == True:
- pathway_filter = (
- pathway_filter
- if pathway_filter
- else Q(relationtype__category="Semantic Relations") | Q(relationtype__category="Properties")
- )
- else:
- pathway_filter = pathway_filter if pathway_filter else Q(relationtype="member") | Q(relationtype="hasCollection")
-
- if self.id != "":
- nodetype = kwargs.pop("nodetype", self.nodetype)
- uplevel = kwargs.pop("uplevel", 0)
- downlevel = kwargs.pop("downlevel", 0)
- depth_limit = depth_limit if depth_limit is None else int(depth_limit)
- up_depth_limit = up_depth_limit if up_depth_limit is None else int(up_depth_limit)
-
- if include is not None:
- if len(include) > 0 and len(exclude) > 0:
- raise Exception(_("Only include values for include or exclude, but not both"))
- include = (
- include if len(include) != 0 else models.DValueType.objects.distinct("category").values_list("category", flat=True)
- )
- include = set(include).difference(exclude)
- exclude = []
-
- if len(include) > 0:
- values = models.Value.objects.filter(concept=self.id)
- for value in values:
- if value.valuetype.category in include:
- self.values.append(ConceptValue(value))
-
- hassubconcepts = models.Relation.objects.filter(Q(conceptfrom=self.id), pathway_filter, ~Q(relationtype="related"))[0:1]
- if len(hassubconcepts) > 0:
- self.hassubconcepts = True
-
- if include_subconcepts:
- conceptrealations = models.Relation.objects.filter(Q(conceptfrom=self.id), pathway_filter, ~Q(relationtype="related"))
- if depth_limit is None or downlevel < depth_limit:
- if depth_limit is not None:
- downlevel = downlevel + 1
- for relation in conceptrealations:
- subconcept = (
- _cache[str(relation.conceptto_id)]
- if str(relation.conceptto_id) in _cache
- else self.__class__().get(
- id=relation.conceptto_id,
- include_subconcepts=include_subconcepts,
- include_parentconcepts=include_parentconcepts,
- include_relatedconcepts=include_relatedconcepts,
- exclude=exclude,
- include=include,
- depth_limit=depth_limit,
- up_depth_limit=up_depth_limit,
- downlevel=downlevel,
- uplevel=uplevel,
- nodetype=nodetype,
- semantic=semantic,
- pathway_filter=pathway_filter,
- _cache=_cache.copy(),
- lang=lang,
- )
- )
- subconcept.relationshiptype = relation.relationtype_id
- self.subconcepts.append(subconcept)
-
- self.subconcepts = sorted(
- self.subconcepts, key=lambda concept: self.natural_keys(concept.get_sortkey(lang)), reverse=False
- )
- # self.subconcepts = sorted(self.subconcepts, key=methodcaller(
- # 'get_sortkey', lang=lang), reverse=False)
-
- if include_parentconcepts:
- conceptrealations = models.Relation.objects.filter(Q(conceptto=self.id), pathway_filter, ~Q(relationtype="related"))
- if up_depth_limit is None or uplevel < up_depth_limit:
- if up_depth_limit is not None:
- uplevel = uplevel + 1
- for relation in conceptrealations:
- parentconcept = (
- _cache[str(relation.conceptfrom_id)]
- if str(relation.conceptfrom_id) in _cache
- else self.__class__().get(
- id=relation.conceptfrom_id,
- include_subconcepts=False,
- include_parentconcepts=include_parentconcepts,
- include_relatedconcepts=include_relatedconcepts,
- exclude=exclude,
- include=include,
- depth_limit=depth_limit,
- up_depth_limit=up_depth_limit,
- downlevel=downlevel,
- uplevel=uplevel,
- nodetype=nodetype,
- semantic=semantic,
- pathway_filter=pathway_filter,
- _cache=_cache.copy(),
- lang=lang,
- )
- )
- parentconcept.relationshiptype = relation.relationtype_id
-
- self.parentconcepts.append(parentconcept)
-
- if include_relatedconcepts:
- conceptrealations = models.Relation.objects.filter(
- Q(relationtype="related") | Q(relationtype__category="Mapping Properties"),
- Q(conceptto=self.id) | Q(conceptfrom=self.id),
- )
- relations = []
- for relation in conceptrealations:
- if str(relation.conceptto_id) != self.id and str(relation.relationid) not in relations:
- relations.append(str(relation.relationid))
- relatedconcept = self.__class__().get(relation.conceptto_id, include=["label"], lang=lang)
- relatedconcept.relationshiptype = relation.relationtype_id
-
- self.relatedconcepts.append(relatedconcept)
- if str(relation.conceptfrom_id) != self.id and str(relation.relationid) not in relations:
- relations.append(str(relation.relationid))
- relatedconcept = self.__class__().get(relation.conceptfrom_id, include=["label"], lang=lang)
- relatedconcept.relationshiptype = relation.relationtype_id
-
- self.relatedconcepts.append(relatedconcept)
-
- return self
-
- def save(self):
- self.id = self.id if (self.id != "" and self.id is not None) else str(uuid.uuid4())
- concept, created = models.Concept.objects.get_or_create(
- pk=self.id, defaults={"legacyoid": self.legacyoid if self.legacyoid != "" else self.id, "nodetype_id": self.nodetype}
- )
-
- for value in self.values:
- if not isinstance(value, ConceptValue):
- value = ConceptValue(value)
- value.conceptid = self.id
- value.save()
-
- for parentconcept in self.parentconcepts:
- parentconcept.save()
- parentconcept.add_relation(self, parentconcept.relationshiptype)
-
- for subconcept in self.subconcepts:
- subconcept.save()
- self.add_relation(subconcept, subconcept.relationshiptype)
-
- # if we're moving a Concept Scheme below another Concept or Concept Scheme
- if len(self.parentconcepts) > 0 and concept.nodetype_id == "ConceptScheme":
- concept.nodetype_id = "Concept"
- concept.save()
- self.load(concept)
-
- for relation in models.Relation.objects.filter(conceptfrom=concept, relationtype_id="hasTopConcept"):
- relation.relationtype_id = "narrower"
- relation.save()
-
- for relatedconcept in self.relatedconcepts:
- self.add_relation(relatedconcept, relatedconcept.relationshiptype)
-
- if relatedconcept.relationshiptype == "member":
- child_concepts = relatedconcept.get(include_subconcepts=True)
-
- def applyRelationship(concept):
- for subconcept in concept.subconcepts:
- concept.add_relation(subconcept, relatedconcept.relationshiptype)
-
- child_concepts.traverse(applyRelationship)
-
- return concept
-
- def delete(self, delete_self=False):
- """
- Deletes any subconcepts associated with this concept and additionally this concept if 'delete_self' is True
- If any parentconcepts or relatedconcepts are included then it will only delete the relationship to those concepts but not the concepts themselves
- If any values are passed, then those values as well as the relationship to those values will be deleted
-
- Note, django will automatically take care of deleting any db models that have a foreign key relationship to the model being deleted
- (eg: deleting a concept model will also delete all values and relationships), but because we need to manage deleting
- parent concepts and related concepts and values we have to do that here too
-
- """
-
- for subconcept in self.subconcepts:
- concepts_to_delete = Concept.gather_concepts_to_delete(subconcept)
- for key, concept in concepts_to_delete.items():
- models.Concept.objects.get(pk=key).delete()
-
- for parentconcept in self.parentconcepts:
- relations_filter = (
- (Q(relationtype__category="Semantic Relations") | Q(relationtype="hasTopConcept"))
- & Q(conceptfrom=parentconcept.id)
- & Q(conceptto=self.id)
- )
- conceptrelations = models.Relation.objects.filter(relations_filter)
- for relation in conceptrelations:
- relation.delete()
-
- if not models.Relation.objects.filter(relations_filter).exists():
- # we've removed all parent concepts so now this concept needs to be promoted to a Concept Scheme
- concept = models.Concept.objects.get(pk=self.id)
- concept.nodetype_id = "ConceptScheme"
- concept.save()
- self.load(concept)
-
- for relation in models.Relation.objects.filter(conceptfrom=concept, relationtype_id="narrower"):
- relation.relationtype_id = "hasTopConcept"
- relation.save()
-
- deletedrelatedconcepts = []
- for relatedconcept in self.relatedconcepts:
- conceptrelations = models.Relation.objects.filter(
- Q(relationtype="related") | Q(relationtype="member") | Q(relationtype__category="Mapping Properties"),
- conceptto=relatedconcept.id,
- conceptfrom=self.id,
- )
- for relation in conceptrelations:
- relation.delete()
- deletedrelatedconcepts.append(relatedconcept)
-
- conceptrelations = models.Relation.objects.filter(
- Q(relationtype="related") | Q(relationtype="member") | Q(relationtype__category="Mapping Properties"),
- conceptfrom=relatedconcept.id,
- conceptto=self.id,
- )
- for relation in conceptrelations:
- relation.delete()
- deletedrelatedconcepts.append(relatedconcept)
-
- for deletedrelatedconcept in deletedrelatedconcepts:
- if deletedrelatedconcept in self.relatedconcepts:
- self.relatedconcepts.remove(deletedrelatedconcept)
-
- for value in self.values:
- if not isinstance(value, ConceptValue):
- value = ConceptValue(value)
- value.delete()
-
- if delete_self:
- concepts_to_delete = Concept.gather_concepts_to_delete(self)
- for key, concept in concepts_to_delete.items():
- # delete only member relationships if the nodetype == Collection
- if concept.nodetype == "Collection":
- concept = Concept().get(
- id=concept.id,
- include_subconcepts=True,
- include_parentconcepts=True,
- include=["label"],
- up_depth_limit=1,
- semantic=False,
- )
-
- def find_concepts(concept):
- if len(concept.parentconcepts) <= 1:
- for subconcept in concept.subconcepts:
- conceptrelation = models.Relation.objects.get(
- conceptfrom=concept.id, conceptto=subconcept.id, relationtype="member"
- )
- conceptrelation.delete()
- find_concepts(subconcept)
-
- find_concepts(concept)
- # if the concept is a collection, loop through the nodes and delete their rdmCollection values
- for node in models.Node.objects.filter(config__rdmCollection=concept.id):
- node.config["rdmCollection"] = None
- node.save()
-
- models.Concept.objects.get(pk=key).delete()
- return
-
- def add_relation(self, concepttorelate, relationtype):
- """
- Relates this concept to 'concepttorelate' via the relationtype
-
- """
-
- relation, created = models.Relation.objects.get_or_create(
- conceptfrom_id=self.id, conceptto_id=concepttorelate.id, relationtype_id=relationtype
- )
- return relation
-
- @staticmethod
- def gather_concepts_to_delete(concept, lang=settings.LANGUAGE_CODE):
- """
- Gets a dictionary of all the concepts ids to delete
- The values of the dictionary keys differ somewhat depending on the node type being deleted
- If the nodetype == 'Concept' then return ConceptValue objects keyed to the concept id
- If the nodetype == 'ConceptScheme' then return a ConceptValue object with the value set to any ONE prefLabel keyed to the concept id
- We do this because it takes so long to gather the ids of the concepts when deleting a Scheme or Group
-
- """
-
- concepts_to_delete = {}
-
- # Here we have to worry about making sure we don't delete nodes that have more than 1 parent
- if concept.nodetype == "Concept":
- concept = Concept().get(
- id=concept.id, include_subconcepts=True, include_parentconcepts=True, include=["label"], up_depth_limit=1
- )
-
- def find_concepts(concept):
- if len(concept.parentconcepts) <= 1:
- concepts_to_delete[concept.id] = concept
- for subconcept in concept.subconcepts:
- find_concepts(subconcept)
-
- find_concepts(concept)
- return concepts_to_delete
-
- # here we can just delete everything and so use a recursive CTE to get the concept ids much more quickly
- if concept.nodetype == "ConceptScheme":
- concepts_to_delete[concept.id] = concept
- rows = Concept().get_child_concepts(concept.id)
- for row in rows:
- if row[0] not in concepts_to_delete:
- concepts_to_delete[row[0]] = Concept({"id": row[0]})
-
- concepts_to_delete[row[0]].addvalue({"id": row[2], "conceptid": row[0], "value": row[1]})
-
- if concept.nodetype == "Collection":
- concepts_to_delete[concept.id] = concept
- rows = Concept().get_child_collections(concept.id)
- for row in rows:
- if row[0] not in concepts_to_delete:
- concepts_to_delete[row[0]] = Concept({"id": row[0]})
-
- concepts_to_delete[row[0]].addvalue({"id": row[2], "conceptid": row[0], "value": row[1]})
-
- return concepts_to_delete
-
- def get_child_collections_hierarchically(self, conceptid, child_valuetypes=None, offset=0, limit=50, query=None):
- child_valuetypes = child_valuetypes if child_valuetypes else ["prefLabel"]
- columns = "valueidto::text, conceptidto::text, valueto, valuetypeto, depth, count(*) OVER() AS full_count, collector"
- return self.get_child_edges(
- conceptid, ["member"], child_valuetypes, offset=offset, limit=limit, order_hierarchically=True, query=query, columns=columns
- )
-
- def get_child_collections(self, conceptid, child_valuetypes=None, parent_valuetype="prefLabel", columns=None, depth_limit=None):
- child_valuetypes = child_valuetypes if child_valuetypes else ["prefLabel"]
- columns = columns if columns else "conceptidto::text, valueto, valueidto::text"
- return self.get_child_edges(conceptid, ["member"], child_valuetypes, parent_valuetype, columns, depth_limit)
-
- def get_child_concepts(self, conceptid, child_valuetypes=None, parent_valuetype="prefLabel", columns=None, depth_limit=None):
- columns = columns if columns else "conceptidto::text, valueto, valueidto::text"
- return self.get_child_edges(conceptid, ["narrower", "hasTopConcept"], child_valuetypes, parent_valuetype, columns, depth_limit)
-
- def get_child_concepts_for_indexing(self, conceptid, child_valuetypes=None, parent_valuetype="prefLabel", depth_limit=None):
- columns = "valueidto::text, conceptidto::text, valuetypeto, categoryto, valueto, languageto"
- data = self.get_child_edges(conceptid, ["narrower", "hasTopConcept"], child_valuetypes, parent_valuetype, columns, depth_limit)
- return [dict(list(zip(["id", "conceptid", "type", "category", "value", "language"], d)), top_concept="") for d in data]
-
- def get_child_edges(
- self,
- conceptid,
- relationtypes,
- child_valuetypes=None,
- parent_valuetype="prefLabel",
- columns=None,
- depth_limit=None,
- offset=None,
- limit=20,
- order_hierarchically=False,
- query=None,
- languageid=None,
- ):
- """
- Recursively builds a list of concept relations for a given concept and all it's subconcepts based on its relationship type and valuetypes.
-
- """
-
- # if the conceptid isn't a UUID then Postgres will throw an error and transactions will be aborted #7822
- try:
- uuid.UUID(conceptid)
- except:
- return []
-
- # this interpolation is safe because `relationtypes` is hardcoded in all calls, and not accessible via the API
- relationtypes = " or ".join(["r.relationtype = '%s'" % (relationtype) for relationtype in relationtypes])
- offset_clause = " limit %(limit)s offset %(offset)s" if offset is not None else ""
- depth_clause = " and depth < %(depth_limit)s" if depth_limit is not None else ""
-
- cursor = connection.cursor()
-
- if order_hierarchically:
- sql = """
- WITH RECURSIVE
-
- ordered_relationships AS (
- (
- SELECT r.conceptidfrom, r.conceptidto, r.relationtype, (
- SELECT value
- FROM values
- WHERE conceptid=r.conceptidto
- AND valuetype in ('prefLabel')
- ORDER BY (
- CASE WHEN languageid = %(languageid)s THEN 10
- WHEN languageid like %(short_languageid)s THEN 5
- WHEN languageid like %(default_languageid)s THEN 2
- ELSE 0
- END
- ) desc limit 1
- ) as valuesto,
- (
- SELECT value::int
- FROM values
- WHERE conceptid=r.conceptidto
- AND valuetype in ('sortorder')
- limit 1
- ) as sortorder,
- (
- SELECT value
- FROM values
- WHERE conceptid=r.conceptidto
- AND valuetype in ('collector')
- limit 1
- ) as collector
- FROM relations r
- WHERE r.conceptidfrom = %(conceptid)s
- and (%(relationtypes)s)
- ORDER BY sortorder, valuesto
- )
- UNION
- (
- SELECT r.conceptidfrom, r.conceptidto, r.relationtype,(
- SELECT value
- FROM values
- WHERE conceptid=r.conceptidto
- AND valuetype in ('prefLabel')
- ORDER BY (
- CASE WHEN languageid = %(languageid)s THEN 10
- WHEN languageid like %(short_languageid)s THEN 5
- WHEN languageid like %(default_languageid)s THEN 2
- ELSE 0
- END
- ) desc limit 1
- ) as valuesto,
- (
- SELECT value::int
- FROM values
- WHERE conceptid=r.conceptidto
- AND valuetype in ('sortorder')
- limit 1
- ) as sortorder,
- (
- SELECT value
- FROM values
- WHERE conceptid=r.conceptidto
- AND valuetype in ('collector')
- limit 1
- ) as collector
- FROM relations r
- JOIN ordered_relationships b ON(b.conceptidto = r.conceptidfrom)
- WHERE (%(relationtypes)s)
- ORDER BY sortorder, valuesto
- )
- ),
-
- children AS (
- SELECT r.conceptidfrom, r.conceptidto,
- to_char(row_number() OVER (), 'fm000000') as row,
- r.collector,
- 1 AS depth ---|NonRecursive Part
- FROM ordered_relationships r
- WHERE r.conceptidfrom = %(conceptid)s
- and (%(relationtypes)s)
- UNION
- SELECT r.conceptidfrom, r.conceptidto,
- row || '-' || to_char(row_number() OVER (), 'fm000000'),
- r.collector,
- depth+1 ---|RecursivePart
- FROM ordered_relationships r
- JOIN children b ON(b.conceptidto = r.conceptidfrom)
- WHERE (%(relationtypes)s)
- {depth_clause}
- )
-
- {subquery}
-
- SELECT
- (
- select row_to_json(d)
- FROM (
- SELECT *
- FROM values
- WHERE conceptid=%(recursive_table)s.conceptidto
- AND valuetype in ('prefLabel')
- ORDER BY (
- CASE WHEN languageid = %(languageid)s THEN 10
- WHEN languageid like %(short_languageid)s THEN 5
- WHEN languageid like %(default_languageid)s THEN 2
- ELSE 0
- END
- ) desc limit 1
- ) d
- ) as valueto,
- depth, collector, count(*) OVER() AS full_count
-
- FROM %(recursive_table)s order by row {offset_clause};
- """
-
- if query:
- subquery = """
- , results as (
- SELECT c.conceptidfrom, c.conceptidto, c.row, c.depth, c.collector
- FROM children c
- JOIN values ON(values.conceptid = c.conceptidto)
- WHERE LOWER(values.value) like %(query)s
- AND values.valuetype in ('prefLabel')
- AND LOWER(values.value) != %(match)s
- UNION
- SELECT c.conceptidfrom, c.conceptidto, '0' as row, c.depth, c.collector
- FROM children c
- JOIN values ON(values.conceptid = c.conceptidto)
- WHERE LOWER(values.value) = %(match)s
- AND values.valuetype in ('prefLabel')
- UNION
- SELECT c.conceptidfrom, c.conceptidto, c.row, c.depth, c.collector
- FROM children c
- JOIN results r on (r.conceptidfrom=c.conceptidto)
- )
- """
- else:
- subquery = ""
-
- sql = sql.format(subquery=subquery, offset_clause=offset_clause, depth_clause=depth_clause)
-
- recursive_table = "results" if query else "children"
- languageid = get_language() if languageid is None else languageid
-
- cursor.execute(
- sql,
- {
- "conceptid": conceptid,
- "relationtypes": AsIs(relationtypes),
- "depth_limit": depth_limit,
- "limit": limit,
- "offset": offset,
- "query": "%" + query.lower() + "%",
- "match": query.lower(),
- "recursive_table": AsIs(recursive_table),
- "languageid": languageid,
- "short_languageid": languageid.split("-")[0] + "%",
- "default_languageid": settings.LANGUAGE_CODE + "%",
- },
- )
- else:
- sql = """
- WITH RECURSIVE
- children AS (
- SELECT r.conceptidfrom, r.conceptidto, r.relationtype, 1 AS depth
- FROM relations r
- WHERE r.conceptidfrom = %(conceptid)s
- AND (%(relationtypes)s)
- UNION
- SELECT r.conceptidfrom, r.conceptidto, r.relationtype, depth+1
- FROM relations r
- JOIN children c ON(c.conceptidto = r.conceptidfrom)
- WHERE (%(relationtypes)s)
- {depth_clause}
- ),
- results AS (
- SELECT
- valuefrom.value as valuefrom, valueto.value as valueto,
- valuefrom.valueid as valueidfrom, valueto.valueid as valueidto,
- valuefrom.valuetype as valuetypefrom, valueto.valuetype as valuetypeto,
- valuefrom.languageid as languagefrom, valueto.languageid as languageto,
- dtypesfrom.category as categoryfrom, dtypesto.category as categoryto,
- c.conceptidfrom, c.conceptidto
- FROM values valueto
- JOIN d_value_types dtypesto ON(dtypesto.valuetype = valueto.valuetype)
- JOIN children c ON(c.conceptidto = valueto.conceptid)
- JOIN values valuefrom ON(c.conceptidfrom = valuefrom.conceptid)
- JOIN d_value_types dtypesfrom ON(dtypesfrom.valuetype = valuefrom.valuetype)
- WHERE valueto.valuetype in (%(child_valuetypes)s)
- AND valuefrom.valuetype in (%(child_valuetypes)s)
- )
- SELECT distinct %(columns)s
- FROM results {offset_clause}
- """
-
- sql = sql.format(offset_clause=offset_clause, depth_clause=depth_clause)
-
- if not columns:
- columns = """
- conceptidfrom::text, conceptidto::text,
- valuefrom, valueto,
- valueidfrom::text, valueidto::text,
- valuetypefrom, valuetypeto,
- languagefrom, languageto,
- categoryfrom, categoryto
- """
-
- cursor.execute(
- sql,
- {
- "conceptid": conceptid,
- "relationtypes": AsIs(relationtypes),
- "child_valuetypes": ("','").join(
- child_valuetypes
- if child_valuetypes
- else models.DValueType.objects.filter(category="label").values_list("valuetype", flat=True)
- ),
- "columns": AsIs(columns),
- "depth_limit": depth_limit,
- "limit": limit,
- "offset": offset,
- },
- )
-
- return cursor.fetchall()
-
- def traverse(self, func, direction="down", scope=None, **kwargs):
- """
- Traverses a concept graph from self to leaf (direction='down') or root (direction='up') calling
- the given function on each node, passes an optional scope to each function
-
- Return a value from the function to prematurely end the traversal
-
- """
-
- _cache = kwargs.pop("_cache", [])
- if self.id not in _cache:
- _cache.append(self.id)
-
- if scope is None:
- ret = func(self, **kwargs)
- else:
- ret = func(self, scope, **kwargs)
-
- # break out of the traversal if the function returns a value
- if ret is not None:
- return ret
-
- if direction == "down":
- for subconcept in self.subconcepts:
- ret = subconcept.traverse(func, direction, scope, _cache=_cache, **kwargs)
- if ret is not None:
- return ret
- else:
- for parentconcept in self.parentconcepts:
- ret = parentconcept.traverse(func, direction, scope, _cache=_cache, **kwargs)
- if ret is not None:
- return ret
-
- def get_sortkey(self, lang=settings.LANGUAGE_CODE):
- for value in self.values:
- if value.type == "sortorder":
- try:
- return float(value.value)
- except:
- return None
-
- return self.get_preflabel(lang=lang).value
-
- def natural_keys(self, text):
- """
- alist.sort(key=natural_keys) sorts in human order
- http://nedbatchelder.com/blog/200712/human_sorting.html
- (See Toothy's implementation in the comments)
- float regex comes from https://stackoverflow.com/a/12643073/190597
- """
-
- def atof(text):
- try:
- retval = float(text)
- except ValueError:
- retval = text
- return retval
-
- return [atof(c) for c in re.split(r"[+-]?([0-9]+(?:[.][0-9]*)?|[.][0-9]+)", str(text))]
-
- def get_preflabel(self, lang=settings.LANGUAGE_CODE):
- ranked_labels = []
- if self.values == []:
- concept = Concept().get(id=self.id, include_subconcepts=False, include_parentconcepts=False, include=["label"])
- else:
- concept = self
-
- ranked_labels = sorted(
- concept.values,
- key=lambda label: rank_label(kind=label.type, source_lang=label.language, target_lang=lang),
- reverse=True,
- )
- if len(ranked_labels) == 0:
- return ConceptValue()
-
- return ranked_labels[0]
-
- def flatten(self, ret=None):
- """
- Flattens the graph into a unordered list of concepts
-
- """
-
- if ret is None:
- ret = []
-
- ret.append(self)
- for subconcept in self.subconcepts:
- subconcept.flatten(ret)
-
- return ret
-
- def addparent(self, value):
- if isinstance(value, dict):
- self.parentconcepts.append(Concept(value))
- elif isinstance(value, Concept):
- self.parentconcepts.append(value)
- else:
- raise Exception("Invalid parent concept definition: %s" % (value))
-
- def addsubconcept(self, value):
- if isinstance(value, dict):
- self.subconcepts.append(Concept(value))
- elif isinstance(value, Concept):
- self.subconcepts.append(value)
- else:
- raise Exception(_("Invalid subconcept definition: %s") % (value))
-
- def addrelatedconcept(self, value):
- if isinstance(value, dict):
- self.relatedconcepts.append(Concept(value))
- elif isinstance(value, Concept):
- self.relatedconcepts.append(value)
- else:
- raise Exception(_("Invalid related concept definition: %s") % (value))
-
- def addvalue(self, value):
- if isinstance(value, dict):
- value["conceptid"] = self.id
- self.values.append(ConceptValue(value))
- elif isinstance(value, ConceptValue):
- self.values.append(value)
- elif isinstance(value, models.Value):
- self.values.append(ConceptValue(value))
- else:
- raise Exception(_("Invalid value definition: %s") % (value))
-
- def index(self, scheme=None):
- if scheme is None:
- scheme = self.get_context()
- for value in self.values:
- value.index(scheme=scheme)
-
- if self.nodetype == "ConceptScheme":
- scheme = None
-
- for subconcept in self.subconcepts:
- subconcept.index(scheme=scheme)
-
- def bulk_index(self):
- concept_docs = []
-
- if self.nodetype == "ConceptScheme":
- concept = Concept().get(id=self.id, values=["label"])
- concept.index()
- for topConcept in self.get_child_concepts_for_indexing(self.id, depth_limit=1):
- concept = Concept().get(id=topConcept["conceptid"])
- scheme = concept.get_context()
- topConcept["top_concept"] = scheme.id
- concept_docs.append(se.create_bulk_item(index=CONCEPTS_INDEX, id=topConcept["id"], data=topConcept))
- for childConcept in concept.get_child_concepts_for_indexing(topConcept["conceptid"]):
- childConcept["top_concept"] = scheme.id
- concept_docs.append(se.create_bulk_item(index=CONCEPTS_INDEX, id=childConcept["id"], data=childConcept))
-
- if self.nodetype == "Concept":
- concept = Concept().get(id=self.id, values=["label"])
- scheme = concept.get_context()
- concept.index(scheme)
- for childConcept in concept.get_child_concepts_for_indexing(self.id):
- childConcept["top_concept"] = scheme.id
- concept_docs.append(se.create_bulk_item(index=CONCEPTS_INDEX, id=childConcept["id"], data=childConcept))
-
- se.bulk_index(concept_docs)
-
- def delete_index(self, delete_self=False):
- def delete_concept_values_index(concepts_to_delete):
- for concept in concepts_to_delete.values():
- query = Query(se, start=0, limit=10000)
- term = Term(field="conceptid", term=concept.id)
- query.add_query(term)
- query.delete(index=CONCEPTS_INDEX)
-
- if delete_self:
- concepts_to_delete = Concept.gather_concepts_to_delete(self)
- delete_concept_values_index(concepts_to_delete)
- else:
- for subconcept in self.subconcepts:
- concepts_to_delete = Concept.gather_concepts_to_delete(subconcept)
- delete_concept_values_index(concepts_to_delete)
-
- def concept_tree(
- self,
- top_concept="00000000-0000-0000-0000-000000000001",
- lang=settings.LANGUAGE_CODE,
- mode="semantic",
- ):
- class concept(object):
- def __init__(self, *args, **kwargs):
- self.label = ""
- self.labelid = ""
- self.id = ""
- self.sortorder = None
- self.load_on_demand = False
- self.children = []
-
- def _findNarrowerConcept(conceptid, depth_limit=None, level=0):
- labels = models.Value.objects.filter(concept=conceptid)
- ret = concept()
- temp = Concept()
- for label in labels:
- temp.addvalue(label)
- if label.valuetype_id == "sortorder":
- try:
- ret.sortorder = float(label.value)
- except:
- ret.sortorder = None
-
- label = temp.get_preflabel(lang=lang)
- ret.label = label.value
- ret.id = label.conceptid
- ret.labelid = label.id
-
- if mode == "semantic":
- conceptrealations = models.Relation.objects.filter(
- Q(conceptfrom=conceptid), Q(relationtype__category="Semantic Relations") | Q(relationtype__category="Properties")
- )
- if mode == "collections":
- conceptrealations = models.Relation.objects.filter(
- Q(conceptfrom=conceptid), Q(relationtype="member") | Q(relationtype="hasCollection")
- )
- if depth_limit is not None and len(conceptrealations) > 0 and level >= depth_limit:
- ret.load_on_demand = True
- else:
- if depth_limit is not None:
- level = level + 1
- for relation in conceptrealations:
- ret.children.append(_findNarrowerConcept(relation.conceptto_id, depth_limit=depth_limit, level=level))
-
- ret.children = sorted(
- ret.children,
- key=lambda concept: self.natural_keys(concept.sortorder if concept.sortorder else concept.label),
- reverse=False,
- )
- return ret
-
- def _findBroaderConcept(conceptid, child_concept, depth_limit=None, level=0):
- conceptrealations = models.Relation.objects.filter(
- Q(conceptto=conceptid), ~Q(relationtype="related"), ~Q(relationtype__category="Mapping Properties")
- )
- if len(conceptrealations) > 0 and conceptid != top_concept:
- labels = models.Value.objects.filter(concept=conceptrealations[0].conceptfrom_id)
- ret = concept()
- temp = Concept()
- for label in labels:
- temp.addvalue(label)
- label = temp.get_preflabel(lang=lang)
- ret.label = label.value
- ret.id = label.conceptid
- ret.labelid = label.id
-
- ret.children.append(child_concept)
- return _findBroaderConcept(conceptrealations[0].conceptfrom_id, ret, depth_limit=depth_limit, level=level)
- else:
- return child_concept
-
- graph = []
- if self.id is None or self.id == "" or self.id == "None" or self.id == top_concept:
- if mode == "semantic":
- concepts = models.Concept.objects.filter(nodetype="ConceptScheme")
- for conceptmodel in concepts:
- graph.append(_findNarrowerConcept(conceptmodel.pk, depth_limit=1))
- if mode == "collections":
- concepts = models.Concept.objects.filter(nodetype="Collection")
- for conceptmodel in concepts:
- graph.append(_findNarrowerConcept(conceptmodel.pk, depth_limit=0))
-
- graph = sorted(graph, key=lambda concept: concept.label)
- # graph = _findNarrowerConcept(concepts[0].pk, depth_limit=1).children
-
- else:
- graph = _findNarrowerConcept(self.id, depth_limit=1).children
- # concepts = _findNarrowerConcept(self.id, depth_limit=1)
- # graph = [_findBroaderConcept(self.id, concepts, depth_limit=1)]
-
- return graph
-
- def get_paths(self, lang=settings.LANGUAGE_CODE):
- def graph_to_paths(current_concept, path=[], path_list=[], _cache=[]):
- if len(path) == 0:
- current_path = []
- else:
- current_path = path[:]
-
- current_path.insert(
- 0,
- {
- "label": current_concept.get_preflabel(lang=lang).value,
- "relationshiptype": current_concept.relationshiptype,
- "id": current_concept.id,
- },
- )
-
- if len(current_concept.parentconcepts) == 0 or current_concept.id in _cache:
- path_list.append(current_path[:])
- else:
- _cache.append(current_concept.id)
- for parent in current_concept.parentconcepts:
- ret = graph_to_paths(parent, current_path, path_list, _cache)
-
- return path_list
-
- # def graph_to_paths(current_concept, **kwargs):
- # path = kwargs.get('path', [])
- # path_list = kwargs.get('path_list', [])
-
- # if len(path) == 0:
- # current_path = []
- # else:
- # current_path = path[:]
-
- # current_path.insert(0, {'label': current_concept.get_preflabel(lang=lang).value, 'relationshiptype': current_concept.relationshiptype, 'id': current_concept.id})
-
- # if len(current_concept.parentconcepts) == 0:
- # path_list.append(current_path[:])
- # # else:
- # # for parent in current_concept.parentconcepts:
- # # ret = graph_to_paths(parent, current_path, path_list, _cache)
-
- # #return path_list
-
- # self.traverse(graph_to_paths, direction='up')
-
- return graph_to_paths(self)
-
- def get_node_and_links(self, lang=settings.LANGUAGE_CODE):
- nodes = [{"concept_id": self.id, "name": self.get_preflabel(lang=lang).value, "type": "Current"}]
- links = []
-
- def get_parent_nodes_and_links(current_concept, _cache=[]):
- if current_concept.id not in _cache:
- _cache.append(current_concept.id)
- parents = current_concept.parentconcepts
- for parent in parents:
- nodes.append(
- {
- "concept_id": parent.id,
- "name": parent.get_preflabel(lang=lang).value,
- "type": "Root" if len(parent.parentconcepts) == 0 else "Ancestor",
- }
- )
- links.append(
- {
- "target": current_concept.id,
- "source": parent.id,
- "relationship": "broader",
- }
- )
- get_parent_nodes_and_links(parent, _cache)
-
- get_parent_nodes_and_links(self)
-
- # def get_parent_nodes_and_links(current_concept):
- # parents = current_concept.parentconcepts
- # for parent in parents:
- # nodes.append({'concept_id': parent.id, 'name': parent.get_preflabel(lang=lang).value, 'type': 'Root' if len(parent.parentconcepts) == 0 else 'Ancestor'})
- # links.append({'target': current_concept.id, 'source': parent.id, 'relationship': 'broader' })
-
- # self.traverse(get_parent_nodes_and_links, direction='up')
-
- for child in self.subconcepts:
- nodes.append(
- {
- "concept_id": child.id,
- "name": child.get_preflabel(lang=lang).value,
- "type": "Descendant",
- }
- )
- links.append({"source": self.id, "target": child.id, "relationship": "narrower"})
-
- for related in self.relatedconcepts:
- nodes.append(
- {
- "concept_id": related.id,
- "name": related.get_preflabel(lang=lang).value,
- "type": "Related",
- }
- )
- links.append({"source": self.id, "target": related.id, "relationship": "related"})
-
- # get unique node list and assign unique integer ids for each node (required by d3)
- nodes = list({node["concept_id"]: node for node in nodes}.values())
- for i in range(len(nodes)):
- nodes[i]["id"] = i
- for link in links:
- link["source"] = i if link["source"] == nodes[i]["concept_id"] else link["source"]
- link["target"] = i if link["target"] == nodes[i]["concept_id"] else link["target"]
-
- return {"nodes": nodes, "links": links}
-
- def get_context(self):
- """
- get the Top Concept that the Concept particpates in
-
- """
-
- if self.nodetype == "Concept" or self.nodetype == "Collection":
- concept = Concept().get(id=self.id, include_parentconcepts=True, include=None)
-
- def get_scheme_id(concept):
- for parentconcept in concept.parentconcepts:
- if parentconcept.relationshiptype == "hasTopConcept":
- return concept
-
- if len(concept.parentconcepts) > 0:
- return concept.traverse(get_scheme_id, direction="up")
- else:
- return self
-
- else: # like ConceptScheme or EntityType
- return self
-
- def get_scheme(self):
- """
- get the ConceptScheme that the Concept particpates in
-
- """
-
- topConcept = self.get_context()
- if len(topConcept.parentconcepts) == 1:
- if topConcept.parentconcepts[0].nodetype == "ConceptScheme":
- return topConcept.parentconcepts[0]
-
- return None
-
- def check_if_concept_in_use(self):
- """Checks if a concept or any of its subconcepts is in use by a resource instance"""
-
- in_use = False
- cursor = connection.cursor()
- for value in self.values:
- sql = (
- """
- SELECT count(*) from tiles t, jsonb_each_text(t.tiledata) as json_data
- WHERE json_data.value = '%s'
- """
- % value.id
- )
- cursor.execute(sql)
- rows = cursor.fetchall()
- if rows[0][0] > 0:
- in_use = True
- break
- if in_use is not True:
- for subconcept in self.subconcepts:
- in_use = subconcept.check_if_concept_in_use()
- if in_use == True:
- return in_use
- return in_use
-
- def get_e55_domain(self, conceptid):
- """
- For a given entitytypeid creates a dictionary representing that entitytypeid's concept graph (member pathway) formatted to support
- select2 dropdowns
-
- """
- cursor = connection.cursor()
- cursor.execute(
- """
- WITH RECURSIVE children AS (
- SELECT d.conceptidfrom, d.conceptidto, c2.value, c2.valueid as valueid, c.value as valueto, c.valueid as valueidto, c.valuetype as vtype, c.languageid, 1 AS depth, array[d.conceptidto] AS conceptpath, array[c.valueid] AS idpath ---|NonRecursive Part
- FROM relations d
- JOIN values c ON(c.conceptid = d.conceptidto)
- JOIN values c2 ON(c2.conceptid = d.conceptidfrom)
- WHERE d.conceptidfrom = %s
- and c2.valuetype IN ('prefLabel', 'altLabel')
- and c.valuetype IN ('prefLabel', 'altLabel', 'sortorder', 'collector')
- and (d.relationtype = 'member' or d.relationtype = 'hasTopConcept')
- UNION
- SELECT d.conceptidfrom, d.conceptidto, v2.value, v2.valueid as valueid, v.value as valueto, v.valueid as valueidto, v.valuetype as vtype, v.languageid, depth+1, (conceptpath || d.conceptidto), (idpath || v.valueid) ---|RecursivePart
- FROM relations d
- JOIN children b ON(b.conceptidto = d.conceptidfrom)
- JOIN values v ON(v.conceptid = d.conceptidto)
- JOIN values v2 ON(v2.conceptid = d.conceptidfrom)
- WHERE v2.valuetype IN ('prefLabel', 'altLabel')
- and v.valuetype IN ('prefLabel', 'altLabel', 'sortorder', 'collector')
- and (d.relationtype = 'member' or d.relationtype = 'hasTopConcept')
- ) SELECT conceptidfrom::text, conceptidto::text, value, valueid::text, valueto, valueidto::text, languageid, depth, idpath::text, conceptpath::text, vtype FROM children ORDER BY depth, conceptpath;
- """,
- [conceptid],
- )
- rows = cursor.fetchall()
-
- column_names = [
- "conceptidfrom",
- "conceptidto",
- "value",
- "valueid",
- "valueto",
- "valueidto",
- "languageid",
- "depth",
- "idpath",
- "conceptpath",
- "vtype",
- ]
-
- class Val(object):
- def __init__(self, conceptid):
- self.text = ""
- self.conceptid = conceptid
- self.id = ""
- self.sortorder = ""
- self.collector = ""
- self.children = []
-
- result = Val(conceptid)
-
- def _findNarrower(val, path, rec):
- for conceptid in path:
- childids = [child.conceptid for child in val.children]
- if conceptid not in childids:
- new_val = Val(rec["conceptidto"])
- if rec["vtype"] == "sortorder":
- new_val.sortorder = rec["valueto"]
- elif rec["vtype"] in ("prefLabel", "altLabel"):
- new_val.text = rec["valueto"]
- new_val.id = rec["valueidto"]
- elif rec["vtype"] == "collector":
- new_val.collector = "collector"
- val.children.append(new_val)
- else:
- for child in val.children:
- if conceptid == child.conceptid:
- if conceptid == path[-1]:
- if rec["vtype"] == "sortorder":
- child.sortorder = rec["valueto"]
- elif rec["vtype"] in ("prefLabel", "altLabel"):
- child.text = rec["valueto"]
- child.id = rec["valueidto"]
- elif rec["vtype"] == "collector":
- child.collector = "collector"
- path.pop(0)
- _findNarrower(child, path, rec)
- val.children.sort(key=lambda x: (x.sortorder, x.text))
-
- def best_language_last(rec):
- """_findNarrower updates via recursive search, so sort the best language last."""
- label_rank = rank_label(kind=rec["vtype"], source_lang=rec["languageid"])
- return (rec["depth"], rec["conceptpath"], label_rank)
-
- records = [dict(list(zip(column_names, row))) for row in rows]
- for rec in sorted(records, key=best_language_last):
- path = rec["conceptpath"][1:-1].split(",")
- _findNarrower(result, path, rec)
-
- return JSONSerializer().serializeToPython(result)["children"]
-
- def make_collection(self):
- if len(self.values) == 0:
- raise Exception(_("Need to include values when creating a collection"))
- values = JSONSerializer().serializeToPython(self.values)
- for value in values:
- value["id"] = ""
- collection_concept = Concept({"nodetype": "Collection", "values": values})
-
- def create_collection(conceptfrom):
- for relation in models.Relation.objects.filter(
- Q(conceptfrom_id=conceptfrom.id),
- Q(relationtype__category="Semantic Relations") | Q(relationtype__category="Properties"),
- ~Q(relationtype="related"),
- ):
- conceptto = Concept(relation.conceptto)
- if conceptfrom == self:
- collection_concept.add_relation(conceptto, "member")
- else:
- conceptfrom.add_relation(conceptto, "member")
- create_collection(conceptto)
-
- with transaction.atomic():
- collection_concept.save()
- create_collection(self)
-
- return collection_concept
-
-
-class ConceptValue(object):
- def __init__(self, *args, **kwargs):
- self.id = ""
- self.conceptid = ""
- self.type = ""
- self.category = ""
- self.value = ""
- self.language = ""
-
- if len(args) != 0:
- if isinstance(args[0], str):
- try:
- uuid.UUID(args[0])
- self.get(args[0])
- except ValueError:
- self.load(JSONDeserializer().deserialize(args[0]))
- elif isinstance(args[0], object):
- self.load(args[0])
-
- def __repr__(self):
- return ('%s: %s = "%s" in lang %s') % (self.__class__, self.type, self.value, self.language)
-
- def get(self, id=""):
- self.load(models.Value.objects.get(pk=id))
- return self
-
- def save(self):
- if self.value.strip() != "":
- self.id = self.id if (self.id != "" and self.id is not None) else str(uuid.uuid4())
- value = models.Value()
- value.pk = self.id
- value.value = self.value
- value.concept_id = self.conceptid # models.Concept.objects.get(pk=self.conceptid)
- value.valuetype_id = self.type # models.DValueType.objects.get(pk=self.type)
-
- if self.language != "":
- # need to normalize language ids to the form xx-XX
- lang_parts = self.language.lower().replace("_", "-").split("-")
- try:
- lang_parts[1] = lang_parts[1].upper()
- except:
- pass
- self.language = "-".join(lang_parts)
- value.language_id = self.language # models.DLanguage.objects.get(pk=self.language)
- else:
- value.language_id = settings.LANGUAGE_CODE
-
- value.save()
- self.category = value.valuetype.category
-
- def delete(self):
- if self.id != "":
- newvalue = models.Value.objects.get(pk=self.id)
- if newvalue.valuetype.valuetype == "image":
- newvalue = models.FileValue.objects.get(pk=self.id)
- newvalue.delete()
- self = ConceptValue()
- return self
-
- def load(self, value):
- if isinstance(value, models.Value):
- self.id = str(value.pk)
- self.conceptid = str(value.concept_id)
- self.type = value.valuetype_id
- self.category = value.valuetype.category
- self.value = value.value
- self.language = value.language_id
-
- if isinstance(value, dict):
- self.id = str(value["id"]) if "id" in value else ""
- self.conceptid = str(value["conceptid"]) if "conceptid" in value else ""
- self.type = value["type"] if "type" in value else ""
- self.category = value["category"] if "category" in value else ""
- self.value = value["value"] if "value" in value else ""
- self.language = value["language"] if "language" in value else ""
-
- def index(self, scheme=None):
- if self.category == "label":
- data = JSONSerializer().serializeToPython(self)
- if scheme is None:
- scheme = self.get_scheme_id()
- if scheme is None:
- raise Exception(_("Index of label failed. Index type (scheme id) could not be derived from the label."))
-
- data["top_concept"] = scheme.id
- se.index_data(index=CONCEPTS_INDEX, body=data, idfield="id")
-
- def delete_index(self):
- query = Query(se, start=0, limit=10000)
- term = Term(field="id", term=self.id)
- query.add_query(term)
- query.delete(index=CONCEPTS_INDEX)
-
- def get_scheme_id(self):
- result = se.search(index=CONCEPTS_INDEX, id=self.id)
- if result["found"]:
- return Concept(result["top_concept"])
- else:
- return None
-
-
-def get_preflabel_from_conceptid(conceptid, lang):
- default = {
- "category": "",
- "conceptid": "",
- "language": "",
- "value": "",
- "type": "",
- "id": "",
- }
- query = Query(se)
- bool_query = Bool()
- bool_query.must(Match(field="type", query="prefLabel", type="phrase"))
- bool_query.filter(Terms(field="conceptid", terms=[conceptid]))
- query.add_query(bool_query)
- preflabels = query.search(index=CONCEPTS_INDEX)["hits"]["hits"]
-
- ranked = sorted(
- preflabels,
- key=lambda prefLabel: rank_label(kind=prefLabel["_source"]["type"],
- source_lang=prefLabel["_source"]["language"],
- target_lang=lang,
- ),
- reverse=True,
- )
-
- if not ranked:
- return default
- return ranked[0]["_source"]
-
-
-def get_valueids_from_concept_label(label, conceptid=None, lang=None):
- def exact_val_match(val, conceptid=None):
- # exact term match, don't care about relevance ordering.
- # due to language formating issues, and with (hopefully) small result sets
- # easier to have filter logic in python than to craft it in dsl
- if conceptid is None:
- return {"query": {"bool": {"filter": {"match_phrase": {"value": val}}}}}
- else:
- return {
- "query": {
- "bool": {
- "filter": [
- {"match_phrase": {"value": val}},
- {"term": {"conceptid": conceptid}},
- ]
- }
- }
- }
-
- concept_label_results = se.search(index=CONCEPTS_INDEX, **exact_val_match(label, conceptid))
- if concept_label_results is None:
- print("Found no matches for label:'{0}' and concept_id: '{1}'".format(label, conceptid))
- return
- return [
- res["_source"]
- for res in concept_label_results["hits"]["hits"]
- if lang is None or res["_source"]["language"].lower() == lang.lower()
- ]
-
-
-def get_preflabel_from_valueid(valueid, lang):
- concept_label = se.search(index=CONCEPTS_INDEX, id=valueid)
- if concept_label["found"]:
- return get_preflabel_from_conceptid(concept_label["_source"]["conceptid"], lang)
+"""
+ARCHES - a program developed to inventory and manage immovable cultural heritage.
+Copyright (C) 2013 J. Paul Getty Trust and World Monuments Fund
+
+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 .
+"""
+
+import re
+import uuid
+import copy
+from operator import itemgetter
+from operator import methodcaller
+from django.db import transaction, connection
+from django.db.models import Q
+from arches.app.models import models
+from arches.app.models.system_settings import settings
+from arches.app.search.search_engine_factory import SearchEngineInstance as se
+from arches.app.search.elasticsearch_dsl_builder import Term, Query, Bool, Match, Terms
+from arches.app.search.mappings import CONCEPTS_INDEX
+from arches.app.utils.betterJSONSerializer import JSONSerializer, JSONDeserializer
+from arches.app.utils.i18n import rank_label
+from django.utils.translation import get_language, gettext as _
+from django.db import IntegrityError
+from psycopg2.extensions import AsIs
+
+import logging
+
+
+logger = logging.getLogger(__name__)
+
+CORE_CONCEPTS = (
+ "00000000-0000-0000-0000-000000000001",
+ "00000000-0000-0000-0000-000000000004",
+ "00000000-0000-0000-0000-000000000005",
+ "00000000-0000-0000-0000-000000000006",
+)
+
+
+class Concept(object):
+ def __init__(self, *args, **kwargs):
+ self.id = ""
+ self.nodetype = ""
+ self.legacyoid = ""
+ self.relationshiptype = ""
+ self.values = []
+ self.subconcepts = []
+ self.parentconcepts = []
+ self.relatedconcepts = []
+ self.hassubconcepts = False
+
+ if len(args) != 0:
+ if isinstance(args[0], str):
+ try:
+ uuid.UUID(args[0])
+ self.get(args[0])
+ except ValueError:
+ self.load(JSONDeserializer().deserialize(args[0]))
+ elif isinstance(args[0], dict):
+ self.load(args[0])
+ elif isinstance(args[0], object):
+ self.load(args[0])
+
+ def __unicode__(self):
+ return ("%s - %s") % (self.get_preflabel().value, self.id)
+
+ def __hash__(self):
+ return hash(self.id)
+
+ def __eq__(self, x):
+ return hash(self) == hash(x)
+
+ def __ne__(self, x):
+ return hash(self) != hash(x)
+
+ def load(self, value):
+ if isinstance(value, dict):
+ self.id = str(value["id"]) if "id" in value else ""
+ self.nodetype = value["nodetype"] if "nodetype" in value else ""
+ self.legacyoid = value["legacyoid"] if "legacyoid" in value else ""
+ self.relationshiptype = value["relationshiptype"] if "relationshiptype" in value else ""
+ if "values" in value:
+ for val in value["values"]:
+ self.addvalue(val)
+ if "subconcepts" in value:
+ for subconcept in value["subconcepts"]:
+ self.addsubconcept(subconcept)
+ if "parentconcepts" in value:
+ for parentconcept in value["parentconcepts"]:
+ self.addparent(parentconcept)
+ if "relatedconcepts" in value:
+ for relatedconcept in value["relatedconcepts"]:
+ self.addrelatedconcept(relatedconcept)
+
+ if isinstance(value, models.Concept):
+ self.id = str(value.pk)
+ self.nodetype = value.nodetype_id
+ self.legacyoid = value.legacyoid
+
+ def get(
+ self,
+ id="",
+ legacyoid="",
+ include_subconcepts=False,
+ include_parentconcepts=False,
+ include_relatedconcepts=False,
+ exclude=[],
+ include=[],
+ depth_limit=None,
+ up_depth_limit=None,
+ lang=settings.LANGUAGE_CODE,
+ semantic=True,
+ pathway_filter=None,
+ **kwargs,
+ ):
+ if id != "":
+ self.load(models.Concept.objects.get(pk=id))
+ elif legacyoid != "":
+ self.load(models.Concept.objects.get(legacyoid=legacyoid))
+
+ _cache = kwargs.pop("_cache", {})
+ _cache[self.id] = self.__class__(
+ {"id": self.id, "nodetype": self.nodetype, "legacyoid": self.legacyoid, "relationshiptype": self.relationshiptype}
+ )
+
+ if semantic == True:
+ pathway_filter = (
+ pathway_filter
+ if pathway_filter
+ else Q(relationtype__category="Semantic Relations") | Q(relationtype__category="Properties")
+ )
+ else:
+ pathway_filter = pathway_filter if pathway_filter else Q(relationtype="member") | Q(relationtype="hasCollection")
+
+ if self.id != "":
+ nodetype = kwargs.pop("nodetype", self.nodetype)
+ uplevel = kwargs.pop("uplevel", 0)
+ downlevel = kwargs.pop("downlevel", 0)
+ depth_limit = depth_limit if depth_limit is None else int(depth_limit)
+ up_depth_limit = up_depth_limit if up_depth_limit is None else int(up_depth_limit)
+
+ if include is not None:
+ if len(include) > 0 and len(exclude) > 0:
+ raise Exception(_("Only include values for include or exclude, but not both"))
+ include = (
+ include if len(include) != 0 else models.DValueType.objects.distinct("category").values_list("category", flat=True)
+ )
+ include = set(include).difference(exclude)
+ exclude = []
+
+ if len(include) > 0:
+ values = models.Value.objects.filter(concept=self.id)
+ for value in values:
+ if value.valuetype.category in include:
+ self.values.append(ConceptValue(value))
+
+ hassubconcepts = models.Relation.objects.filter(Q(conceptfrom=self.id), pathway_filter, ~Q(relationtype="related"))[0:1]
+ if len(hassubconcepts) > 0:
+ self.hassubconcepts = True
+
+ if include_subconcepts:
+ conceptrealations = models.Relation.objects.filter(Q(conceptfrom=self.id), pathway_filter, ~Q(relationtype="related"))
+ if depth_limit is None or downlevel < depth_limit:
+ if depth_limit is not None:
+ downlevel = downlevel + 1
+ for relation in conceptrealations:
+ subconcept = (
+ _cache[str(relation.conceptto_id)]
+ if str(relation.conceptto_id) in _cache
+ else self.__class__().get(
+ id=relation.conceptto_id,
+ include_subconcepts=include_subconcepts,
+ include_parentconcepts=include_parentconcepts,
+ include_relatedconcepts=include_relatedconcepts,
+ exclude=exclude,
+ include=include,
+ depth_limit=depth_limit,
+ up_depth_limit=up_depth_limit,
+ downlevel=downlevel,
+ uplevel=uplevel,
+ nodetype=nodetype,
+ semantic=semantic,
+ pathway_filter=pathway_filter,
+ _cache=_cache.copy(),
+ lang=lang,
+ )
+ )
+ subconcept.relationshiptype = relation.relationtype_id
+ self.subconcepts.append(subconcept)
+
+ self.subconcepts = sorted(
+ self.subconcepts, key=lambda concept: self.natural_keys(concept.get_sortkey(lang)), reverse=False
+ )
+ # self.subconcepts = sorted(self.subconcepts, key=methodcaller(
+ # 'get_sortkey', lang=lang), reverse=False)
+
+ if include_parentconcepts:
+ conceptrealations = models.Relation.objects.filter(Q(conceptto=self.id), pathway_filter, ~Q(relationtype="related"))
+ if up_depth_limit is None or uplevel < up_depth_limit:
+ if up_depth_limit is not None:
+ uplevel = uplevel + 1
+ for relation in conceptrealations:
+ parentconcept = (
+ _cache[str(relation.conceptfrom_id)]
+ if str(relation.conceptfrom_id) in _cache
+ else self.__class__().get(
+ id=relation.conceptfrom_id,
+ include_subconcepts=False,
+ include_parentconcepts=include_parentconcepts,
+ include_relatedconcepts=include_relatedconcepts,
+ exclude=exclude,
+ include=include,
+ depth_limit=depth_limit,
+ up_depth_limit=up_depth_limit,
+ downlevel=downlevel,
+ uplevel=uplevel,
+ nodetype=nodetype,
+ semantic=semantic,
+ pathway_filter=pathway_filter,
+ _cache=_cache.copy(),
+ lang=lang,
+ )
+ )
+ parentconcept.relationshiptype = relation.relationtype_id
+
+ self.parentconcepts.append(parentconcept)
+
+ if include_relatedconcepts:
+ conceptrealations = models.Relation.objects.filter(
+ Q(relationtype="related") | Q(relationtype__category="Mapping Properties"),
+ Q(conceptto=self.id) | Q(conceptfrom=self.id),
+ )
+ relations = []
+ for relation in conceptrealations:
+ if str(relation.conceptto_id) != self.id and str(relation.relationid) not in relations:
+ relations.append(str(relation.relationid))
+ relatedconcept = self.__class__().get(relation.conceptto_id, include=["label"], lang=lang)
+ relatedconcept.relationshiptype = relation.relationtype_id
+
+ self.relatedconcepts.append(relatedconcept)
+ if str(relation.conceptfrom_id) != self.id and str(relation.relationid) not in relations:
+ relations.append(str(relation.relationid))
+ relatedconcept = self.__class__().get(relation.conceptfrom_id, include=["label"], lang=lang)
+ relatedconcept.relationshiptype = relation.relationtype_id
+
+ self.relatedconcepts.append(relatedconcept)
+
+ return self
+
+ def save(self):
+ self.id = self.id if (self.id != "" and self.id is not None) else str(uuid.uuid4())
+ concept, created = models.Concept.objects.get_or_create(
+ pk=self.id, defaults={"legacyoid": self.legacyoid if self.legacyoid != "" else self.id, "nodetype_id": self.nodetype}
+ )
+
+ for value in self.values:
+ if not isinstance(value, ConceptValue):
+ value = ConceptValue(value)
+ value.conceptid = self.id
+ value.save()
+
+ for parentconcept in self.parentconcepts:
+ parentconcept.save()
+ parentconcept.add_relation(self, parentconcept.relationshiptype)
+
+ for subconcept in self.subconcepts:
+ subconcept.save()
+ self.add_relation(subconcept, subconcept.relationshiptype)
+
+ # if we're moving a Concept Scheme below another Concept or Concept Scheme
+ if len(self.parentconcepts) > 0 and concept.nodetype_id == "ConceptScheme":
+ concept.nodetype_id = "Concept"
+ concept.save()
+ self.load(concept)
+
+ for relation in models.Relation.objects.filter(conceptfrom=concept, relationtype_id="hasTopConcept"):
+ relation.relationtype_id = "narrower"
+ relation.save()
+
+ for relatedconcept in self.relatedconcepts:
+ self.add_relation(relatedconcept, relatedconcept.relationshiptype)
+
+ if relatedconcept.relationshiptype == "member":
+ child_concepts = relatedconcept.get(include_subconcepts=True)
+
+ def applyRelationship(concept):
+ for subconcept in concept.subconcepts:
+ concept.add_relation(subconcept, relatedconcept.relationshiptype)
+
+ child_concepts.traverse(applyRelationship)
+
+ return concept
+
+ def delete(self, delete_self=False):
+ """
+ Deletes any subconcepts associated with this concept and additionally this concept if 'delete_self' is True
+ If any parentconcepts or relatedconcepts are included then it will only delete the relationship to those concepts but not the concepts themselves
+ If any values are passed, then those values as well as the relationship to those values will be deleted
+
+ Note, django will automatically take care of deleting any db models that have a foreign key relationship to the model being deleted
+ (eg: deleting a concept model will also delete all values and relationships), but because we need to manage deleting
+ parent concepts and related concepts and values we have to do that here too
+
+ """
+
+ for subconcept in self.subconcepts:
+ concepts_to_delete = Concept.gather_concepts_to_delete(subconcept)
+ for key, concept in concepts_to_delete.items():
+ models.Concept.objects.get(pk=key).delete()
+
+ for parentconcept in self.parentconcepts:
+ relations_filter = (
+ (Q(relationtype__category="Semantic Relations") | Q(relationtype="hasTopConcept"))
+ & Q(conceptfrom=parentconcept.id)
+ & Q(conceptto=self.id)
+ )
+ conceptrelations = models.Relation.objects.filter(relations_filter)
+ for relation in conceptrelations:
+ relation.delete()
+
+ if not models.Relation.objects.filter(relations_filter).exists():
+ # we've removed all parent concepts so now this concept needs to be promoted to a Concept Scheme
+ concept = models.Concept.objects.get(pk=self.id)
+ concept.nodetype_id = "ConceptScheme"
+ concept.save()
+ self.load(concept)
+
+ for relation in models.Relation.objects.filter(conceptfrom=concept, relationtype_id="narrower"):
+ relation.relationtype_id = "hasTopConcept"
+ relation.save()
+
+ deletedrelatedconcepts = []
+ for relatedconcept in self.relatedconcepts:
+ conceptrelations = models.Relation.objects.filter(
+ Q(relationtype="related") | Q(relationtype="member") | Q(relationtype__category="Mapping Properties"),
+ conceptto=relatedconcept.id,
+ conceptfrom=self.id,
+ )
+ for relation in conceptrelations:
+ relation.delete()
+ deletedrelatedconcepts.append(relatedconcept)
+
+ conceptrelations = models.Relation.objects.filter(
+ Q(relationtype="related") | Q(relationtype="member") | Q(relationtype__category="Mapping Properties"),
+ conceptfrom=relatedconcept.id,
+ conceptto=self.id,
+ )
+ for relation in conceptrelations:
+ relation.delete()
+ deletedrelatedconcepts.append(relatedconcept)
+
+ for deletedrelatedconcept in deletedrelatedconcepts:
+ if deletedrelatedconcept in self.relatedconcepts:
+ self.relatedconcepts.remove(deletedrelatedconcept)
+
+ for value in self.values:
+ if not isinstance(value, ConceptValue):
+ value = ConceptValue(value)
+ value.delete()
+
+ if delete_self:
+ concepts_to_delete = Concept.gather_concepts_to_delete(self)
+ for key, concept in concepts_to_delete.items():
+ # delete only member relationships if the nodetype == Collection
+ if concept.nodetype == "Collection":
+ concept = Concept().get(
+ id=concept.id,
+ include_subconcepts=True,
+ include_parentconcepts=True,
+ include=["label"],
+ up_depth_limit=1,
+ semantic=False,
+ )
+
+ def find_concepts(concept):
+ if len(concept.parentconcepts) <= 1:
+ for subconcept in concept.subconcepts:
+ conceptrelation = models.Relation.objects.get(
+ conceptfrom=concept.id, conceptto=subconcept.id, relationtype="member"
+ )
+ conceptrelation.delete()
+ find_concepts(subconcept)
+
+ find_concepts(concept)
+ # if the concept is a collection, loop through the nodes and delete their rdmCollection values
+ for node in models.Node.objects.filter(config__rdmCollection=concept.id):
+ node.config["rdmCollection"] = None
+ node.save()
+
+ models.Concept.objects.get(pk=key).delete()
+ return
+
+ def add_relation(self, concepttorelate, relationtype):
+ """
+ Relates this concept to 'concepttorelate' via the relationtype
+
+ """
+
+ relation, created = models.Relation.objects.get_or_create(
+ conceptfrom_id=self.id, conceptto_id=concepttorelate.id, relationtype_id=relationtype
+ )
+ return relation
+
+ @staticmethod
+ def gather_concepts_to_delete(concept, lang=settings.LANGUAGE_CODE):
+ """
+ Gets a dictionary of all the concepts ids to delete
+ The values of the dictionary keys differ somewhat depending on the node type being deleted
+ If the nodetype == 'Concept' then return ConceptValue objects keyed to the concept id
+ If the nodetype == 'ConceptScheme' then return a ConceptValue object with the value set to any ONE prefLabel keyed to the concept id
+ We do this because it takes so long to gather the ids of the concepts when deleting a Scheme or Group
+
+ """
+
+ concepts_to_delete = {}
+
+ # Here we have to worry about making sure we don't delete nodes that have more than 1 parent
+ if concept.nodetype == "Concept":
+ concept = Concept().get(
+ id=concept.id, include_subconcepts=True, include_parentconcepts=True, include=["label"], up_depth_limit=1
+ )
+
+ def find_concepts(concept):
+ if len(concept.parentconcepts) <= 1:
+ concepts_to_delete[concept.id] = concept
+ for subconcept in concept.subconcepts:
+ find_concepts(subconcept)
+
+ find_concepts(concept)
+ return concepts_to_delete
+
+ # here we can just delete everything and so use a recursive CTE to get the concept ids much more quickly
+ if concept.nodetype == "ConceptScheme":
+ concepts_to_delete[concept.id] = concept
+ rows = Concept().get_child_concepts(concept.id)
+ for row in rows:
+ if row[0] not in concepts_to_delete:
+ concepts_to_delete[row[0]] = Concept({"id": row[0]})
+
+ concepts_to_delete[row[0]].addvalue({"id": row[2], "conceptid": row[0], "value": row[1]})
+
+ if concept.nodetype == "Collection":
+ concepts_to_delete[concept.id] = concept
+ rows = Concept().get_child_collections(concept.id)
+ for row in rows:
+ if row[0] not in concepts_to_delete:
+ concepts_to_delete[row[0]] = Concept({"id": row[0]})
+
+ concepts_to_delete[row[0]].addvalue({"id": row[2], "conceptid": row[0], "value": row[1]})
+
+ return concepts_to_delete
+
+ def get_child_collections_hierarchically(self, conceptid, child_valuetypes=None, offset=0, limit=50, query=None):
+ child_valuetypes = child_valuetypes if child_valuetypes else ["prefLabel"]
+ columns = "valueidto::text, conceptidto::text, valueto, valuetypeto, depth, count(*) OVER() AS full_count, collector"
+ return self.get_child_edges(
+ conceptid, ["member"], child_valuetypes, offset=offset, limit=limit, order_hierarchically=True, query=query, columns=columns
+ )
+
+ def get_child_collections(self, conceptid, child_valuetypes=None, parent_valuetype="prefLabel", columns=None, depth_limit=None):
+ child_valuetypes = child_valuetypes if child_valuetypes else ["prefLabel"]
+ columns = columns if columns else "conceptidto::text, valueto, valueidto::text"
+ return self.get_child_edges(conceptid, ["member"], child_valuetypes, parent_valuetype, columns, depth_limit)
+
+ def get_child_concepts(self, conceptid, child_valuetypes=None, parent_valuetype="prefLabel", columns=None, depth_limit=None):
+ columns = columns if columns else "conceptidto::text, valueto, valueidto::text"
+ return self.get_child_edges(conceptid, ["narrower", "hasTopConcept"], child_valuetypes, parent_valuetype, columns, depth_limit)
+
+ def get_child_concepts_for_indexing(self, conceptid, child_valuetypes=None, parent_valuetype="prefLabel", depth_limit=None):
+ columns = "valueidto::text, conceptidto::text, valuetypeto, categoryto, valueto, languageto"
+ data = self.get_child_edges(conceptid, ["narrower", "hasTopConcept"], child_valuetypes, parent_valuetype, columns, depth_limit)
+ return [dict(list(zip(["id", "conceptid", "type", "category", "value", "language"], d)), top_concept="") for d in data]
+
+ def get_child_edges(
+ self,
+ conceptid,
+ relationtypes,
+ child_valuetypes=None,
+ parent_valuetype="prefLabel",
+ columns=None,
+ depth_limit=None,
+ offset=None,
+ limit=20,
+ order_hierarchically=False,
+ query=None,
+ languageid=None,
+ ):
+ """
+ Recursively builds a list of concept relations for a given concept and all it's subconcepts based on its relationship type and valuetypes.
+
+ """
+
+ # if the conceptid isn't a UUID then Postgres will throw an error and transactions will be aborted #7822
+ try:
+ uuid.UUID(conceptid)
+ except:
+ return []
+
+ # this interpolation is safe because `relationtypes` is hardcoded in all calls, and not accessible via the API
+ relationtypes = " or ".join(["r.relationtype = '%s'" % (relationtype) for relationtype in relationtypes])
+ offset_clause = " limit %(limit)s offset %(offset)s" if offset is not None else ""
+ depth_clause = " and depth < %(depth_limit)s" if depth_limit is not None else ""
+
+ cursor = connection.cursor()
+
+ if order_hierarchically:
+ sql = """
+ WITH RECURSIVE
+
+ ordered_relationships AS (
+ (
+ SELECT r.conceptidfrom, r.conceptidto, r.relationtype, (
+ SELECT value
+ FROM values
+ WHERE conceptid=r.conceptidto
+ AND valuetype in ('prefLabel')
+ ORDER BY (
+ CASE WHEN languageid = %(languageid)s THEN 10
+ WHEN languageid like %(short_languageid)s THEN 5
+ WHEN languageid like %(default_languageid)s THEN 2
+ ELSE 0
+ END
+ ) desc limit 1
+ ) as valuesto,
+ (
+ SELECT value::int
+ FROM values
+ WHERE conceptid=r.conceptidto
+ AND valuetype in ('sortorder')
+ limit 1
+ ) as sortorder,
+ (
+ SELECT value
+ FROM values
+ WHERE conceptid=r.conceptidto
+ AND valuetype in ('collector')
+ limit 1
+ ) as collector
+ FROM relations r
+ WHERE r.conceptidfrom = %(conceptid)s
+ and (%(relationtypes)s)
+ ORDER BY sortorder, valuesto
+ )
+ UNION
+ (
+ SELECT r.conceptidfrom, r.conceptidto, r.relationtype,(
+ SELECT value
+ FROM values
+ WHERE conceptid=r.conceptidto
+ AND valuetype in ('prefLabel')
+ ORDER BY (
+ CASE WHEN languageid = %(languageid)s THEN 10
+ WHEN languageid like %(short_languageid)s THEN 5
+ WHEN languageid like %(default_languageid)s THEN 2
+ ELSE 0
+ END
+ ) desc limit 1
+ ) as valuesto,
+ (
+ SELECT value::int
+ FROM values
+ WHERE conceptid=r.conceptidto
+ AND valuetype in ('sortorder')
+ limit 1
+ ) as sortorder,
+ (
+ SELECT value
+ FROM values
+ WHERE conceptid=r.conceptidto
+ AND valuetype in ('collector')
+ limit 1
+ ) as collector
+ FROM relations r
+ JOIN ordered_relationships b ON(b.conceptidto = r.conceptidfrom)
+ WHERE (%(relationtypes)s)
+ ORDER BY sortorder, valuesto
+ )
+ ),
+
+ children AS (
+ SELECT r.conceptidfrom, r.conceptidto,
+ to_char(row_number() OVER (), 'fm000000') as row,
+ r.collector,
+ 1 AS depth ---|NonRecursive Part
+ FROM ordered_relationships r
+ WHERE r.conceptidfrom = %(conceptid)s
+ and (%(relationtypes)s)
+ UNION
+ SELECT r.conceptidfrom, r.conceptidto,
+ row || '-' || to_char(row_number() OVER (), 'fm000000'),
+ r.collector,
+ depth+1 ---|RecursivePart
+ FROM ordered_relationships r
+ JOIN children b ON(b.conceptidto = r.conceptidfrom)
+ WHERE (%(relationtypes)s)
+ {depth_clause}
+ )
+
+ {subquery}
+
+ SELECT
+ (
+ select row_to_json(d)
+ FROM (
+ SELECT *
+ FROM values
+ WHERE conceptid=%(recursive_table)s.conceptidto
+ AND valuetype in ('prefLabel')
+ ORDER BY (
+ CASE WHEN languageid = %(languageid)s THEN 10
+ WHEN languageid like %(short_languageid)s THEN 5
+ WHEN languageid like %(default_languageid)s THEN 2
+ ELSE 0
+ END
+ ) desc limit 1
+ ) d
+ ) as valueto,
+ depth, collector, count(*) OVER() AS full_count
+
+ FROM %(recursive_table)s order by row {offset_clause};
+ """
+
+ if query:
+ subquery = """
+ , results as (
+ SELECT c.conceptidfrom, c.conceptidto, c.row, c.depth, c.collector
+ FROM children c
+ JOIN values ON(values.conceptid = c.conceptidto)
+ WHERE LOWER(values.value) like %(query)s
+ AND values.valuetype in ('prefLabel')
+ AND LOWER(values.value) != %(match)s
+ UNION
+ SELECT c.conceptidfrom, c.conceptidto, '0' as row, c.depth, c.collector
+ FROM children c
+ JOIN values ON(values.conceptid = c.conceptidto)
+ WHERE LOWER(values.value) = %(match)s
+ AND values.valuetype in ('prefLabel')
+ UNION
+ SELECT c.conceptidfrom, c.conceptidto, c.row, c.depth, c.collector
+ FROM children c
+ JOIN results r on (r.conceptidfrom=c.conceptidto)
+ )
+ """
+ else:
+ subquery = ""
+
+ sql = sql.format(subquery=subquery, offset_clause=offset_clause, depth_clause=depth_clause)
+
+ recursive_table = "results" if query else "children"
+ languageid = get_language() if languageid is None else languageid
+
+ cursor.execute(
+ sql,
+ {
+ "conceptid": conceptid,
+ "relationtypes": AsIs(relationtypes),
+ "depth_limit": depth_limit,
+ "limit": limit,
+ "offset": offset,
+ "query": "%" + query.lower() + "%",
+ "match": query.lower(),
+ "recursive_table": AsIs(recursive_table),
+ "languageid": languageid,
+ "short_languageid": languageid.split("-")[0] + "%",
+ "default_languageid": settings.LANGUAGE_CODE + "%",
+ },
+ )
+ else:
+ sql = """
+ WITH RECURSIVE
+ children AS (
+ SELECT r.conceptidfrom, r.conceptidto, r.relationtype, 1 AS depth
+ FROM relations r
+ WHERE r.conceptidfrom = %(conceptid)s
+ AND (%(relationtypes)s)
+ UNION
+ SELECT r.conceptidfrom, r.conceptidto, r.relationtype, depth+1
+ FROM relations r
+ JOIN children c ON(c.conceptidto = r.conceptidfrom)
+ WHERE (%(relationtypes)s)
+ {depth_clause}
+ ),
+ results AS (
+ SELECT
+ valuefrom.value as valuefrom, valueto.value as valueto,
+ valuefrom.valueid as valueidfrom, valueto.valueid as valueidto,
+ valuefrom.valuetype as valuetypefrom, valueto.valuetype as valuetypeto,
+ valuefrom.languageid as languagefrom, valueto.languageid as languageto,
+ dtypesfrom.category as categoryfrom, dtypesto.category as categoryto,
+ c.conceptidfrom, c.conceptidto
+ FROM values valueto
+ JOIN d_value_types dtypesto ON(dtypesto.valuetype = valueto.valuetype)
+ JOIN children c ON(c.conceptidto = valueto.conceptid)
+ JOIN values valuefrom ON(c.conceptidfrom = valuefrom.conceptid)
+ JOIN d_value_types dtypesfrom ON(dtypesfrom.valuetype = valuefrom.valuetype)
+ WHERE valueto.valuetype in (%(child_valuetypes)s)
+ AND valuefrom.valuetype in (%(child_valuetypes)s)
+ )
+ SELECT distinct %(columns)s
+ FROM results {offset_clause}
+ """
+
+ sql = sql.format(offset_clause=offset_clause, depth_clause=depth_clause)
+
+ if not columns:
+ columns = """
+ conceptidfrom::text, conceptidto::text,
+ valuefrom, valueto,
+ valueidfrom::text, valueidto::text,
+ valuetypefrom, valuetypeto,
+ languagefrom, languageto,
+ categoryfrom, categoryto
+ """
+
+ cursor.execute(
+ sql,
+ {
+ "conceptid": conceptid,
+ "relationtypes": AsIs(relationtypes),
+ "child_valuetypes": ("','").join(
+ child_valuetypes
+ if child_valuetypes
+ else models.DValueType.objects.filter(category="label").values_list("valuetype", flat=True)
+ ),
+ "columns": AsIs(columns),
+ "depth_limit": depth_limit,
+ "limit": limit,
+ "offset": offset,
+ },
+ )
+
+ return cursor.fetchall()
+
+ def traverse(self, func, direction="down", scope=None, **kwargs):
+ """
+ Traverses a concept graph from self to leaf (direction='down') or root (direction='up') calling
+ the given function on each node, passes an optional scope to each function
+
+ Return a value from the function to prematurely end the traversal
+
+ """
+
+ _cache = kwargs.pop("_cache", [])
+ if self.id not in _cache:
+ _cache.append(self.id)
+
+ if scope is None:
+ ret = func(self, **kwargs)
+ else:
+ ret = func(self, scope, **kwargs)
+
+ # break out of the traversal if the function returns a value
+ if ret is not None:
+ return ret
+
+ if direction == "down":
+ for subconcept in self.subconcepts:
+ ret = subconcept.traverse(func, direction, scope, _cache=_cache, **kwargs)
+ if ret is not None:
+ return ret
+ else:
+ for parentconcept in self.parentconcepts:
+ ret = parentconcept.traverse(func, direction, scope, _cache=_cache, **kwargs)
+ if ret is not None:
+ return ret
+
+ def get_sortkey(self, lang=settings.LANGUAGE_CODE):
+ for value in self.values:
+ if value.type == "sortorder":
+ try:
+ return float(value.value)
+ except:
+ return None
+
+ return self.get_preflabel(lang=lang).value
+
+ def natural_keys(self, text):
+ """
+ alist.sort(key=natural_keys) sorts in human order
+ http://nedbatchelder.com/blog/200712/human_sorting.html
+ (See Toothy's implementation in the comments)
+ float regex comes from https://stackoverflow.com/a/12643073/190597
+ """
+
+ def atof(text):
+ try:
+ retval = float(text)
+ except ValueError:
+ retval = text
+ return retval
+
+ return [atof(c) for c in re.split(r"[+-]?([0-9]+(?:[.][0-9]*)?|[.][0-9]+)", str(text))]
+
+ def get_preflabel(self, lang=settings.LANGUAGE_CODE):
+ ranked_labels = []
+ if self.values == []:
+ concept = Concept().get(id=self.id, include_subconcepts=False, include_parentconcepts=False, include=["label"])
+ else:
+ concept = self
+
+ ranked_labels = sorted(
+ concept.values,
+ key=lambda label: rank_label(kind=label.type, source_lang=label.language, target_lang=lang),
+ reverse=True,
+ )
+ if len(ranked_labels) == 0:
+ return ConceptValue()
+
+ return ranked_labels[0]
+
+ def flatten(self, ret=None):
+ """
+ Flattens the graph into a unordered list of concepts
+
+ """
+
+ if ret is None:
+ ret = []
+
+ ret.append(self)
+ for subconcept in self.subconcepts:
+ subconcept.flatten(ret)
+
+ return ret
+
+ def addparent(self, value):
+ if isinstance(value, dict):
+ self.parentconcepts.append(Concept(value))
+ elif isinstance(value, Concept):
+ self.parentconcepts.append(value)
+ else:
+ raise Exception("Invalid parent concept definition: %s" % (value))
+
+ def addsubconcept(self, value):
+ if isinstance(value, dict):
+ self.subconcepts.append(Concept(value))
+ elif isinstance(value, Concept):
+ self.subconcepts.append(value)
+ else:
+ raise Exception(_("Invalid subconcept definition: %s") % (value))
+
+ def addrelatedconcept(self, value):
+ if isinstance(value, dict):
+ self.relatedconcepts.append(Concept(value))
+ elif isinstance(value, Concept):
+ self.relatedconcepts.append(value)
+ else:
+ raise Exception(_("Invalid related concept definition: %s") % (value))
+
+ def addvalue(self, value):
+ if isinstance(value, dict):
+ value["conceptid"] = self.id
+ self.values.append(ConceptValue(value))
+ elif isinstance(value, ConceptValue):
+ self.values.append(value)
+ elif isinstance(value, models.Value):
+ self.values.append(ConceptValue(value))
+ else:
+ raise Exception(_("Invalid value definition: %s") % (value))
+
+ def index(self, scheme=None):
+ if scheme is None:
+ scheme = self.get_context()
+ for value in self.values:
+ value.index(scheme=scheme)
+
+ if self.nodetype == "ConceptScheme":
+ scheme = None
+
+ for subconcept in self.subconcepts:
+ subconcept.index(scheme=scheme)
+
+ def bulk_index(self):
+ concept_docs = []
+
+ if self.nodetype == "ConceptScheme":
+ concept = Concept().get(id=self.id, values=["label"])
+ concept.index()
+ for topConcept in self.get_child_concepts_for_indexing(self.id, depth_limit=1):
+ concept = Concept().get(id=topConcept["conceptid"])
+ scheme = concept.get_context()
+ topConcept["top_concept"] = scheme.id
+ concept_docs.append(se.create_bulk_item(index=CONCEPTS_INDEX, id=topConcept["id"], data=topConcept))
+ for childConcept in concept.get_child_concepts_for_indexing(topConcept["conceptid"]):
+ childConcept["top_concept"] = scheme.id
+ concept_docs.append(se.create_bulk_item(index=CONCEPTS_INDEX, id=childConcept["id"], data=childConcept))
+
+ if self.nodetype == "Concept":
+ concept = Concept().get(id=self.id, values=["label"])
+ scheme = concept.get_context()
+ concept.index(scheme)
+ for childConcept in concept.get_child_concepts_for_indexing(self.id):
+ childConcept["top_concept"] = scheme.id
+ concept_docs.append(se.create_bulk_item(index=CONCEPTS_INDEX, id=childConcept["id"], data=childConcept))
+
+ se.bulk_index(concept_docs)
+
+ def delete_index(self, delete_self=False):
+ def delete_concept_values_index(concepts_to_delete):
+ for concept in concepts_to_delete.values():
+ query = Query(se, start=0, limit=10000)
+ term = Term(field="conceptid", term=concept.id)
+ query.add_query(term)
+ query.delete(index=CONCEPTS_INDEX)
+
+ if delete_self:
+ concepts_to_delete = Concept.gather_concepts_to_delete(self)
+ delete_concept_values_index(concepts_to_delete)
+ else:
+ for subconcept in self.subconcepts:
+ concepts_to_delete = Concept.gather_concepts_to_delete(subconcept)
+ delete_concept_values_index(concepts_to_delete)
+
+ def concept_tree(
+ self,
+ top_concept="00000000-0000-0000-0000-000000000001",
+ lang=settings.LANGUAGE_CODE,
+ mode="semantic",
+ ):
+ class concept(object):
+ def __init__(self, *args, **kwargs):
+ self.label = ""
+ self.labelid = ""
+ self.id = ""
+ self.sortorder = None
+ self.load_on_demand = False
+ self.children = []
+
+ def _findNarrowerConcept(conceptid, depth_limit=None, level=0):
+ labels = models.Value.objects.filter(concept=conceptid)
+ ret = concept()
+ temp = Concept()
+ for label in labels:
+ temp.addvalue(label)
+ if label.valuetype_id == "sortorder":
+ try:
+ ret.sortorder = float(label.value)
+ except:
+ ret.sortorder = None
+
+ label = temp.get_preflabel(lang=lang)
+ ret.label = label.value
+ ret.id = label.conceptid
+ ret.labelid = label.id
+
+ if mode == "semantic":
+ conceptrealations = models.Relation.objects.filter(
+ Q(conceptfrom=conceptid), Q(relationtype__category="Semantic Relations") | Q(relationtype__category="Properties")
+ )
+ if mode == "collections":
+ conceptrealations = models.Relation.objects.filter(
+ Q(conceptfrom=conceptid), Q(relationtype="member") | Q(relationtype="hasCollection")
+ )
+ if depth_limit is not None and len(conceptrealations) > 0 and level >= depth_limit:
+ ret.load_on_demand = True
+ else:
+ if depth_limit is not None:
+ level = level + 1
+ for relation in conceptrealations:
+ ret.children.append(_findNarrowerConcept(relation.conceptto_id, depth_limit=depth_limit, level=level))
+
+ ret.children = sorted(
+ ret.children,
+ key=lambda concept: self.natural_keys(concept.sortorder if concept.sortorder else concept.label),
+ reverse=False,
+ )
+ return ret
+
+ def _findBroaderConcept(conceptid, child_concept, depth_limit=None, level=0):
+ conceptrealations = models.Relation.objects.filter(
+ Q(conceptto=conceptid), ~Q(relationtype="related"), ~Q(relationtype__category="Mapping Properties")
+ )
+ if len(conceptrealations) > 0 and conceptid != top_concept:
+ labels = models.Value.objects.filter(concept=conceptrealations[0].conceptfrom_id)
+ ret = concept()
+ temp = Concept()
+ for label in labels:
+ temp.addvalue(label)
+ label = temp.get_preflabel(lang=lang)
+ ret.label = label.value
+ ret.id = label.conceptid
+ ret.labelid = label.id
+
+ ret.children.append(child_concept)
+ return _findBroaderConcept(conceptrealations[0].conceptfrom_id, ret, depth_limit=depth_limit, level=level)
+ else:
+ return child_concept
+
+ graph = []
+ if self.id is None or self.id == "" or self.id == "None" or self.id == top_concept:
+ if mode == "semantic":
+ concepts = models.Concept.objects.filter(nodetype="ConceptScheme")
+ for conceptmodel in concepts:
+ graph.append(_findNarrowerConcept(conceptmodel.pk, depth_limit=1))
+ if mode == "collections":
+ concepts = models.Concept.objects.filter(nodetype="Collection")
+ for conceptmodel in concepts:
+ graph.append(_findNarrowerConcept(conceptmodel.pk, depth_limit=0))
+
+ graph = sorted(graph, key=lambda concept: concept.label)
+ # graph = _findNarrowerConcept(concepts[0].pk, depth_limit=1).children
+
+ else:
+ graph = _findNarrowerConcept(self.id, depth_limit=1).children
+ # concepts = _findNarrowerConcept(self.id, depth_limit=1)
+ # graph = [_findBroaderConcept(self.id, concepts, depth_limit=1)]
+
+ return graph
+
+ def get_paths(self, lang=settings.LANGUAGE_CODE):
+ def graph_to_paths(current_concept, path=[], path_list=[], _cache=[]):
+ if len(path) == 0:
+ current_path = []
+ else:
+ current_path = path[:]
+
+ current_path.insert(
+ 0,
+ {
+ "label": current_concept.get_preflabel(lang=lang).value,
+ "relationshiptype": current_concept.relationshiptype,
+ "id": current_concept.id,
+ },
+ )
+
+ if len(current_concept.parentconcepts) == 0 or current_concept.id in _cache:
+ path_list.append(current_path[:])
+ else:
+ _cache.append(current_concept.id)
+ for parent in current_concept.parentconcepts:
+ ret = graph_to_paths(parent, current_path, path_list, _cache)
+
+ return path_list
+
+ # def graph_to_paths(current_concept, **kwargs):
+ # path = kwargs.get('path', [])
+ # path_list = kwargs.get('path_list', [])
+
+ # if len(path) == 0:
+ # current_path = []
+ # else:
+ # current_path = path[:]
+
+ # current_path.insert(0, {'label': current_concept.get_preflabel(lang=lang).value, 'relationshiptype': current_concept.relationshiptype, 'id': current_concept.id})
+
+ # if len(current_concept.parentconcepts) == 0:
+ # path_list.append(current_path[:])
+ # # else:
+ # # for parent in current_concept.parentconcepts:
+ # # ret = graph_to_paths(parent, current_path, path_list, _cache)
+
+ # #return path_list
+
+ # self.traverse(graph_to_paths, direction='up')
+
+ return graph_to_paths(self)
+
+ def get_node_and_links(self, lang=settings.LANGUAGE_CODE):
+ nodes = [{"concept_id": self.id, "name": self.get_preflabel(lang=lang).value, "type": "Current"}]
+ links = []
+
+ def get_parent_nodes_and_links(current_concept, _cache=[]):
+ if current_concept.id not in _cache:
+ _cache.append(current_concept.id)
+ parents = current_concept.parentconcepts
+ for parent in parents:
+ nodes.append(
+ {
+ "concept_id": parent.id,
+ "name": parent.get_preflabel(lang=lang).value,
+ "type": "Root" if len(parent.parentconcepts) == 0 else "Ancestor",
+ }
+ )
+ links.append(
+ {
+ "target": current_concept.id,
+ "source": parent.id,
+ "relationship": "broader",
+ }
+ )
+ get_parent_nodes_and_links(parent, _cache)
+
+ get_parent_nodes_and_links(self)
+
+ # def get_parent_nodes_and_links(current_concept):
+ # parents = current_concept.parentconcepts
+ # for parent in parents:
+ # nodes.append({'concept_id': parent.id, 'name': parent.get_preflabel(lang=lang).value, 'type': 'Root' if len(parent.parentconcepts) == 0 else 'Ancestor'})
+ # links.append({'target': current_concept.id, 'source': parent.id, 'relationship': 'broader' })
+
+ # self.traverse(get_parent_nodes_and_links, direction='up')
+
+ for child in self.subconcepts:
+ nodes.append(
+ {
+ "concept_id": child.id,
+ "name": child.get_preflabel(lang=lang).value,
+ "type": "Descendant",
+ }
+ )
+ links.append({"source": self.id, "target": child.id, "relationship": "narrower"})
+
+ for related in self.relatedconcepts:
+ nodes.append(
+ {
+ "concept_id": related.id,
+ "name": related.get_preflabel(lang=lang).value,
+ "type": "Related",
+ }
+ )
+ links.append({"source": self.id, "target": related.id, "relationship": "related"})
+
+ # get unique node list and assign unique integer ids for each node (required by d3)
+ nodes = list({node["concept_id"]: node for node in nodes}.values())
+ for i in range(len(nodes)):
+ nodes[i]["id"] = i
+ for link in links:
+ link["source"] = i if link["source"] == nodes[i]["concept_id"] else link["source"]
+ link["target"] = i if link["target"] == nodes[i]["concept_id"] else link["target"]
+
+ return {"nodes": nodes, "links": links}
+
+ def get_context(self):
+ """
+ get the Top Concept that the Concept particpates in
+
+ """
+
+ if self.nodetype == "Concept" or self.nodetype == "Collection":
+ concept = Concept().get(id=self.id, include_parentconcepts=True, include=None)
+
+ def get_scheme_id(concept):
+ for parentconcept in concept.parentconcepts:
+ if parentconcept.relationshiptype == "hasTopConcept":
+ return concept
+
+ if len(concept.parentconcepts) > 0:
+ return concept.traverse(get_scheme_id, direction="up")
+ else:
+ return self
+
+ else: # like ConceptScheme or EntityType
+ return self
+
+ def get_scheme(self):
+ """
+ get the ConceptScheme that the Concept particpates in
+
+ """
+
+ topConcept = self.get_context()
+ if len(topConcept.parentconcepts) == 1:
+ if topConcept.parentconcepts[0].nodetype == "ConceptScheme":
+ return topConcept.parentconcepts[0]
+
+ return None
+
+ def check_if_concept_in_use(self):
+ """Checks if a concept or any of its subconcepts is in use by a resource instance"""
+
+ in_use = False
+ cursor = connection.cursor()
+ for value in self.values:
+ sql = (
+ """
+ SELECT count(*) from tiles t, jsonb_each_text(t.tiledata) as json_data
+ WHERE json_data.value = '%s'
+ """
+ % value.id
+ )
+ cursor.execute(sql)
+ rows = cursor.fetchall()
+ if rows[0][0] > 0:
+ in_use = True
+ break
+ if in_use is not True:
+ for subconcept in self.subconcepts:
+ in_use = subconcept.check_if_concept_in_use()
+ if in_use == True:
+ return in_use
+ return in_use
+
+ def get_e55_domain(self, conceptid):
+ """
+ For a given entitytypeid creates a dictionary representing that entitytypeid's concept graph (member pathway) formatted to support
+ select2 dropdowns
+
+ """
+ cursor = connection.cursor()
+ cursor.execute(
+ """
+ WITH RECURSIVE children AS (
+ SELECT d.conceptidfrom, d.conceptidto, c2.value, c2.valueid as valueid, c.value as valueto, c.valueid as valueidto, c.valuetype as vtype, c.languageid, 1 AS depth, array[d.conceptidto] AS conceptpath, array[c.valueid] AS idpath ---|NonRecursive Part
+ FROM relations d
+ JOIN values c ON(c.conceptid = d.conceptidto)
+ JOIN values c2 ON(c2.conceptid = d.conceptidfrom)
+ WHERE d.conceptidfrom = %s
+ and c2.valuetype IN ('prefLabel', 'altLabel')
+ and c.valuetype IN ('prefLabel', 'altLabel', 'sortorder', 'collector')
+ and (d.relationtype = 'member' or d.relationtype = 'hasTopConcept')
+ UNION
+ SELECT d.conceptidfrom, d.conceptidto, v2.value, v2.valueid as valueid, v.value as valueto, v.valueid as valueidto, v.valuetype as vtype, v.languageid, depth+1, (conceptpath || d.conceptidto), (idpath || v.valueid) ---|RecursivePart
+ FROM relations d
+ JOIN children b ON(b.conceptidto = d.conceptidfrom)
+ JOIN values v ON(v.conceptid = d.conceptidto)
+ JOIN values v2 ON(v2.conceptid = d.conceptidfrom)
+ WHERE v2.valuetype IN ('prefLabel', 'altLabel')
+ and v.valuetype IN ('prefLabel', 'altLabel', 'sortorder', 'collector')
+ and (d.relationtype = 'member' or d.relationtype = 'hasTopConcept')
+ ) SELECT conceptidfrom::text, conceptidto::text, value, valueid::text, valueto, valueidto::text, languageid, depth, idpath::text, conceptpath::text, vtype FROM children ORDER BY depth, conceptpath;
+ """,
+ [conceptid],
+ )
+ rows = cursor.fetchall()
+
+ column_names = [
+ "conceptidfrom",
+ "conceptidto",
+ "value",
+ "valueid",
+ "valueto",
+ "valueidto",
+ "languageid",
+ "depth",
+ "idpath",
+ "conceptpath",
+ "vtype",
+ ]
+
+ class Val(object):
+ def __init__(self, conceptid):
+ self.text = ""
+ self.conceptid = conceptid
+ self.id = ""
+ self.sortorder = ""
+ self.collector = ""
+ self.children = []
+
+ result = Val(conceptid)
+
+ def _findNarrower(val, path, rec):
+ for conceptid in path:
+ childids = [child.conceptid for child in val.children]
+ if conceptid not in childids:
+ new_val = Val(rec["conceptidto"])
+ if rec["vtype"] == "sortorder":
+ new_val.sortorder = rec["valueto"]
+ elif rec["vtype"] in ("prefLabel", "altLabel"):
+ new_val.text = rec["valueto"]
+ new_val.id = rec["valueidto"]
+ elif rec["vtype"] == "collector":
+ new_val.collector = "collector"
+ val.children.append(new_val)
+ else:
+ for child in val.children:
+ if conceptid == child.conceptid:
+ if conceptid == path[-1]:
+ if rec["vtype"] == "sortorder":
+ child.sortorder = rec["valueto"]
+ elif rec["vtype"] in ("prefLabel", "altLabel"):
+ child.text = rec["valueto"]
+ child.id = rec["valueidto"]
+ elif rec["vtype"] == "collector":
+ child.collector = "collector"
+ path.pop(0)
+ _findNarrower(child, path, rec)
+ val.children.sort(key=lambda x: (x.sortorder, x.text))
+
+ def best_language_last(rec):
+ """_findNarrower updates via recursive search, so sort the best language last."""
+ label_rank = rank_label(kind=rec["vtype"], source_lang=rec["languageid"])
+ return (rec["depth"], rec["conceptpath"], label_rank)
+
+ records = [dict(list(zip(column_names, row))) for row in rows]
+ for rec in sorted(records, key=best_language_last):
+ path = rec["conceptpath"][1:-1].split(",")
+ _findNarrower(result, path, rec)
+
+ return JSONSerializer().serializeToPython(result)["children"]
+
+ def make_collection(self):
+ if len(self.values) == 0:
+ raise Exception(_("Need to include values when creating a collection"))
+ values = JSONSerializer().serializeToPython(self.values)
+ for value in values:
+ value["id"] = ""
+ collection_concept = Concept({"nodetype": "Collection", "values": values})
+
+ def create_collection(conceptfrom):
+ for relation in models.Relation.objects.filter(
+ Q(conceptfrom_id=conceptfrom.id),
+ Q(relationtype__category="Semantic Relations") | Q(relationtype__category="Properties"),
+ ~Q(relationtype="related"),
+ ):
+ conceptto = Concept(relation.conceptto)
+ if conceptfrom == self:
+ collection_concept.add_relation(conceptto, "member")
+ else:
+ conceptfrom.add_relation(conceptto, "member")
+ create_collection(conceptto)
+
+ with transaction.atomic():
+ collection_concept.save()
+ create_collection(self)
+
+ return collection_concept
+
+
+class ConceptValue(object):
+ def __init__(self, *args, **kwargs):
+ self.id = ""
+ self.conceptid = ""
+ self.type = ""
+ self.category = ""
+ self.value = ""
+ self.language = ""
+
+ if len(args) != 0:
+ if isinstance(args[0], str):
+ try:
+ uuid.UUID(args[0])
+ self.get(args[0])
+ except ValueError:
+ self.load(JSONDeserializer().deserialize(args[0]))
+ elif isinstance(args[0], object):
+ self.load(args[0])
+
+ def __repr__(self):
+ return ('%s: %s = "%s" in lang %s') % (self.__class__, self.type, self.value, self.language)
+
+ def get(self, id=""):
+ self.load(models.Value.objects.get(pk=id))
+ return self
+
+ def save(self):
+ if self.value.strip() != "":
+ self.id = self.id if (self.id != "" and self.id is not None) else str(uuid.uuid4())
+ value = models.Value()
+ value.pk = self.id
+ value.value = self.value
+ value.concept_id = self.conceptid # models.Concept.objects.get(pk=self.conceptid)
+ value.valuetype_id = self.type # models.DValueType.objects.get(pk=self.type)
+
+ if self.language != "":
+ # need to normalize language ids to the form xx-XX
+ lang_parts = self.language.lower().replace("_", "-").split("-")
+ try:
+ lang_parts[1] = lang_parts[1].upper()
+ except:
+ pass
+ self.language = "-".join(lang_parts)
+ value.language_id = self.language # models.DLanguage.objects.get(pk=self.language)
+ else:
+ value.language_id = settings.LANGUAGE_CODE
+
+ value.save()
+ self.category = value.valuetype.category
+
+ def delete(self):
+ if self.id != "":
+ newvalue = models.Value.objects.get(pk=self.id)
+ if newvalue.valuetype.valuetype == "image":
+ newvalue = models.FileValue.objects.get(pk=self.id)
+ newvalue.delete()
+ self = ConceptValue()
+ return self
+
+ def load(self, value):
+ if isinstance(value, models.Value):
+ self.id = str(value.pk)
+ self.conceptid = str(value.concept_id)
+ self.type = value.valuetype_id
+ self.category = value.valuetype.category
+ self.value = value.value
+ self.language = value.language_id
+
+ if isinstance(value, dict):
+ self.id = str(value["id"]) if "id" in value else ""
+ self.conceptid = str(value["conceptid"]) if "conceptid" in value else ""
+ self.type = value["type"] if "type" in value else ""
+ self.category = value["category"] if "category" in value else ""
+ self.value = value["value"] if "value" in value else ""
+ self.language = value["language"] if "language" in value else ""
+
+ def index(self, scheme=None):
+ if self.category == "label":
+ data = JSONSerializer().serializeToPython(self)
+ if scheme is None:
+ scheme = self.get_scheme_id()
+ if scheme is None:
+ raise Exception(_("Index of label failed. Index type (scheme id) could not be derived from the label."))
+
+ data["top_concept"] = scheme.id
+ se.index_data(index=CONCEPTS_INDEX, body=data, idfield="id")
+
+ def delete_index(self):
+ query = Query(se, start=0, limit=10000)
+ term = Term(field="id", term=self.id)
+ query.add_query(term)
+ query.delete(index=CONCEPTS_INDEX)
+
+ def get_scheme_id(self):
+ result = se.search(index=CONCEPTS_INDEX, id=self.id)
+ if result["found"]:
+ return Concept(result["top_concept"])
+ else:
+ return None
+
+
+def get_preflabel_from_conceptid(conceptid, lang):
+ default = {
+ "category": "",
+ "conceptid": "",
+ "language": "",
+ "value": "",
+ "type": "",
+ "id": "",
+ }
+ query = Query(se)
+ bool_query = Bool()
+ bool_query.must(Match(field="type", query="prefLabel", type="phrase"))
+ bool_query.filter(Terms(field="conceptid", terms=[conceptid]))
+ query.add_query(bool_query)
+ preflabels = query.search(index=CONCEPTS_INDEX)["hits"]["hits"]
+
+ ranked = sorted(
+ preflabels,
+ key=lambda prefLabel: rank_label(kind=prefLabel["_source"]["type"],
+ source_lang=prefLabel["_source"]["language"],
+ target_lang=lang,
+ ),
+ reverse=True,
+ )
+
+ if not ranked:
+ return default
+ return ranked[0]["_source"]
+
+
+def get_valueids_from_concept_label(label, conceptid=None, lang=None):
+ def exact_val_match(val, conceptid=None):
+ # exact term match, don't care about relevance ordering.
+ # due to language formating issues, and with (hopefully) small result sets
+ # easier to have filter logic in python than to craft it in dsl
+ if conceptid is None:
+ return {"query": {"bool": {"filter": {"match_phrase": {"value": val}}}}}
+ else:
+ return {
+ "query": {
+ "bool": {
+ "filter": [
+ {"match_phrase": {"value": val}},
+ {"term": {"conceptid": conceptid}},
+ ]
+ }
+ }
+ }
+
+ concept_label_results = se.search(index=CONCEPTS_INDEX, **exact_val_match(label, conceptid))
+ if concept_label_results is None:
+ print("Found no matches for label:'{0}' and concept_id: '{1}'".format(label, conceptid))
+ return
+ return [
+ res["_source"]
+ for res in concept_label_results["hits"]["hits"]
+ if lang is None or res["_source"]["language"].lower() == lang.lower()
+ ]
+
+
+def get_preflabel_from_valueid(valueid, lang):
+ concept_label = se.search(index=CONCEPTS_INDEX, id=valueid)
+ if concept_label["found"]:
+ return get_preflabel_from_conceptid(concept_label["_source"]["conceptid"], lang)
diff --git a/arches/app/permissions/arches_standard.py b/arches/app/permissions/arches_standard.py
index e1e1e1095a9..d2dd968f53e 100644
--- a/arches/app/permissions/arches_standard.py
+++ b/arches/app/permissions/arches_standard.py
@@ -418,8 +418,8 @@ def get_resource_types_by_perm(self, user: User, perms: str | Iterable[str]) ->
graphs = set()
nodegroups = self.get_nodegroups_by_perm(user, perms)
for node in Node.objects.filter(nodegroup__in=nodegroups).prefetch_related("graph"):
- if node.graph.isresource and str(node.graph_id) != settings.SYSTEM_SETTINGS_RESOURCE_MODEL_ID:
- graphs.add(node.graph)
+ if node.graph.isresource and str(node.graph_id) != SystemSettings.SYSTEM_SETTINGS_RESOURCE_MODEL_ID:
+ graphs.add(str(node.graph.pk))
return list(graphs)
diff --git a/arches/app/utils/permission_backend.py b/arches/app/utils/permission_backend.py
index ce320524c33..dea884eaf1b 100644
--- a/arches/app/utils/permission_backend.py
+++ b/arches/app/utils/permission_backend.py
@@ -167,6 +167,9 @@ def _get_permission_framework():
_PERMISSION_FRAMEWORK = ArchesStandardPermissionFramework()
return _PERMISSION_FRAMEWORK
+def get_createable_resource_models(user):
+ return GraphModel.objects.filter(pk__in=list(get_createable_resource_types(user))).all()
+
def assign_perm(perm, user_or_group, obj=None):
return _get_permission_framework().assign_perm(perm, user_or_group, obj=obj)
diff --git a/arches/app/views/base.py b/arches/app/views/base.py
index b0c5b4b8028..49813cbd338 100644
--- a/arches/app/views/base.py
+++ b/arches/app/views/base.py
@@ -26,13 +26,12 @@
from django.views.generic import TemplateView
from arches.app.datatypes.datatypes import DataTypeFactory
from arches.app.utils.permission_backend import (
- get_createable_resource_types,
+ get_createable_resource_models,
user_is_resource_reviewer,
get_editable_resource_types,
get_resource_types_by_perm,
user_can_read_map_layers,
)
-from arches.app.utils.permission_backend import get_createable_resource_types, user_is_resource_reviewer
class BaseManagerView(TemplateView):
@@ -52,9 +51,7 @@ def get_context_data(self, **kwargs):
if self.request.user.has_perm("view_plugin", plugin):
context["plugins"].append(plugin)
- createable = list(
- models.GraphModel.objects.filter(pk__in=list(get_createable_resource_types(self.request.user))).all()
- )
+ createable = list(get_createable_resource_models(self.request.user))
createable.sort(key=lambda x: x.name.lower())
context["createable_resources"] = JSONSerializer().serialize(
createable,