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

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 

6 

7class SkosViewer(BaseViewer): 

8 """Viewer SKOS con visualizzazione LODE-style.""" 

9 

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) 

14 

15 # 2. Define the Table of Contents structure for SKOS 

16 toc_config = [ 

17 ('Collection', 'collections', 'Collections'), 

18 ('Concept', 'concepts', 'Concepts'), 

19 ] 

20 

21 # 3. Build grouped view with SKOS-specific formatting 

22 return self._build_skos_grouped_view(toc_config, language) 

23 

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 = [] 

28 

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 ] 

34 

35 if instances: 

36 sections.append({ 

37 'id': section_id, 

38 'title': section_title, 

39 'entities': self._format_skos_entities(instances, language) 

40 }) 

41 

42 return { 

43 'grouped_view': True, 

44 'sections': sections 

45 } 

46 

47 def _format_skos_entities(self, instances: List, language: Optional[str] = None) -> List[Dict]: 

48 """Formatta le entità SKOS in stile LODE.""" 

49 entities = [] 

50 

51 for instance in instances: 

52 uri = instance.has_identifier 

53 safe_id = hashlib.md5(str(uri).encode('utf-8')).hexdigest() 

54 

55 # Get definition 

56 definition = self._get_definition(instance, language) 

57 

58 # Build SKOS-specific semantic sections 

59 semantic_sections = [] 

60 

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 }) 

67 

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 }) 

75 

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 }) 

82 

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 }) 

89 

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 }) 

96 

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 }) 

104 

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 }) 

111 

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 }) 

118 

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 }) 

125 

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 }) 

132 

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 

147 

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 }) 

157 

158 entities.sort(key=lambda x: (x['label'] or x['uri']).lower()) 

159 return entities 

160 

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] 

165 

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 }) 

181 

182 return items 

183 

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 

189 

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