3web2ldap.app.schema.viewer - Display LDAPv3 schema
5web2ldap - a web-based LDAP Client,
6see https://www.web2ldap.de for details
8(C) 1998-2022 by Michael Stroeder <michael
@stroeder.com>
10This software
is distributed under the terms of the
11Apache License Version 2.0 (Apache-2.0)
12https://www.apache.org/licenses/LICENSE-2.0
17from ldap0.schema.subentry import (
22from ldap0.schema.models
import (
34from ..
import ErrorExit
40from ..form
import OIDInput
41from ..searchform
import SEARCH_OPT_ATTR_EXISTS, SEARCH_OPT_IS_EQUAL
42from .
import OBSOLETE_TEMPL, schema_link_text, schema_anchor
43from .syntaxes
import syntax_registry
45SCHEMA_VIEWER_USAGE =
"""
48 <li>You can search for schema elements by OID
or name.</li>
49 <li>Wildcard search
with *
is supported.</li>
50 <li>For browsing choose
from context menu on the right</li>
54SCHEMA_ELEMENT_HEAD_TMPL = """
56<h1>%s <em>%s</em> (%s)</h1>
58<a id=
"alvestrand_oid" href=
"%s?https://www.alvestrand.no/objectid/%s.html">[Alvestrand]</a>
59<a id=
"oid-info_oid" href=
"%s?http://www.oid-info.com/get/%s">[oid-info.com]</a>
61<dt>Schema element string:</dt>
62<dd><code>%s</code></dd>
68def schema_anchors(app, se_names, se_class):
70 for se_nameoroid
in se_names:
72 se_obj = app.schema.get_obj(se_class, se_nameoroid, default=
None, raise_keyerror=
True)
74 link_texts.append((se_nameoroid, se_nameoroid))
78 schema_id = se_obj.oid
79 except AttributeError:
80 schema_id = se_obj.ruleid
86 (
'oid_class', SCHEMA_ATTR_MAPPING[se_class]),
89 link_texts.append((ltxt, anchor))
90 link_texts.sort(key=
lambda x: x[0].lower())
91 return [i[1]
for i
in link_texts]
96 returns HTML for displaying a schema descriptions inheritance tree
98 app.outf.write('<dl>')
99 se_obj = schema.get_obj(se_class, se_oid)
100 if se_obj
is not None:
101 display_id = (se_obj.names
or (se_oid,))[0]
102 app.outf.write(
schema_anchor(app, display_id, se_class, name_template=
'<dt>{anchor}</dt>'))
104 app.outf.write(
'<dd>')
105 for sub_se_oid
in se_tree[se_oid]:
107 app.outf.write(
'</dd>')
109 app.outf.write(
'<dd></dd>')
110 app.outf.write(
'</dl>')
115 """Build context menu with schema-related items"""
116 context_menu_list = []
119 sub_schema_dn = app.ls.l.search_subschemasubentry_s(app.dn)
120 except ldap0.LDAPError:
123 if sub_schema_dn
is not None:
125 (
'dn', sub_schema_dn),
126 (
'filterstr',
'(objectClass=subschema)'),
128 for schema_attr
in SCHEMA_ATTRS+[
'objectClass',
'cn']:
129 form_param_list.append((
'read_attr', schema_attr))
130 context_menu_list.append(
132 'read',
'Subschema Subentry',
134 title=
'Directly read the subschema subentry'),
138 SCHEMA_ATTR_MAPPING[se_class]
139 for se_class
in app.schema.sed.keys()
140 if app.schema.sed[se_class]
142 se_class_attrs.sort(key=str.lower)
143 for se_class_attr
in se_class_attrs:
144 context_menu_list.append(
146 'oid', se_class_attr,
147 [(
'dn', app.dn), (
'oid_class', se_class_attr)],
148 title=
'Browse all %s' % (se_class_attr,),
151 return context_menu_list
155 type_desc =
'Abstract Schema Element'
163 schema_id = self.
_se.oid
164 except AttributeError:
165 schema_id = self.
_se.ruleid
166 self.
_sei = app.schema.get_inheritedobj(self.
_se.__class__, schema_id, [])
170 class_attr_value = getattr(self.
_sei, class_attr,
None)
171 if class_attr_value
is None:
173 if isinstance(class_attr_value, (tuple, list)):
174 class_attr_value_list = list(class_attr_value)
175 class_attr_value_list.sort(key=str.lower)
177 class_attr_value_list = [class_attr_value]
179 value_output =
', '.join([
180 self.
_app.form.s2d(v, sp_entity=
' ', lf_entity=
'<br>')
181 for v
in class_attr_value_list
184 value_output =
', '.join(
187 self.
_app.outf.write(
'<dt>%s</dt>\n<dd>\n%s\n</dd>\n' % (text, value_output))
191 ms_ad_schema_link =
''
192 if 'schemaNamingContext' in self.
_app.ls.root_dse:
194 result = self.
_app.ls.l.search_s(
195 self.
_app.ls.root_dse[
'schemaNamingContext'][0].decode(self.
_app.ls.charset),
199 '(&(objectClass=attributeSchema)(attributeID=%s))'
200 '(&(objectClass=classSchema)(governsID=%s))'
208 except ldap0.LDAPError:
212 ad_schema_dn, ad_schema_entry = result[0].dn_s, result[0].entry_s
213 ms_ad_schema_link = (
214 '<dt>Schema Definition Entry (MS AD)</dt>\n'
218 'read', ad_schema_entry[
'cn'][0],
219 [(
'dn', ad_schema_dn)],
222 obsolete = getattr(self.
_se,
'obsolete', 0)
228 getattr(self.
_se,
'names', (()))
235 self.
_app.outf.write(
236 SCHEMA_ELEMENT_HEAD_TMPL % (
239 OBSOLETE_TEMPL[obsolete] % (
240 ', '.join(getattr(self.
_se,
'names', (()))),
243 self.
_app.form.action_url(
'urlredirect', self.
_app.sid), self.
_se.oid,
244 self.
_app.form.action_url(
'urlredirect', self.
_app.sid), self.
_se.oid,
245 self.
_app.form.s2d(str(self.
_se)),
254 type_desc =
'Object class'
256 (
'Description',
'desc',
None),
257 (
'Derived from',
'sup', ObjectClass),
261 DisplaySchemaElement.__init__(self, app, se)
262 self.
_sei_sei = app.schema.get_inheritedobj(self.
_se.__class__, self.
_se.oid, [
'kind'])
265 DisplaySchemaElement.disp_details(self)
266 must, may = self.
_schema.attribute_types([self.
_se.oid], raise_keyerror=
False)
268 self.
_app.outf.write(
'<dt>Kind of object class:</dt><dd>\n%s </dd>\n' % (
269 OBJECTCLASS_KIND_STR[self.
_sei_sei.kind],
272 self.
_app.outf.write(
'<dt>All required attributes:</dt><dd>\n%s </dd>\n' % (
275 self.
_app.outf.write(
'<dt>All allowed attributes:</dt><dd>\n%s </dd>\n' % (
280 content_rule = self.
_schema.get_obj(DITContentRule, self.
_se.oid)
282 self.
_app.outf.write(
283 '<dt>Governed by DIT content rule:</dt><dd>\n%s </dd>\n' % (
287 self.
_app.outf.write(
288 '<dt>Applicable auxiliary object classes:</dt><dd>\n%s </dd>\n' % (
294 structural_oc_list = []
295 for _, content_rule
in self.
_schema.sed[DITContentRule].items():
296 for aux_class_name
in content_rule.aux:
297 aux_class_oid = self.
_schema.get_oid(ObjectClass, aux_class_name)
298 if aux_class_oid == self.
_se.oid:
299 dcr_list.append(content_rule.oid)
300 structural_oc_list.append(content_rule.oid)
302 self.
_app.outf.write(
303 '<dt>Referring DIT content rules:</dt><dd>\n%s </dd>\n' % (
307 if structural_oc_list:
308 self.
_app.outf.write(
309 '<dt>Allowed with structural object classes:</dt><dd>\n%s </dd>\n' % (
315 for nf_oid, name_form_se
in self.
_schema.sed[NameForm].items():
316 name_form_oc = name_form_se.oc.lower()
317 se_names = {o.lower()
for o
in self.
_sei_sei.names}
318 if name_form_se.oc == self.
_sei_sei.oid
or name_form_oc
in se_names:
319 oc_ref_list.append(nf_oid)
321 self.
_app.outf.write(
322 '<dt>Applicable name forms:</dt>\n<dd>\n%s\n</dd>\n' % (
327 self.
_app.outf.write(
'<dt>Object class tree:</dt>\n')
328 self.
_app.outf.write(
'<dd>\n')
330 oc_tree = self.
_schema.tree(ObjectClass)
331 except KeyError
as err:
332 self.
_app.outf.write(
333 '<strong>Missing schema elements referenced:<pre>%s</pre></strong>\n' % (
334 self.
_app.form.s2d(err),
338 if self.
_se.oid
in oc_tree
and oc_tree[self.
_se.oid]:
340 self.
_app.outf.write(
' </dd>\n')
342 self.
_app.outf.write(
343 '<dt>Search entries</dt>\n<dd>\n%s\n</dd>\n' % (
346 '(objectClass=%s)' % (
347 self.
_app.form.s2d((self.
_se.names
or [self.
_se.oid])[0]),
350 (
'dn', self.
_app.dn),
351 (
'searchform_mode',
'adv'),
352 (
'search_attr',
'objectClass'),
353 (
'search_option', SEARCH_OPT_IS_EQUAL),
354 (
'search_string', (self.
_se.names
or [self.
_se.oid])[0]),
356 title=
'Search entries by object class',
364 type_desc =
'Attribute type'
366 (
'Description',
'desc',
None),
367 (
'Syntax',
'syntax', LDAPSyntax),
368 (
'Derived from',
'sup', AttributeType),
369 (
'Equality matching rule',
'equality', MatchingRule),
370 (
'Sub-string matching rule',
'substr', MatchingRule),
371 (
'Ordering matching rule',
'ordering', MatchingRule),
375 DisplaySchemaElement.__init__(self, app, se)
378 self.
_se.__class__, self.
_se.oid,
379 (
'syntax',
'equality',
'substr',
'ordering'),
383 self.
_sei_sei = app.schema.get_obj(self.
_se.__class__, self.
_se.oid)
387 DisplaySchemaElement.disp_details(self)
389 at_oid = self.
_se.oid
392 self.
_app.outf.write(
'<dt>Usage:</dt>\n<dd>\n%s\n</dd>\n' % (
394 0:
'userApplications',
395 1:
'directoryOperation',
396 2:
'distributedOperation',
401 if syntax_oid
is not None:
405 mr_use_se = self.
_schema.get_obj(MatchingRuleUse, syntax_oid)
407 for mr_oid, mr_use_se
in self.
_schema.sed[MatchingRuleUse].items():
408 applies_dict[mr_oid] = {}
409 mr_use_se = self.
_schema.get_obj(MatchingRuleUse, mr_oid)
410 for at_nameoroid
in mr_use_se.applies:
411 applies_dict[mr_oid][self.
_schema.get_oid(AttributeType, at_nameoroid)] =
None
413 mr_applicable_for = [
415 for mr_oid
in self.
_schema.sed[MatchingRule].keys()
416 if mr_oid
in applies_dict
and at_oid
in applies_dict[mr_oid]
418 if mr_applicable_for:
419 self.
_app.outf.write(
'<dt>Applicable matching rules:</dt>\n<dd>\n%s\n</dd>\n' % (
427 attr_type_ref_list = []
428 for oc_oid, object_class_se
in self.
_schema.sed[ObjectClass].items():
429 object_class_se = self.
_schema.get_obj(ObjectClass, oc_oid)
430 for dcr_at
in object_class_se.must+object_class_se.may:
431 if dcr_at == at_oid
or dcr_at
in self.
_sei_sei.names:
432 attr_type_ref_list.append(oc_oid)
433 if attr_type_ref_list:
434 self.
_app.outf.write(
435 '<dt>Directly referencing object classes:</dt>\n<dd>\n%s\n</dd>\n' % (
442 all_object_classes = self.
_schema.sed[ObjectClass].keys()
443 attr_type_ref_list = []
444 for oc_oid
in all_object_classes:
445 must, may = self.
_schema.attribute_types([oc_oid], raise_keyerror=
False)
446 if at_oid
in must
or at_oid
in may:
447 attr_type_ref_list.append(oc_oid)
448 if attr_type_ref_list:
449 self.
_app.outf.write(
450 '<dt>Usable in these object classes:</dt>\n<dd>\n%s\n</dd>\n' % (
457 attr_type_ref_list = []
458 for dcr_oid, dit_content_rule_se
in self.
_schema.sed[DITContentRule].items():
459 dit_content_rule_se = self.
_schema.get_obj(DITContentRule, dcr_oid)
460 for dcr_at
in dit_content_rule_se.must+dit_content_rule_se.may+dit_content_rule_se.nots:
461 if dcr_at == at_oid
or dcr_at
in self.
_sei_sei.names:
462 attr_type_ref_list.append(dcr_oid)
463 if attr_type_ref_list:
464 self.
_app.outf.write(
'<dt>Referencing DIT content rules:</dt>\n<dd>\n%s\n</dd>\n' % (
470 attr_type_ref_list = []
471 for nf_oid, name_form_se
in self.
_schema.sed[NameForm].items():
472 name_form_se = self.
_schema.get_obj(NameForm, nf_oid)
473 for nf_at
in name_form_se.must+name_form_se.may:
474 if nf_at == at_oid
or nf_at
in self.
_sei_sei.names:
475 attr_type_ref_list.append(nf_oid)
476 if attr_type_ref_list:
477 self.
_app.outf.write(
'<dt>Referencing name forms:</dt>\n<dd>\n%s\n</dd>\n' % (
484 self.
_app.outf.write(
'<dt>Attribute type tree:</dt>\n<dd>\n')
487 at_tree = self.
_schema.tree(AttributeType)
488 except KeyError
as err:
489 self.
_app.outf.write(
490 '<strong>Missing schema elements referenced:<pre>%s</pre></strong>\n' % (
491 self.
_app.form.s2d(err),
495 if at_oid
in at_tree
and at_tree[at_oid]:
498 self.
_app.outf.write(
499 '</dd>\n<dt>Search entries</dt>\n<dd>\n%s\n</dd>\n' % (
503 self.
_app.form.s2d((self.
_se.names
or [self.
_se.oid])[0]),
506 (
'dn', self.
_app.dn),
507 (
'searchform_mode',
'adv'),
508 (
'search_attr', (self.
_se.names
or [self.
_se.oid])[0]),
509 (
'search_option', SEARCH_OPT_ATTR_EXISTS),
510 (
'search_string',
''),
512 title=
'Search entries by attribute presence',
520 self.
_app.outf.write(
"""
521 <dt>Associated plugin class(es):</dt>
524 <tr><th>Structural<br>object class</th><th>Plugin
class</th>
""")
525 for structural_oc
in (syntax_registry.at2syntax[at_oid].keys()
or [
None]):
526 syntax_class = syntax_registry.get_syntax(
535 self.
_app.outf.write(
'<tr><td>%s</td><td>%s.%s</td></th>\n' % (
537 self.
_app.form.s2d(syntax_class.__module__),
538 self.
_app.form.s2d(syntax_class.__name__),
540 self.
_app.outf.write(
'</table>\n</dd>\n')
545 type_desc =
'LDAP Syntax'
547 (
'Description',
'desc',
None),
551 DisplaySchemaElement.disp_details(self)
553 syntax_using_at_list = [
555 for at_oid
in self.
_schema.sed[AttributeType].keys()
556 if self.
_schema.get_syntax(at_oid) == self.
_se.oid
558 if syntax_using_at_list:
559 self.
_app.outf.write(
'<dt>Referencing attribute types:</dt>\n<dd>\n%s\n</dd>\n' % (
562 syntax_ref_mr_list = self.
_schema.listall(MatchingRule, [(
'syntax', self.
_se.oid)])
563 if syntax_ref_mr_list:
564 self.
_app.outf.write(
'<dt>Referencing matching rules:</dt>\n<dd>\n%s\n</dd>\n' % (
568 x_subst = self.
_se.x_subst
569 except AttributeError:
573 self.
_app.outf.write(
'<dt>Substituted by:</dt>\n<dd>\n%s\n</dd>\n' % (
579 syntax_class = syntax_registry.oid2syntax.get(self.
_se.oid, LDAPSyntax)
580 self.
_app.outf.write(
'<dt>Associated syntax class</dt>\n<dd>\n%s\n</dd>\n' % (
581 '.'.join((syntax_class.__module__, syntax_class.__name__))
587 type_desc =
'Matching Rule'
589 (
'Description',
'desc',
None),
590 (
'LDAP syntax',
'syntax', LDAPSyntax),
594 DisplaySchemaElement.disp_details(self)
595 mr_use_se = self.
_schema.get_obj(MatchingRuleUse, self.
_se.oid)
598 for at_nameoroid
in mr_use_se.applies:
599 applies_dict[self.
_schema.get_oid(AttributeType, at_nameoroid)] =
None
601 mr_applicable_for = [
603 for at_oid
in self.
_schema.sed[AttributeType].keys()
604 if at_oid
in applies_dict
606 if mr_applicable_for:
607 self.
_app.outf.write(
609 '<dt>Applicable for attribute types per matching rule use:</dt>\n'
616 mr_names = set(self.
_se.names)
617 for at_oid
in self.
_schema.sed[AttributeType]:
619 at_se = self.
_schema.get_inheritedobj(
622 (
'equality',
'substr',
'ordering'),
628 at_mr_set = {at_se.equality, at_se.substr, at_se.ordering}
630 at_se.equality
in mr_names
or
631 at_se.substr
in mr_names
or
632 at_se.ordering
in mr_names
or
633 self.
_se.oid
in at_mr_set
635 mr_used_by.append(at_se.oid)
637 self.
_app.outf.write(
'<dt>Referencing attribute types:</dt>\n<dd>\n%s\n</dd>\n' % (
644 type_desc =
'Matching Rule Use'
646 (
'Names',
'names',
None),
647 (
'Matching Rule',
'oid', MatchingRule),
648 (
'Applies to',
'applies', AttributeType),
653 type_desc =
'DIT content rule'
655 (
'Names',
'names',
None),
656 (
'Governs structural object class',
'oid', ObjectClass),
657 (
'Auxiliary classes',
'aux', ObjectClass),
658 (
'Must have',
'must', AttributeType),
659 (
'May have',
'may', AttributeType),
660 (
'Must not have',
'nots', AttributeType),
665 type_desc =
'DIT structure rule'
667 (
'Description',
'desc',
None),
668 (
'Associated name form',
'form', NameForm),
669 (
'Superior structure rules',
'sup', DITStructureRule),
678 getattr(self.
_se,
'names', (()))
685 self.
_app.outf.write(
688 <h1>%s <em>%s</em> (%s)</h1>
690 <dt>Schema element string:</dt>
691 <dd><code>%s</code></dd>
697 getattr(self.
_se,
'names', (()))
700 self.
_app.form.s2d(str(self.
_se)),
708 Display subordinate DIT structure rule(s)
710 DisplaySchemaElement.disp_details(self)
711 ditsr_rules_ref_list = []
712 for ditsr_id, ditsr_se
in self.
_schema.sed[DITStructureRule].items():
713 if self.
_sei.ruleid
in ditsr_se.sup:
714 ditsr_rules_ref_list.append(ditsr_id)
715 if ditsr_rules_ref_list:
716 self.
_app.outf.write(
'<dt>Subordinate DIT structure rules:</dt>\n<dd>\n%s\n</dd>\n' % (
723 type_desc =
'Name form'
725 (
'Description',
'desc',
None),
726 (
'Structural object class this rule applies to',
'oc', ObjectClass),
727 (
'Mandantory naming attributes',
'must', AttributeType),
728 (
'Allowed naming attributes',
'may', AttributeType),
733 Display referencing DIT structure rule(s)
735 DisplaySchemaElement.disp_details(self)
736 ditsr_rules_ref_list = []
737 for ditsr_id, ditsr_se
in self.
_schema.sed[DITStructureRule].items():
738 if ditsr_se.form == self.
_sei.oid
or ditsr_se.form
in self.
_sei.names:
739 ditsr_rules_ref_list.append(ditsr_id)
740 if ditsr_rules_ref_list:
741 self.
_app.outf.write(
'<dt>Referencing DIT structure rule:</dt>\n<dd>\n%s\n</dd>\n' % (
747SCHEMA_VIEWER_CLASS = {
748 ObjectClass: DisplayObjectClass,
749 AttributeType: DisplayAttributeType,
750 LDAPSyntax: DisplayLDAPSyntax,
751 MatchingRule: DisplayMatchingRule,
752 MatchingRuleUse: DisplayMatchingRuleUse,
753 DITContentRule: DisplayDITContentRule,
754 DITStructureRule: DisplayDITStructureRule,
755 NameForm: DisplayNameForm,
762 'OID or descriptive name of schema element',
765 oid_class_select_html = app.form.field[
'oid_class'].input_html(
'')
766 return app.form_html(
767 'oid',
'Search',
'GET',
769 extrastr=
'\n'.join((oid_input_field_html, oid_class_select_html)),
774 se_list = se_list
or []
775 se_classes = tuple(filter(
None, se_classes
or [])
or SCHEMA_CLASS_MAPPING.values())
784 if app.schema
is None:
785 raise ErrorExit(
'No sub schema available!')
789 for schema_class
in se_classes:
790 oid_dict[schema_class] = []
791 for se_obj
in se_list:
794 except AttributeError:
795 se_id = se_obj.ruleid
797 oid_dict[se_obj.__class__].append(se_id)
799 oid_dict[se_obj.__class__] = [se_id]
801 for schema_class
in se_classes:
802 oid_dict[schema_class] = app.schema.sed[schema_class].keys()
806 for schema_class, schema_elements
in oid_dict.items():
807 if not schema_elements:
809 app.outf.write(
'<h2>%s</h2>\n<p>found %d</p>\n%s\n' % (
810 SCHEMA_VIEWER_CLASS[schema_class].type_desc,
811 len(schema_elements),
815 app.outf.write(SCHEMA_VIEWER_USAGE)
822 def contains_oid(val, oid):
823 return val.__contains__(oid)
825 def startswith_oid(val, oid):
826 return val.startswith(oid)
828 def endswith_oid(val, oid):
829 return val.endswith(oid)
832 oid = app.form.getInputValue(
'oid', [
None])[0]
834 SCHEMA_CLASS_MAPPING[se_name]
835 for se_name
in app.form.getInputValue(
'oid_class', [])
846 if oid.lower().endswith(
';binary'):
850 if oid.startswith(
'*')
and oid.endswith(
'*'):
851 oid_mv = oid[1:-1].lower()
852 cmp_method = contains_oid
853 elif oid.startswith(
'*'):
854 oid_mv = oid[1:].lower()
855 cmp_method = endswith_oid
856 elif oid.endswith(
'*'):
857 oid_mv = oid[:-1].lower()
858 cmp_method = startswith_oid
862 if len(se_classes) == 1
and cmp_method
is None:
865 se_obj = app.schema.get_obj(se_classes[0], oid,
None)
866 if se_obj
is not None:
867 se_list.append(se_obj)
871 if cmp_method
is None:
873 for schema_element_type
in se_classes
or SCHEMA_VIEWER_CLASS.keys():
875 se_obj = app.schema.get_obj(schema_element_type, oid,
None, raise_keyerror=
True)
879 se_list.append(se_obj)
882 for schema_element_type
in se_classes
or SCHEMA_VIEWER_CLASS.keys():
883 for se_obj
in app.schema.sed[schema_element_type].values():
886 except AttributeError:
887 se_id = se_obj.ruleid
888 if cmp_method(se_id.lower(), oid_mv):
890 se_list.append(se_obj)
894 se_names = se_obj.names
895 except AttributeError:
897 for se_name
in se_names
or []:
898 if cmp_method(se_name.lower(), oid_mv):
899 se_list.append(se_obj)
907 '<h1>Schema elements</h1><p class="ErrorMessage">'
908 'Name or OID not found in schema!</p><p>%s</p>'
912 main_div_id=
'Message',
924 if se_obj.__class__
not in SCHEMA_VIEWER_CLASS:
925 raise ErrorExit(
'No viewer for this type of schema element!')
926 schema_viewer = SCHEMA_VIEWER_CLASS[se_obj.__class__](app, se_obj)
927 schema_viewer.display()
def __init__(self, app, se)
def __init__(self, app, se)
def __init__(self, app, se_obj)
def top_section(app, title, main_menu_list, context_menu_list=None, main_div_id='Message')
def schema_anchors(app, se_names, se_class)
def schema_tree_html(app, schema, se_class, se_tree, se_oid, level)
def oid_input_form(app, oid=None)
def schema_context_menu(app)
def display_schema_elements(app, se_classes, se_list)
def w2l_schema_viewer(app)
def schema_anchor(app, se_nameoroid, se_class, name_template='{name}\n{anchor}', link_text=None)
def schema_link_text(se_obj)