|
1
|
|
|
""" |
|
2
|
|
|
Module that validates incoming JSON. |
|
3
|
|
|
""" |
|
4
|
|
|
|
|
5
|
|
|
import copy |
|
6
|
|
|
|
|
7
|
|
|
import bleach |
|
8
|
|
|
import colander |
|
9
|
|
|
from language_tags import tags |
|
10
|
|
|
from skosprovider_sqlalchemy.models import ( |
|
11
|
|
|
Language |
|
12
|
|
|
) |
|
13
|
|
|
from sqlalchemy.exc import NoResultFound |
|
14
|
|
|
|
|
15
|
|
|
from atramhasis.errors import ValidationError |
|
16
|
|
|
from atramhasis.skos import IDGenerationStrategy |
|
17
|
|
|
|
|
18
|
|
|
|
|
19
|
|
|
class ForcedString(colander.String): |
|
20
|
|
|
""" |
|
21
|
|
|
Acts just like colander.String but anything it gets will be turned into a string. |
|
22
|
|
|
|
|
23
|
|
|
eg. Passing a number 1 will treat it as "1". null/None remains null/None |
|
24
|
|
|
""" |
|
25
|
|
|
|
|
26
|
|
|
def deserialize(self, node, cstruct): |
|
27
|
|
|
if ( |
|
28
|
|
|
not isinstance(cstruct, str) |
|
29
|
|
|
and not isinstance(cstruct, bytes) |
|
30
|
|
|
and cstruct is not colander.null |
|
31
|
|
|
): |
|
32
|
|
|
cstruct = str(cstruct) |
|
33
|
|
|
return super().deserialize(node, cstruct) |
|
34
|
|
|
|
|
35
|
|
|
|
|
36
|
|
|
class Label(colander.MappingSchema): |
|
37
|
|
|
label = colander.SchemaNode( |
|
38
|
|
|
colander.String() |
|
39
|
|
|
) |
|
40
|
|
|
type = colander.SchemaNode( |
|
41
|
|
|
colander.String() |
|
42
|
|
|
) |
|
43
|
|
|
language = colander.SchemaNode( |
|
44
|
|
|
colander.String() |
|
45
|
|
|
) |
|
46
|
|
|
|
|
47
|
|
|
|
|
48
|
|
|
def html_preparer(value): |
|
49
|
|
|
""" |
|
50
|
|
|
Prepare the value by stripping all html except certain tags. |
|
51
|
|
|
|
|
52
|
|
|
:param value: The value to be cleaned. |
|
53
|
|
|
:rtype: str |
|
54
|
|
|
""" |
|
55
|
|
|
try: |
|
56
|
|
|
return bleach.clean(value, tags=['strong', 'em', 'a'], strip=True) |
|
57
|
|
|
except TypeError: |
|
58
|
|
|
# Trying to clean a non-string |
|
59
|
|
|
# Ignore for now so it can be caught later on |
|
60
|
|
|
return value |
|
61
|
|
|
|
|
62
|
|
|
|
|
63
|
|
|
class Note(colander.MappingSchema): |
|
64
|
|
|
note = colander.SchemaNode( |
|
65
|
|
|
colander.String(), |
|
66
|
|
|
preparer=html_preparer |
|
67
|
|
|
) |
|
68
|
|
|
type = colander.SchemaNode( |
|
69
|
|
|
colander.String() |
|
70
|
|
|
) |
|
71
|
|
|
language = colander.SchemaNode( |
|
72
|
|
|
colander.String() |
|
73
|
|
|
) |
|
74
|
|
|
|
|
75
|
|
|
|
|
76
|
|
|
class Source(colander.MappingSchema): |
|
77
|
|
|
citation = colander.SchemaNode( |
|
78
|
|
|
colander.String(), |
|
79
|
|
|
preparer=html_preparer |
|
80
|
|
|
) |
|
81
|
|
|
|
|
82
|
|
|
|
|
83
|
|
|
class Labels(colander.SequenceSchema): |
|
84
|
|
|
label = Label() |
|
85
|
|
|
|
|
86
|
|
|
|
|
87
|
|
|
class Notes(colander.SequenceSchema): |
|
88
|
|
|
note = Note() |
|
89
|
|
|
|
|
90
|
|
|
|
|
91
|
|
|
class Sources(colander.SequenceSchema): |
|
92
|
|
|
source = Source() |
|
93
|
|
|
|
|
94
|
|
|
|
|
95
|
|
|
class RelatedConcept(colander.MappingSchema): |
|
96
|
|
|
id = colander.SchemaNode( |
|
97
|
|
|
ForcedString() |
|
98
|
|
|
) |
|
99
|
|
|
|
|
100
|
|
|
|
|
101
|
|
|
class Concepts(colander.SequenceSchema): |
|
102
|
|
|
concept = RelatedConcept() |
|
103
|
|
|
|
|
104
|
|
|
|
|
105
|
|
|
class MatchList(colander.SequenceSchema): |
|
106
|
|
|
match = colander.SchemaNode( |
|
107
|
|
|
colander.String(), |
|
108
|
|
|
missing=None |
|
109
|
|
|
) |
|
110
|
|
|
|
|
111
|
|
|
|
|
112
|
|
|
class Matches(colander.MappingSchema): |
|
113
|
|
|
broad = MatchList(missing=[]) |
|
114
|
|
|
close = MatchList(missing=[]) |
|
115
|
|
|
exact = MatchList(missing=[]) |
|
116
|
|
|
narrow = MatchList(missing=[]) |
|
117
|
|
|
related = MatchList(missing=[]) |
|
118
|
|
|
|
|
119
|
|
|
|
|
120
|
|
|
class Concept(colander.MappingSchema): |
|
121
|
|
|
id = colander.SchemaNode( |
|
122
|
|
|
ForcedString(), |
|
123
|
|
|
missing=None |
|
124
|
|
|
) |
|
125
|
|
|
type = colander.SchemaNode( |
|
126
|
|
|
colander.String(), |
|
127
|
|
|
missing='concept' |
|
128
|
|
|
) |
|
129
|
|
|
labels = Labels(missing=[]) |
|
130
|
|
|
notes = Notes(missing=[]) |
|
131
|
|
|
sources = Sources(missing=[]) |
|
132
|
|
|
broader = Concepts(missing=[]) |
|
133
|
|
|
narrower = Concepts(missing=[]) |
|
134
|
|
|
related = Concepts(missing=[]) |
|
135
|
|
|
members = Concepts(missing=[]) |
|
136
|
|
|
member_of = Concepts(missing=[]) |
|
137
|
|
|
subordinate_arrays = Concepts(missing=[]) |
|
138
|
|
|
superordinates = Concepts(missing=[]) |
|
139
|
|
|
matches = Matches(missing={}) |
|
140
|
|
|
infer_concept_relations = colander.SchemaNode( |
|
141
|
|
|
colander.Boolean(), |
|
142
|
|
|
missing=colander.drop |
|
143
|
|
|
) |
|
144
|
|
|
|
|
145
|
|
|
def preparer(self, concept): |
|
146
|
|
|
return concept |
|
147
|
|
|
|
|
148
|
|
|
|
|
149
|
|
|
class ConceptScheme(colander.MappingSchema): |
|
150
|
|
|
labels = Labels(missing=[]) |
|
151
|
|
|
notes = Notes(missing=[]) |
|
152
|
|
|
sources = Sources(missing=[]) |
|
153
|
|
|
|
|
154
|
|
|
|
|
155
|
|
|
class LanguageTag(colander.MappingSchema): |
|
156
|
|
|
id = colander.SchemaNode( |
|
157
|
|
|
colander.String() |
|
158
|
|
|
) |
|
159
|
|
|
name = colander.SchemaNode( |
|
160
|
|
|
colander.String() |
|
161
|
|
|
) |
|
162
|
|
|
|
|
163
|
|
|
|
|
164
|
|
|
def concept_schema_validator(node, cstruct): |
|
165
|
|
|
""" |
|
166
|
|
|
This validator validates an incoming concept or collection |
|
167
|
|
|
|
|
168
|
|
|
This validator will run a list of rules against the concept or collection |
|
169
|
|
|
to see that there are no validation rules being broken. |
|
170
|
|
|
|
|
171
|
|
|
:param colander.SchemaNode node: The schema that's being used while validating. |
|
172
|
|
|
:param cstruct: The concept or collection being validated. |
|
173
|
|
|
""" |
|
174
|
|
|
request = node.bindings['request'] |
|
175
|
|
|
skos_manager = request.data_managers['skos_manager'] |
|
176
|
|
|
languages_manager = request.data_managers['languages_manager'] |
|
177
|
|
|
provider = node.bindings['provider'] |
|
178
|
|
|
conceptscheme_id = provider.conceptscheme_id |
|
179
|
|
|
concept_type = cstruct['type'] |
|
180
|
|
|
collection_id = cstruct['id'] |
|
181
|
|
|
narrower = None |
|
182
|
|
|
broader = None |
|
183
|
|
|
related = None |
|
184
|
|
|
members = None |
|
185
|
|
|
member_of = None |
|
186
|
|
|
r_validated = False |
|
187
|
|
|
n_validated = False |
|
188
|
|
|
b_validated = False |
|
189
|
|
|
m_validated = False |
|
190
|
|
|
o_validated = False |
|
191
|
|
|
errors = [] |
|
192
|
|
|
min_labels_rule(errors, node, cstruct) |
|
193
|
|
|
if 'labels' in cstruct: |
|
194
|
|
|
labels = copy.deepcopy(cstruct['labels']) |
|
195
|
|
|
label_type_rule(errors, node, skos_manager, labels) |
|
196
|
|
|
label_lang_rule(errors, node, languages_manager, labels) |
|
197
|
|
|
max_preflabels_rule(errors, node, labels) |
|
198
|
|
|
if 'related' in cstruct: |
|
199
|
|
|
related = copy.deepcopy(cstruct['related']) |
|
200
|
|
|
related = [m['id'] for m in related] |
|
201
|
|
|
r_validated = semantic_relations_rule(errors, node['related'], skos_manager, |
|
202
|
|
|
conceptscheme_id, related, collection_id) |
|
203
|
|
|
concept_relations_rule(errors, node['related'], related, concept_type) |
|
204
|
|
|
if 'narrower' in cstruct: |
|
205
|
|
|
narrower = copy.deepcopy(cstruct['narrower']) |
|
206
|
|
|
narrower = [m['id'] for m in narrower] |
|
207
|
|
|
n_validated = semantic_relations_rule(errors, node['narrower'], skos_manager, |
|
208
|
|
|
conceptscheme_id, narrower, collection_id) |
|
209
|
|
|
concept_relations_rule(errors, node['narrower'], narrower, concept_type) |
|
210
|
|
|
if 'broader' in cstruct: |
|
211
|
|
|
broader = copy.deepcopy(cstruct['broader']) |
|
212
|
|
|
broader = [m['id'] for m in broader] |
|
213
|
|
|
b_validated = semantic_relations_rule(errors, node['broader'], skos_manager, |
|
214
|
|
|
conceptscheme_id, broader, collection_id) |
|
215
|
|
|
concept_relations_rule(errors, node['broader'], broader, concept_type) |
|
216
|
|
|
if 'members' in cstruct: |
|
217
|
|
|
members = copy.deepcopy(cstruct['members']) |
|
218
|
|
|
members = [m['id'] for m in members] |
|
219
|
|
|
m_validated = semantic_relations_rule(errors, node['members'], skos_manager, |
|
220
|
|
|
conceptscheme_id, members, collection_id) |
|
221
|
|
|
if 'member_of' in cstruct: |
|
222
|
|
|
member_of = copy.deepcopy(cstruct['member_of']) |
|
223
|
|
|
member_of = [m['id'] for m in member_of] |
|
224
|
|
|
o_validated = semantic_relations_rule(errors, node['member_of'], skos_manager, |
|
225
|
|
|
conceptscheme_id, member_of, collection_id) |
|
226
|
|
|
if r_validated and n_validated and b_validated: |
|
227
|
|
|
concept_type_rule(errors, node['narrower'], skos_manager, conceptscheme_id, narrower) |
|
228
|
|
|
narrower_hierarchy_rule(errors, node['narrower'], skos_manager, conceptscheme_id, cstruct) |
|
229
|
|
|
concept_type_rule(errors, node['broader'], skos_manager, conceptscheme_id, broader) |
|
230
|
|
|
broader_hierarchy_rule(errors, node['broader'], skos_manager, conceptscheme_id, cstruct) |
|
231
|
|
|
concept_type_rule(errors, node['related'], skos_manager, conceptscheme_id, related) |
|
232
|
|
|
|
|
233
|
|
|
if m_validated and o_validated: |
|
234
|
|
|
members_only_in_collection_rule(errors, node['members'], concept_type, members) |
|
235
|
|
|
collection_members_unique_rule(errors, node['members'], members) |
|
236
|
|
|
collection_type_rule(errors, node['member_of'], skos_manager, conceptscheme_id, member_of) |
|
237
|
|
|
memberof_hierarchy_rule(errors, node['member_of'], skos_manager, conceptscheme_id, cstruct) |
|
238
|
|
|
members_hierarchy_rule(errors, node['members'], skos_manager, conceptscheme_id, cstruct) |
|
239
|
|
|
|
|
240
|
|
|
if 'matches' in cstruct: |
|
241
|
|
|
matches = copy.deepcopy(cstruct['matches']) |
|
242
|
|
|
concept_matches_rule(errors, node['matches'], matches, concept_type) |
|
243
|
|
|
concept_matches_unique_rule(errors, node['matches'], matches) |
|
244
|
|
|
|
|
245
|
|
|
if 'subordinate_arrays' in cstruct: |
|
246
|
|
|
subordinate_arrays = copy.deepcopy(cstruct['subordinate_arrays']) |
|
247
|
|
|
subordinate_arrays = [m['id'] for m in subordinate_arrays] |
|
248
|
|
|
subordinate_arrays_only_in_concept_rule(errors, node['subordinate_arrays'], concept_type, subordinate_arrays) |
|
249
|
|
|
subordinate_arrays_type_rule(errors, node['subordinate_arrays'], skos_manager, conceptscheme_id, |
|
250
|
|
|
subordinate_arrays) |
|
251
|
|
|
subordinate_arrays_hierarchy_rule(errors, node['subordinate_arrays'], skos_manager, conceptscheme_id, cstruct) |
|
252
|
|
|
|
|
253
|
|
|
if 'superordinates' in cstruct: |
|
254
|
|
|
superordinates = copy.deepcopy(cstruct['superordinates']) |
|
255
|
|
|
superordinates = [m['id'] for m in superordinates] |
|
256
|
|
|
superordinates_only_in_concept_rule(errors, node['superordinates'], concept_type, superordinates) |
|
257
|
|
|
superordinates_type_rule(errors, node['superordinates'], skos_manager, conceptscheme_id, superordinates) |
|
258
|
|
|
superordinates_hierarchy_rule(errors, node['superordinates'], skos_manager, conceptscheme_id, cstruct) |
|
259
|
|
|
|
|
260
|
|
|
if cstruct['type'] == 'concept' and 'infer_concept_relations' in cstruct: |
|
261
|
|
|
msg = "'infer_concept_relations' can only be set for collections." |
|
262
|
|
|
errors.append(colander.Invalid(node['infer_concept_relations'], msg=msg)) |
|
263
|
|
|
|
|
264
|
|
|
id_generation_strategy = provider.metadata.get( |
|
265
|
|
|
"atramhasis.id_generation_strategy", IDGenerationStrategy.NUMERIC |
|
266
|
|
|
) |
|
267
|
|
|
if id_generation_strategy == IDGenerationStrategy.MANUAL: |
|
268
|
|
|
if not cstruct.get("id"): |
|
269
|
|
|
msg = "Required for this provider." |
|
270
|
|
|
errors.append(colander.Invalid(node["id"], msg=msg)) |
|
271
|
|
|
else: |
|
272
|
|
|
try: |
|
273
|
|
|
skos_manager.get_thing(cstruct["id"], conceptscheme_id) |
|
274
|
|
|
except NoResultFound: |
|
275
|
|
|
# this is desired |
|
276
|
|
|
pass |
|
277
|
|
|
else: |
|
278
|
|
|
msg = f"{cstruct['id']} already exists." |
|
279
|
|
|
errors.append(colander.Invalid(node["id"], msg=msg)) |
|
280
|
|
|
|
|
281
|
|
|
if len(errors) > 0: |
|
282
|
|
|
raise ValidationError( |
|
283
|
|
|
'Concept could not be validated', |
|
284
|
|
|
[e.asdict() for e in errors] |
|
285
|
|
|
) |
|
286
|
|
|
|
|
287
|
|
|
|
|
288
|
|
|
def conceptscheme_schema_validator(node, cstruct): |
|
289
|
|
|
""" |
|
290
|
|
|
This validator validates the incoming conceptscheme labels |
|
291
|
|
|
|
|
292
|
|
|
:param colander.SchemaNode node: The schema that's being used while validating. |
|
293
|
|
|
:param cstruct: The conceptscheme being validated. |
|
294
|
|
|
""" |
|
295
|
|
|
request = node.bindings['request'] |
|
296
|
|
|
skos_manager = request.data_managers['skos_manager'] |
|
297
|
|
|
languages_manager = request.data_managers['languages_manager'] |
|
298
|
|
|
errors = [] |
|
299
|
|
|
min_labels_rule(errors, node, cstruct) |
|
300
|
|
|
if 'labels' in cstruct: |
|
301
|
|
|
labels = copy.deepcopy(cstruct['labels']) |
|
302
|
|
|
label_type_rule(errors, node, skos_manager, labels) |
|
303
|
|
|
label_lang_rule(errors, node, languages_manager, labels) |
|
304
|
|
|
max_preflabels_rule(errors, node, labels) |
|
305
|
|
|
if len(errors) > 0: |
|
306
|
|
|
raise ValidationError( |
|
307
|
|
|
'ConceptScheme could not be validated', |
|
308
|
|
|
[e.asdict() for e in errors] |
|
309
|
|
|
) |
|
310
|
|
|
|
|
311
|
|
|
|
|
312
|
|
|
def concept_relations_rule(errors, node_location, relations, concept_type): |
|
313
|
|
|
""" |
|
314
|
|
|
Checks that only concepts have narrower, broader and related relations. |
|
315
|
|
|
""" |
|
316
|
|
|
if relations is not None and len(relations) > 0 and concept_type != 'concept': |
|
317
|
|
|
errors.append(colander.Invalid( |
|
318
|
|
|
node_location, |
|
319
|
|
|
'Only concepts can have narrower/broader/related relations' |
|
320
|
|
|
)) |
|
321
|
|
|
|
|
322
|
|
|
|
|
323
|
|
|
def max_preflabels_rule(errors, node, labels): |
|
324
|
|
|
""" |
|
325
|
|
|
Checks that there's only one prefLabel for a certain language. |
|
326
|
|
|
""" |
|
327
|
|
|
preflabel_found = [] |
|
328
|
|
|
for label in labels: |
|
329
|
|
|
if label['type'] == 'prefLabel': |
|
330
|
|
|
if label['language'] in preflabel_found: |
|
331
|
|
|
errors.append(colander.Invalid( |
|
332
|
|
|
node['labels'], |
|
333
|
|
|
'Only one prefLabel per language allowed.' |
|
334
|
|
|
)) |
|
335
|
|
|
else: |
|
336
|
|
|
preflabel_found.append(label['language']) |
|
337
|
|
|
|
|
338
|
|
|
|
|
339
|
|
|
def min_labels_rule(errors, node, cstruct): |
|
340
|
|
|
""" |
|
341
|
|
|
Checks that a label or collection always has a least one label. |
|
342
|
|
|
""" |
|
343
|
|
|
if 'labels' in cstruct: |
|
344
|
|
|
labels = copy.deepcopy(cstruct['labels']) |
|
345
|
|
|
if len(labels) == 0: |
|
346
|
|
|
errors.append(colander.Invalid( |
|
347
|
|
|
node['labels'], |
|
348
|
|
|
'At least one label is necessary' |
|
349
|
|
|
)) |
|
350
|
|
|
|
|
351
|
|
|
|
|
352
|
|
|
def label_type_rule(errors, node, skos_manager, labels): |
|
353
|
|
|
""" |
|
354
|
|
|
Checks that a label has the correct type. |
|
355
|
|
|
""" |
|
356
|
|
|
label_types = skos_manager.get_all_label_types() |
|
357
|
|
|
label_types = [label_type.name for label_type in label_types] |
|
358
|
|
|
for label in labels: |
|
359
|
|
|
if label['type'] not in label_types: |
|
360
|
|
|
errors.append(colander.Invalid( |
|
361
|
|
|
node['labels'], |
|
362
|
|
|
'Invalid labeltype.' |
|
363
|
|
|
)) |
|
364
|
|
|
|
|
365
|
|
|
|
|
366
|
|
|
def label_lang_rule(errors, node, languages_manager, labels): |
|
367
|
|
|
""" |
|
368
|
|
|
Checks that languages of a label are valid. |
|
369
|
|
|
|
|
370
|
|
|
Checks that they are valid IANA language tags. If the language tag was not |
|
371
|
|
|
already present in the database, it adds them. |
|
372
|
|
|
""" |
|
373
|
|
|
for label in labels: |
|
374
|
|
|
language_tag = label['language'] |
|
375
|
|
|
if not tags.check(language_tag): |
|
376
|
|
|
errors.append(colander.Invalid( |
|
377
|
|
|
node['labels'], |
|
378
|
|
|
'Invalid language tag: %s' % ", ".join([err.message for err in tags.tag(language_tag).errors]) |
|
379
|
|
|
)) |
|
380
|
|
|
else: |
|
381
|
|
|
languages_present = languages_manager.count_languages(language_tag) |
|
382
|
|
|
if not languages_present: |
|
383
|
|
|
descriptions = ', '.join(tags.description(language_tag)) |
|
384
|
|
|
language_item = Language(id=language_tag, name=descriptions) |
|
385
|
|
|
languages_manager.save(language_item) |
|
386
|
|
|
|
|
387
|
|
|
|
|
388
|
|
|
def concept_type_rule(errors, node_location, skos_manager, conceptscheme_id, items): |
|
389
|
|
|
""" |
|
390
|
|
|
Checks that the targets of narrower, broader and related are concepts and |
|
391
|
|
|
not collections. |
|
392
|
|
|
""" |
|
393
|
|
|
for item_concept_id in items: |
|
394
|
|
|
item_concept = skos_manager.get_thing(item_concept_id, conceptscheme_id) |
|
395
|
|
|
if item_concept.type != 'concept': |
|
396
|
|
|
errors.append(colander.Invalid( |
|
397
|
|
|
node_location, |
|
398
|
|
|
'A narrower, broader or related concept should always be a concept, not a collection' |
|
399
|
|
|
)) |
|
400
|
|
|
|
|
401
|
|
|
|
|
402
|
|
|
def collection_type_rule(errors, node_location, skos_manager, conceptscheme_id, members): |
|
403
|
|
|
""" |
|
404
|
|
|
Checks that the targets of member_of are collections and not concepts. |
|
405
|
|
|
""" |
|
406
|
|
|
for member_collection_id in members: |
|
407
|
|
|
member_collection = skos_manager.get_thing(member_collection_id, conceptscheme_id) |
|
408
|
|
|
if member_collection.type != 'collection': |
|
409
|
|
|
errors.append(colander.Invalid( |
|
410
|
|
|
node_location, |
|
411
|
|
|
'A member_of parent should always be a collection' |
|
412
|
|
|
)) |
|
413
|
|
|
|
|
414
|
|
|
|
|
415
|
|
|
def semantic_relations_rule(errors, node_location, skos_manager, conceptscheme_id, members, collection_id): |
|
416
|
|
|
""" |
|
417
|
|
|
Checks that the elements in a group of concepts or collections are not the |
|
418
|
|
|
the group itself, that they actually exist and are within |
|
419
|
|
|
the same conceptscheme. |
|
420
|
|
|
""" |
|
421
|
|
|
for member_concept_id in members: |
|
422
|
|
|
if member_concept_id == collection_id: |
|
423
|
|
|
errors.append(colander.Invalid( |
|
424
|
|
|
node_location, |
|
425
|
|
|
'A concept or collection cannot be related to itself' |
|
426
|
|
|
)) |
|
427
|
|
|
return False |
|
428
|
|
|
try: |
|
429
|
|
|
skos_manager.get_thing(member_concept_id, conceptscheme_id) |
|
430
|
|
|
except NoResultFound: |
|
431
|
|
|
errors.append(colander.Invalid( |
|
432
|
|
|
node_location, |
|
433
|
|
|
'Concept not found, check concept_id. Please be aware members should be within one scheme' |
|
434
|
|
|
)) |
|
435
|
|
|
return False |
|
436
|
|
|
return True |
|
437
|
|
|
|
|
438
|
|
|
|
|
439
|
|
|
def hierarchy_build(skos_manager, conceptscheme_id, property_list, property_hierarchy, property_concept_type, |
|
440
|
|
|
property_list_name): |
|
441
|
|
|
for property_concept_id in property_list: |
|
442
|
|
|
try: |
|
443
|
|
|
property_concept = skos_manager.get_thing(property_concept_id, conceptscheme_id) |
|
444
|
|
|
except NoResultFound: |
|
445
|
|
|
property_concept = None |
|
446
|
|
|
if property_concept is not None and ( |
|
447
|
|
|
property_concept.type == property_concept_type or property_concept_type is None): |
|
448
|
|
|
property_concepts = [n.concept_id for n in getattr(property_concept, property_list_name)] |
|
449
|
|
|
for members_id in property_concepts: |
|
450
|
|
|
property_hierarchy.append(members_id) |
|
451
|
|
|
hierarchy_build(skos_manager, conceptscheme_id, property_concepts, property_hierarchy, |
|
452
|
|
|
property_concept_type, property_list_name) |
|
453
|
|
|
|
|
454
|
|
|
|
|
455
|
|
|
def hierarchy_rule(errors, node_location, skos_manager, conceptscheme_id, cstruct, property1, property2, |
|
456
|
|
|
property2_list_name, concept_type, error_message): |
|
457
|
|
|
""" |
|
458
|
|
|
Checks that the property1 of a concept are not already in property2 hierarchy |
|
459
|
|
|
|
|
460
|
|
|
""" |
|
461
|
|
|
property2_hierarchy = [] |
|
462
|
|
|
property1_list = [] |
|
463
|
|
|
if property1 in cstruct: |
|
464
|
|
|
property1_value = copy.deepcopy(cstruct[property1]) |
|
465
|
|
|
property1_list = [m['id'] for m in property1_value] |
|
466
|
|
|
if property2 in cstruct: |
|
467
|
|
|
property2_value = copy.deepcopy(cstruct[property2]) |
|
468
|
|
|
property2_list = [m['id'] for m in property2_value] |
|
469
|
|
|
property2_hierarchy = property2_list |
|
470
|
|
|
hierarchy_build(skos_manager, conceptscheme_id, property2_list, property2_hierarchy, concept_type, |
|
471
|
|
|
property2_list_name) |
|
472
|
|
|
for broader_concept_id in property1_list: |
|
473
|
|
|
if broader_concept_id in property2_hierarchy: |
|
474
|
|
|
errors.append(colander.Invalid( |
|
475
|
|
|
node_location, |
|
476
|
|
|
error_message |
|
477
|
|
|
)) |
|
478
|
|
|
|
|
479
|
|
|
|
|
480
|
|
|
def broader_hierarchy_rule(errors, node_location, skos_manager, conceptscheme_id, cstruct): |
|
481
|
|
|
""" |
|
482
|
|
|
Checks that the broader concepts of a concepts are not alreadt narrower |
|
483
|
|
|
concepts of that concept. |
|
484
|
|
|
""" |
|
485
|
|
|
hierarchy_rule(errors, node_location, skos_manager, conceptscheme_id, cstruct, 'broader', 'narrower', |
|
486
|
|
|
'narrower_concepts', 'concept', |
|
487
|
|
|
'The broader concept of a concept must not itself be a narrower concept of the concept being edited.' |
|
488
|
|
|
) |
|
489
|
|
|
|
|
490
|
|
|
|
|
491
|
|
|
def narrower_hierarchy_rule(errors, node_location, skos_manager, conceptscheme_id, cstruct): |
|
492
|
|
|
""" |
|
493
|
|
|
Checks that the narrower concepts of a concept are not already broader |
|
494
|
|
|
concepts of that concept. |
|
495
|
|
|
""" |
|
496
|
|
|
hierarchy_rule(errors, node_location, skos_manager, conceptscheme_id, cstruct, 'narrower', 'broader', |
|
497
|
|
|
'broader_concepts', 'concept', |
|
498
|
|
|
'The narrower concept of a concept must not itself be a broader concept of the concept being edited.' |
|
499
|
|
|
) |
|
500
|
|
|
|
|
501
|
|
|
|
|
502
|
|
|
def collection_members_unique_rule(errors, node_location, members): |
|
503
|
|
|
""" |
|
504
|
|
|
Checks that a collection has no duplicate members. |
|
505
|
|
|
""" |
|
506
|
|
|
if len(members) > len(set(members)): |
|
507
|
|
|
errors.append(colander.Invalid( |
|
508
|
|
|
node_location, |
|
509
|
|
|
'All members of a collection should be unique.' |
|
510
|
|
|
)) |
|
511
|
|
|
|
|
512
|
|
|
|
|
513
|
|
|
def members_only_in_collection_rule(errors, node, concept_type, members): |
|
514
|
|
|
""" |
|
515
|
|
|
Checks that only collections have members. |
|
516
|
|
|
""" |
|
517
|
|
|
if concept_type != 'collection' and len(members) > 0: |
|
518
|
|
|
errors.append(colander.Invalid( |
|
519
|
|
|
node, |
|
520
|
|
|
'Only collections can have members.' |
|
521
|
|
|
)) |
|
522
|
|
|
|
|
523
|
|
|
|
|
524
|
|
|
def memberof_hierarchy_rule(errors, node_location, skos_manager, conceptscheme_id, cstruct): |
|
525
|
|
|
hierarchy_rule(errors, node_location, skos_manager, conceptscheme_id, cstruct, 'member_of', 'members', |
|
526
|
|
|
'members', 'collection', |
|
527
|
|
|
'The parent member_of collection of a concept must not itself be a member of the concept being edited.' |
|
528
|
|
|
) |
|
529
|
|
|
|
|
530
|
|
|
|
|
531
|
|
|
def members_hierarchy_rule(errors, node_location, skos_manager, conceptscheme_id, cstruct): |
|
532
|
|
|
""" |
|
533
|
|
|
Checks that a collection does not have members that are in themselves |
|
534
|
|
|
already "parents" of that collection. |
|
535
|
|
|
""" |
|
536
|
|
|
hierarchy_rule(errors, node_location, skos_manager, conceptscheme_id, cstruct, 'members', 'member_of', |
|
537
|
|
|
'member_of', 'collection', |
|
538
|
|
|
'The item of a members collection must not itself be a parent of the concept/collection being edited.' |
|
539
|
|
|
) |
|
540
|
|
|
|
|
541
|
|
|
|
|
542
|
|
|
def concept_matches_rule(errors, node_location, matches, concept_type): |
|
543
|
|
|
""" |
|
544
|
|
|
Checks that only concepts have matches. |
|
545
|
|
|
""" |
|
546
|
|
|
if matches is not None and len(matches) > 0 and concept_type != 'concept': |
|
547
|
|
|
errors.append(colander.Invalid( |
|
548
|
|
|
node_location, |
|
549
|
|
|
'Only concepts can have matches' |
|
550
|
|
|
)) |
|
551
|
|
|
|
|
552
|
|
|
|
|
553
|
|
|
def concept_matches_unique_rule(errors, node_location, matches): |
|
554
|
|
|
""" |
|
555
|
|
|
Checks that a concept has not duplicate matches. |
|
556
|
|
|
|
|
557
|
|
|
This means that a concept can only have one match (no matter what the type) |
|
558
|
|
|
with another concept. We don't allow eg. a concept that has both a broadMatch |
|
559
|
|
|
and a relatedMatch with the same concept. |
|
560
|
|
|
""" |
|
561
|
|
|
if matches is not None: |
|
562
|
|
|
uri_list = [] |
|
563
|
|
|
for matchtype in matches: |
|
564
|
|
|
uri_list.extend([uri for uri in matches[matchtype]]) |
|
565
|
|
|
if len(uri_list) > len(set(uri_list)): |
|
566
|
|
|
errors.append(colander.Invalid( |
|
567
|
|
|
node_location, |
|
568
|
|
|
'All matches of a concept should be unique.' |
|
569
|
|
|
)) |
|
570
|
|
|
|
|
571
|
|
|
|
|
572
|
|
|
def languagetag_validator(node, cstruct): |
|
573
|
|
|
""" |
|
574
|
|
|
This validator validates a languagetag. |
|
575
|
|
|
|
|
576
|
|
|
The validator will check if a tag is a valid IANA language tag. The the |
|
577
|
|
|
validator is informed that this should be a new language tag, it will also |
|
578
|
|
|
check if the tag doesn't already exist. |
|
579
|
|
|
|
|
580
|
|
|
:param colander.SchemaNode node: The schema that's being used while validating. |
|
581
|
|
|
:param cstruct: The value being validated. |
|
582
|
|
|
""" |
|
583
|
|
|
request = node.bindings['request'] |
|
584
|
|
|
languages_manager = request.data_managers['languages_manager'] |
|
585
|
|
|
new = node.bindings['new'] |
|
586
|
|
|
errors = [] |
|
587
|
|
|
language_tag = cstruct['id'] |
|
588
|
|
|
|
|
589
|
|
|
if new: |
|
590
|
|
|
languagetag_checkduplicate(node['id'], language_tag, languages_manager, errors) |
|
591
|
|
|
languagetag_isvalid_rule(node['id'], language_tag, errors) |
|
592
|
|
|
|
|
593
|
|
|
if len(errors) > 0: |
|
594
|
|
|
raise ValidationError( |
|
595
|
|
|
'Language could not be validated', |
|
596
|
|
|
[e.asdict() for e in errors] |
|
597
|
|
|
) |
|
598
|
|
|
|
|
599
|
|
|
|
|
600
|
|
|
def languagetag_isvalid_rule(node, language_tag, errors): |
|
601
|
|
|
""" |
|
602
|
|
|
Check that a languagetag is a valid IANA language tag. |
|
603
|
|
|
""" |
|
604
|
|
|
if not tags.check(language_tag): |
|
605
|
|
|
errors.append(colander.Invalid( |
|
606
|
|
|
node, |
|
607
|
|
|
'Invalid language tag: %s' % ", ".join([err.message for err in tags.tag(language_tag).errors]) |
|
608
|
|
|
)) |
|
609
|
|
|
|
|
610
|
|
|
|
|
611
|
|
|
def languagetag_checkduplicate(node, language_tag, languages_manager, errors): |
|
612
|
|
|
""" |
|
613
|
|
|
Check that a languagetag isn't duplicated. |
|
614
|
|
|
""" |
|
615
|
|
|
language_present = languages_manager.count_languages(language_tag) |
|
616
|
|
|
if language_present: |
|
617
|
|
|
errors.append(colander.Invalid( |
|
618
|
|
|
node, |
|
619
|
|
|
'Duplicate language tag: %s' % language_tag) |
|
620
|
|
|
) |
|
621
|
|
|
|
|
622
|
|
|
|
|
623
|
|
|
def subordinate_arrays_only_in_concept_rule(errors, node, concept_type, subordinate_arrays): |
|
624
|
|
|
""" |
|
625
|
|
|
Checks that only a concept has subordinate arrays. |
|
626
|
|
|
""" |
|
627
|
|
|
if concept_type != 'concept' and len(subordinate_arrays) > 0: |
|
628
|
|
|
errors.append(colander.Invalid( |
|
629
|
|
|
node, |
|
630
|
|
|
'Only concept can have subordinate arrays.' |
|
631
|
|
|
)) |
|
632
|
|
|
|
|
633
|
|
|
|
|
634
|
|
|
def subordinate_arrays_type_rule(errors, node_location, skos_manager, conceptscheme_id, subordinate_arrays): |
|
635
|
|
|
""" |
|
636
|
|
|
Checks that subordinate arrays are always collections. |
|
637
|
|
|
""" |
|
638
|
|
|
for subordinate_id in subordinate_arrays: |
|
639
|
|
|
subordinate = skos_manager.get_thing(subordinate_id, conceptscheme_id) |
|
640
|
|
|
if subordinate.type != 'collection': |
|
641
|
|
|
errors.append(colander.Invalid( |
|
642
|
|
|
node_location, |
|
643
|
|
|
'A subordinate array should always be a collection' |
|
644
|
|
|
)) |
|
645
|
|
|
|
|
646
|
|
|
|
|
647
|
|
|
def subordinate_arrays_hierarchy_rule(errors, node_location, skos_manager, conceptscheme_id, cstruct): |
|
648
|
|
|
""" |
|
649
|
|
|
Checks that the subordinate arrays of a concept are not themselves |
|
650
|
|
|
parents of that concept. |
|
651
|
|
|
""" |
|
652
|
|
|
hierarchy_rule(errors, node_location, skos_manager, conceptscheme_id, cstruct, 'subordinate_arrays', 'member_of', |
|
653
|
|
|
'members', 'collection', |
|
654
|
|
|
'The subordinate_array collection of a concept must not itself be a parent of the concept being edited.' |
|
655
|
|
|
) |
|
656
|
|
|
|
|
657
|
|
|
|
|
658
|
|
|
def superordinates_only_in_concept_rule(errors, node, concept_type, superordinates): |
|
659
|
|
|
""" |
|
660
|
|
|
Checks that only collections have superordinates. |
|
661
|
|
|
""" |
|
662
|
|
|
if concept_type != 'collection' and len(superordinates) > 0: |
|
663
|
|
|
errors.append(colander.Invalid( |
|
664
|
|
|
node, |
|
665
|
|
|
'Only collection can have superordinates.' |
|
666
|
|
|
)) |
|
667
|
|
|
|
|
668
|
|
|
|
|
669
|
|
|
def superordinates_type_rule(errors, node_location, skos_manager, conceptscheme_id, superordinates): |
|
670
|
|
|
""" |
|
671
|
|
|
Checks that superordinates are always concepts. |
|
672
|
|
|
""" |
|
673
|
|
|
for superordinate_id in superordinates: |
|
674
|
|
|
superordinate = skos_manager.get_thing(superordinate_id, conceptscheme_id) |
|
675
|
|
|
if superordinate.type != 'concept': |
|
676
|
|
|
errors.append(colander.Invalid( |
|
677
|
|
|
node_location, |
|
678
|
|
|
'A superordinate should always be a concept' |
|
679
|
|
|
)) |
|
680
|
|
|
|
|
681
|
|
|
|
|
682
|
|
|
def superordinates_hierarchy_rule(errors, node_location, skos_manager, conceptscheme_id, cstruct): |
|
683
|
|
|
""" |
|
684
|
|
|
Checks that the superordinate concepts of a collection are not themselves |
|
685
|
|
|
members of that collection. |
|
686
|
|
|
""" |
|
687
|
|
|
hierarchy_rule(errors, node_location, skos_manager, conceptscheme_id, cstruct, 'superordinates', 'members', |
|
688
|
|
|
'members', 'collection', |
|
689
|
|
|
'The superordinates of a collection must not itself be a member of the collection being edited.' |
|
690
|
|
|
) |
|
691
|
|
|
|