Test Failed
Push — develop ( 75ee4b...de0baf )
by Jonas
02:13 queued 12s
created

atramhasis.validators.members_hierarchy_rule()   A

Complexity

Conditions 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 1
nop 5
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