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

1# viewer/skos.py 

2import hashlib 

3from typing import Dict, Optional, List 

4from lode.viewer.base_viewer import BaseViewer 

5 

6class SkosViewer(BaseViewer): 

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

8 

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) 

13 

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

15 toc_config = [ 

16 ('Collection', 'collections', 'Collections'), 

17 ('Concept', 'concepts', 'Concepts'), 

18 ] 

19 

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

21 return self._build_skos_grouped_view(toc_config, language) 

22 

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

27 

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 ] 

33 

34 if instances: 

35 sections.append({ 

36 'id': section_id, 

37 'title': section_title, 

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

39 }) 

40 

41 return { 

42 'grouped_view': True, 

43 'sections': sections 

44 } 

45 

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

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

48 entities = [] 

49 

50 for instance in instances: 

51 uri = instance.has_identifier 

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

53 

54 # Get definition 

55 definition = self._get_definition(instance, language) 

56 

57 # Build SKOS-specific semantic sections 

58 semantic_sections = [] 

59 

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

66 

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

74 

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

81 

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

88 

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

95 

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

103 

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

110 

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

117 

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

124 

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

131 

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 

146 

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

156 

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

158 return entities 

159 

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] 

164 

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

180 

181 return items 

182 

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 

188 

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