Coverage for lode / viewer / skos_viewer.py: 13%
82 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-10-05 16:02 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-10-05 16:02 +0000
1# viewer/skos.py
2import hashlib
3from typing import Dict, Optional, List
4from lode.viewer.base_viewer import BaseViewer
5from lode.helpers.rdf import iri_local_name
7class SkosViewer(BaseViewer):
8 """Viewer SKOS con visualizzazione LODE-style."""
10 def get_view_data(self, resource_uri: Optional[str] = None, language: Optional[str] = None) -> Dict:
11 # 1. Single resource detail view
12 if resource_uri:
13 return super().get_view_data(resource_uri, language)
15 # 2. Define the Table of Contents structure for SKOS
16 toc_config = [
17 ('Collection', 'collections', 'Collections'),
18 ('Concept', 'concepts', 'Concepts'),
19 ]
21 # 3. Build grouped view with SKOS-specific formatting
22 return self._build_skos_grouped_view(toc_config, language)
24 def _build_skos_grouped_view(self, group_definitions: List, language: Optional[str] = None) -> Dict:
25 """Costruisce la vista raggruppata con formattazione SKOS-style."""
26 all_instances = self.get_all_instances()
27 sections = []
29 for class_key, section_id, section_title in group_definitions:
30 instances = [
31 inst for inst in all_instances
32 if type(inst).__name__ == class_key
33 ]
35 if instances:
36 sections.append({
37 'id': section_id,
38 'title': section_title,
39 'entities': self._format_skos_entities(instances, language)
40 })
42 return {
43 'grouped_view': True,
44 'sections': sections
45 }
47 def _format_skos_entities(self, instances: List, language: Optional[str] = None) -> List[Dict]:
48 """Formatta le entità SKOS in stile LODE."""
49 entities = []
51 for instance in instances:
52 uri = instance.has_identifier
53 safe_id = hashlib.md5(str(uri).encode('utf-8')).hexdigest()
55 # Get definition
56 definition = self._get_definition(instance, language)
58 # Build SKOS-specific semantic sections
59 semantic_sections = []
61 # === COLLECTION-specific relations ===
62 if hasattr(instance, 'has_member') and instance.has_member:
63 semantic_sections.append({
64 'title': 'has members',
65 'concepts': self._format_concept_list(instance.has_member, language)
66 })
68 # === CONCEPT-specific relations ===
69 # is equivalent to
70 if hasattr(instance, 'is_equivalent_to') and instance.is_equivalent_to:
71 semantic_sections.append({
72 'title': 'is equivalent to',
73 'concepts': self._format_concept_list(instance.is_equivalent_to, language)
74 })
76 # has super-concepts (broader)
77 if hasattr(instance, 'is_sub_concept_of') and instance.is_sub_concept_of:
78 semantic_sections.append({
79 'title': 'has super-concepts',
80 'concepts': self._format_concept_list(instance.is_sub_concept_of, language)
81 })
83 # is disjoint with
84 if hasattr(instance, 'is_disjoint_with') and instance.is_disjoint_with:
85 semantic_sections.append({
86 'title': 'is disjoint with',
87 'concepts': self._format_concept_list(instance.is_disjoint_with, language)
88 })
90 # is related to
91 if hasattr(instance, 'is_related_to') and instance.is_related_to:
92 semantic_sections.append({
93 'title': 'is related to',
94 'concepts': self._format_concept_list(instance.is_related_to, language)
95 })
97 # Mapping relations
98 # has broad match
99 if hasattr(instance, 'has_broad_match') and instance.has_broad_match:
100 semantic_sections.append({
101 'title': 'has broad match',
102 'concepts': self._format_concept_list(instance.has_broad_match, language)
103 })
105 # has narrow match
106 if hasattr(instance, 'has_narrow_match') and instance.has_narrow_match:
107 semantic_sections.append({
108 'title': 'has narrow match',
109 'concepts': self._format_concept_list(instance.has_narrow_match, language)
110 })
112 # has exact match
113 if hasattr(instance, 'has_exact_match') and instance.has_exact_match:
114 semantic_sections.append({
115 'title': 'has exact match',
116 'concepts': self._format_concept_list(instance.has_exact_match, language)
117 })
119 # has close match
120 if hasattr(instance, 'has_close_match') and instance.has_close_match:
121 semantic_sections.append({
122 'title': 'has close match',
123 'concepts': self._format_concept_list(instance.has_close_match, language)
124 })
126 # has related match
127 if hasattr(instance, 'has_related_match') and instance.has_related_match:
128 semantic_sections.append({
129 'title': 'has related match',
130 'concepts': self._format_concept_list(instance.has_related_match, language)
131 })
133 # Extract ALL relations from model (like BaseViewer does)
134 relations = {}
135 # Attributes to skip (already handled above or not useful to display)
136 skip_attrs = {
137 'has_identifier', 'has_label', 'has_preferred_label', 'has_definition',
138 'has_member', # Handled in semantic_sections for Collections
139 'is_equivalent_to', 'is_sub_concept_of', 'is_disjoint_with', 'is_related_to',
140 'has_broad_match', 'has_narrow_match', 'has_exact_match', 'has_close_match', 'has_related_match',
141 'is_ordered', # Boolean, not a relation
142 }
143 for attr, value in instance.__dict__.items():
144 if not attr.startswith('_') and value and attr not in skip_attrs:
145 clean_name = attr.replace('has_', '').replace('is_', '').replace('_', ' ').title()
146 relations[clean_name] = value
148 entities.append({
149 'type': type(instance).__name__,
150 'uri': uri,
151 'label': self._get_best_label(instance, language),
152 'anchor_id': f"id_{safe_id}",
153 'definition': definition,
154 'semantic_sections': semantic_sections,
155 'relations': relations,
156 })
158 entities.sort(key=lambda x: (x['label'] or x['uri']).lower())
159 return entities
161 def _format_concept_list(self, concepts, language: Optional[str]) -> List[Dict]:
162 """Formatta una lista di concetti con label e URI."""
163 items = []
164 concept_list = concepts if isinstance(concepts, (list, set)) else [concepts]
166 for concept in concept_list:
167 if hasattr(concept, 'has_identifier'):
168 items.append({
169 'label': self._get_best_label(concept, language),
170 'uri': concept.has_identifier,
171 'anchor_id': f"id_{hashlib.md5(concept.has_identifier.encode()).hexdigest()}"
172 })
173 elif isinstance(concept, str):
174 # External URI
175 items.append({
176 'label': iri_local_name(concept),
177 'uri': concept,
178 'anchor_id': None,
179 'external': True
180 })
182 return items
184 def _get_definition(self, instance, language: Optional[str]) -> Optional[str]:
185 """Estrae la definizione."""
186 if hasattr(instance, 'has_definition') and instance.has_definition:
187 return self._get_literal_value(instance.has_definition, language)
188 return None
190 def _get_literal_value(self, value, language: Optional[str]) -> Optional[str]:
191 """Estrae il valore stringa da un Literal o lista di Literal."""
192 if isinstance(value, (set, list)):
193 if language:
194 for v in value:
195 if hasattr(v, 'get_has_language') and v.get_has_language() == language:
196 return v.get_has_value() if hasattr(v, 'get_has_value') else str(v)
197 for v in value:
198 if hasattr(v, 'get_has_value'):
199 return v.get_has_value()
200 return str(v)
201 elif hasattr(value, 'get_has_value'):
202 return value.get_has_value()
203 elif value:
204 return str(value)
205 return None