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