Release govoplan-docs v0.1.23: unify help discovery and batch semantic reads
Module Package Release / publish-packages (push) Successful in 12s
Module Package Release / publish-packages (push) Successful in 12s
This commit is contained in:
@@ -2,4 +2,4 @@
|
||||
|
||||
__all__ = ["__version__"]
|
||||
|
||||
__version__ = "0.1.22"
|
||||
__version__ = "0.1.23"
|
||||
|
||||
@@ -40,6 +40,7 @@ from govoplan_docs.backend.sources import (
|
||||
)
|
||||
from govoplan_docs.backend.semantic_service import (
|
||||
list_semantic_entries,
|
||||
prefetch_semantic_revisions,
|
||||
select_locale_entries,
|
||||
semantic_entry_payload,
|
||||
)
|
||||
@@ -830,6 +831,11 @@ def _classify_documentation(
|
||||
) -> dict[str, list[dict[str, Any]]]:
|
||||
layers: dict[str, list[dict[str, Any]]] = {"always": [], "configured": [], "available": [], "evidence": []}
|
||||
installed = {manifest.id for manifest in registry.manifests()}
|
||||
visible_area_modules = frozenset(
|
||||
str(item["module_id"])
|
||||
for item in _route_items(registry.manifests(), principal)
|
||||
if item["visible"]
|
||||
) if documentation_type == "user" else frozenset(installed)
|
||||
visible_runtime_paths = frozenset([
|
||||
"/settings", # Authenticated shell route, not contributed by a module manifest.
|
||||
*(
|
||||
@@ -909,6 +915,7 @@ def _classify_documentation(
|
||||
visible_runtime_paths=visible_runtime_paths,
|
||||
configuration=configuration,
|
||||
resolved_version=resolved_version,
|
||||
visible_area_modules=visible_area_modules,
|
||||
))
|
||||
return layers
|
||||
|
||||
@@ -1011,6 +1018,7 @@ def _semantic_documentation_topics(
|
||||
list_semantic_entries(session, principal),
|
||||
locale=locale,
|
||||
)
|
||||
revisions = prefetch_semantic_revisions(session, principal, entries=entries, editor=False)
|
||||
topics: list[DocumentationTopic] = []
|
||||
for entry in entries:
|
||||
payload = semantic_entry_payload(
|
||||
@@ -1020,6 +1028,7 @@ def _semantic_documentation_topics(
|
||||
entry=entry,
|
||||
editor=False,
|
||||
requested_locale=locale,
|
||||
revisions=revisions,
|
||||
)
|
||||
if payload is None:
|
||||
continue
|
||||
@@ -1430,6 +1439,7 @@ def _documentation_topic_payload(
|
||||
visible_runtime_paths: frozenset[str],
|
||||
configuration: Mapping[str, DocumentationConfigurationDecision],
|
||||
resolved_version: str,
|
||||
visible_area_modules: frozenset[str] = frozenset(),
|
||||
) -> dict[str, Any]:
|
||||
module_id = topic.source_module_id or source_module_id
|
||||
translation_locale, translation = _translation_for_locale(topic, locale)
|
||||
@@ -1484,6 +1494,16 @@ def _documentation_topic_payload(
|
||||
"conditions": [_documentation_condition_payload(condition) for condition in topic.conditions],
|
||||
"links": [_documentation_link_payload(link) for link in topic.links],
|
||||
"related_modules": list(topic.related_modules),
|
||||
"area_module_ids": sorted({
|
||||
module_id,
|
||||
*(
|
||||
area for area in (
|
||||
*topic.related_modules,
|
||||
*_bounded_string_list(localized_metadata.get("areas"), maximum_items=32, maximum_length=255),
|
||||
)
|
||||
if area in visible_area_modules
|
||||
),
|
||||
}),
|
||||
"unlocks": list(topic.unlocks),
|
||||
"configuration_keys": sorted({*topic.configuration_keys, *(key for condition in topic.conditions for key in condition.configuration_keys)}),
|
||||
"configuration_states": [
|
||||
@@ -1497,6 +1517,7 @@ def _documentation_topic_payload(
|
||||
return {
|
||||
"id": payload["id"],
|
||||
"source_module_id": payload["source_module_id"],
|
||||
"area_module_ids": payload["area_module_ids"],
|
||||
"kind": payload["kind"],
|
||||
"anchor_id": payload["anchor_id"],
|
||||
"title": payload["title"],
|
||||
@@ -1555,6 +1576,9 @@ def _user_topic_metadata(kind: str, metadata: Mapping[str, Any]) -> dict[str, An
|
||||
value = _bounded_string(metadata.get(key), maximum=255)
|
||||
if value:
|
||||
projected[key] = value
|
||||
tags = _bounded_string_list(metadata.get("tags"), maximum_items=32, maximum_length=80)
|
||||
if tags:
|
||||
projected["tags"] = tags
|
||||
if kind == "reference" and isinstance(metadata.get("fields"), list):
|
||||
fields = [_user_field_metadata(item) for item in metadata["fields"][:64] if isinstance(item, Mapping)]
|
||||
if fields:
|
||||
|
||||
@@ -43,6 +43,7 @@ from govoplan_docs.backend.semantic_service import (
|
||||
create_semantic_entry,
|
||||
get_semantic_entry,
|
||||
list_semantic_entries,
|
||||
prefetch_semantic_revisions,
|
||||
publication_policy,
|
||||
publish_semantic_entry,
|
||||
retire_semantic_entry,
|
||||
@@ -167,6 +168,7 @@ def list_entries(
|
||||
subject_kind=subject_kind,
|
||||
)
|
||||
selected = entries if editor else select_locale_entries(entries, locale=locale)
|
||||
revisions = prefetch_semantic_revisions(session, principal, entries=selected, editor=editor)
|
||||
items = [
|
||||
item
|
||||
for entry in selected
|
||||
@@ -178,6 +180,7 @@ def list_entries(
|
||||
entry=entry,
|
||||
editor=editor,
|
||||
requested_locale=locale,
|
||||
revisions=revisions,
|
||||
)
|
||||
)
|
||||
is not None
|
||||
|
||||
@@ -131,7 +131,7 @@ def _dsar_provider(_context: ModuleContext) -> DocsDsarProvider:
|
||||
manifest = ModuleManifest(
|
||||
id="docs",
|
||||
name="Docs",
|
||||
version="0.1.22",
|
||||
version="0.1.23",
|
||||
required_capabilities=(
|
||||
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
|
||||
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
|
||||
@@ -302,6 +302,46 @@ manifest = ModuleManifest(
|
||||
),
|
||||
),
|
||||
documentation=(
|
||||
DocumentationTopic(
|
||||
id="docs.workflow.find-help",
|
||||
title="Find help by area and keyword",
|
||||
summary="Search visible help topics and use area and keyword tags to narrow the results.",
|
||||
body=(
|
||||
"Enter words from your question in Search help topics. Titles, summaries, topic text, area names, and contributed tags are searched together. "
|
||||
"Areas and tags is a multi-selection dropdown: selected tags match any of those tags; Select all removes the restriction and Clear all selects no topics. "
|
||||
"Topics by area includes all visible guidance associated with an area, including guidance contributed by another module. "
|
||||
"A topic can appear in several branches; only the occurrence you select is highlighted, and expanding it does not expand its other occurrences. "
|
||||
"Search results show each topic once. Choosing a result clears the filters and opens the topic. Search and tags only narrow the documentation already authorized for your role. "
|
||||
"Administrators and module authors contribute public keywords in DocumentationTopic metadata.tags and optional module-area IDs in metadata.areas; the source module and authorized related_modules also supply area tags. "
|
||||
"Use stable module IDs for areas and readable keywords for tags; do not put credentials, private configuration, or hidden capability names in public search tags. "
|
||||
"User area facets omit related modules without a visible route; administrative documentation still requires its separate permission."
|
||||
),
|
||||
layer="always",
|
||||
documentation_types=("admin", "user"),
|
||||
order=9,
|
||||
conditions=(DocumentationCondition(required_scopes=(DOCS_READ_SCOPE,)),),
|
||||
translations={
|
||||
"de": {
|
||||
"title": "Hilfe nach Bereich und Stichwort finden",
|
||||
"summary": "Sichtbare Hilfethemen durchsuchen und Ergebnisse mit Bereichen und Schlagwörtern eingrenzen.",
|
||||
"body": (
|
||||
"Geben Sie unter Hilfethemen suchen Wörter aus Ihrer Frage ein. Titel, Zusammenfassungen, Thementexte, Bereichsnamen und beigetragene Schlagwörter werden gemeinsam durchsucht. "
|
||||
"Bereiche und Schlagwörter ist eine Auswahlliste mit Mehrfachauswahl: Ein ausgewähltes Schlagwort genügt für einen Treffer. Alle auswählen entfernt die Einschränkung, Auswahl aufheben wählt keine Themen. "
|
||||
"Themen nach Bereich enthält sämtliche sichtbaren Hinweise eines Bereichs, auch Beiträge anderer Module. "
|
||||
"Ein Thema kann in mehreren Zweigen erscheinen; nur die angeklickte Stelle wird hervorgehoben, und das Aufklappen öffnet nicht zugleich die anderen Vorkommen. "
|
||||
"Suchergebnisse zeigen jedes Thema einmal. Die Auswahl eines Ergebnisses setzt die Filter zurück und öffnet das Thema. Suche und Schlagwörter grenzen ausschließlich die bereits für Ihre Rolle freigegebene Dokumentation ein. "
|
||||
"Administratoren und Modulautoren hinterlegen öffentliche Stichwörter in DocumentationTopic metadata.tags und optionale Modul-Bereichskennungen in metadata.areas; das Quellmodul und berechtigte related_modules liefern ebenfalls Bereichsschlagwörter. "
|
||||
"Verwenden Sie stabile Modulkennungen für Bereiche und lesbare Stichwörter für Schlagwörter. Zugangsdaten, private Konfiguration und Namen verborgener Fähigkeiten gehören nicht in öffentliche Suchschlagwörter. "
|
||||
"Bereichsfilter der Benutzerdokumentation zeigen keine zugeordneten Module ohne sichtbare Route. Administrative Dokumentation benötigt weiterhin ihre gesonderte Berechtigung."
|
||||
),
|
||||
},
|
||||
},
|
||||
metadata={
|
||||
"kind": "workflow",
|
||||
"tags": ["Help", "Hilfe", "Search", "Suche", "Tags", "Schlagwörter"],
|
||||
"help_contexts": ["docs.help-center.search"],
|
||||
},
|
||||
),
|
||||
DocumentationTopic(
|
||||
id="docs.semantic-documentation",
|
||||
title="Tenant semantic documentation",
|
||||
@@ -311,7 +351,8 @@ manifest = ModuleManifest(
|
||||
"Every save creates an immutable revision. Tenant policy chooses direct publication or an independent reviewer. "
|
||||
"Published content remains subject to the subject's current authorization, the documentation audience and classification, tenant isolation, and locale selection. "
|
||||
"Changed, missing, superseded, or temporarily unavailable subjects are shown explicitly; direct links, contextual help, search, caches, and tenant exports apply the same read-time authorization. "
|
||||
"Retirement and supersession preserve history. Generic public documentation exports never include tenant semantic entries; administrators use the separately authorized tenant export."
|
||||
"Retirement and supersession preserve history. Generic public documentation exports never include tenant semantic entries; administrators use the separately authorized tenant export. "
|
||||
"Collection, configured-context, and search-authorization reads batch the required revisions in request-local groups of at most 400 identifiers instead of fetching revisions once per entry. Read-only views load published content, not pending draft bodies. Each revision must belong to the same tenant and entry, and a published pointer must reference a published revision; inconsistent references fail closed. Audience denial avoids unnecessary subject-provider work, while allowed results still require the owning subject's current authorization. No cross-request permission cache is introduced; complete tenant exports and full history remain separately authorized operations."
|
||||
),
|
||||
layer="always",
|
||||
documentation_types=("admin", "user"),
|
||||
@@ -351,7 +392,12 @@ manifest = ModuleManifest(
|
||||
"der Mandantentrennung und der Sprachauswahl. Geänderte, fehlende, abgelöste oder vorübergehend nicht verfügbare Fachobjekte "
|
||||
"werden ausdrücklich gekennzeichnet; Direktlinks, Kontexthilfe, Suche, Zwischenspeicher und Mandantenexporte wenden dieselbe "
|
||||
"Berechtigungsprüfung beim Lesen an. Stilllegung und Ablösung bewahren die Historie. Allgemeine öffentliche Dokumentationsexporte "
|
||||
"enthalten niemals semantische Mandanteneinträge; für diese steht der getrennt berechtigte Mandantenexport bereit."
|
||||
"enthalten niemals semantische Mandanteneinträge; für diese steht der getrennt berechtigte Mandantenexport bereit. "
|
||||
"Listen, Konfigurationskontext und Suchberechtigungsprüfung laden benötigte Revisionen anfragebezogen in Gruppen von höchstens 400 Kennungen statt einzeln je Eintrag. "
|
||||
"Nur lesbare Ansichten laden veröffentlichte Inhalte und keine offenen Entwurfstexte. Jede Revision muss zum selben Mandanten und Eintrag gehören; "
|
||||
"ein Veröffentlichungsverweis muss auf eine veröffentlichte Revision zeigen. Widersprüchliche Verweise werden abgewiesen. "
|
||||
"Bei einer nicht berechtigten Zielgruppe entfällt unnötige Arbeit des Fachobjekt-Providers; zulässige Ergebnisse erfordern weiterhin dessen aktuelle Berechtigungsprüfung. "
|
||||
"Es entsteht kein anfrageübergreifender Berechtigungszwischenspeicher. Vollständiger Mandantenexport und Historie bleiben getrennt berechtigte Vorgänge."
|
||||
),
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from collections.abc import Mapping, Sequence
|
||||
from urllib.parse import quote
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import and_, func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.auth import ApiPrincipal
|
||||
@@ -23,7 +23,11 @@ from govoplan_docs.backend.db.models import (
|
||||
SemanticDocumentationEntry,
|
||||
SemanticDocumentationRevision,
|
||||
)
|
||||
from govoplan_docs.backend.semantic_service import semantic_entry_payload
|
||||
from govoplan_docs.backend.semantic_service import (
|
||||
prefetch_semantic_revisions,
|
||||
published_revision,
|
||||
semantic_entry_payload,
|
||||
)
|
||||
|
||||
|
||||
PROVIDER_ID = "docs.semantic_documentation"
|
||||
@@ -58,8 +62,12 @@ class SemanticDocumentationSearchSource:
|
||||
select(SemanticDocumentationEntry, SemanticDocumentationRevision)
|
||||
.join(
|
||||
SemanticDocumentationRevision,
|
||||
SemanticDocumentationRevision.id
|
||||
== SemanticDocumentationEntry.published_revision_id,
|
||||
and_(
|
||||
SemanticDocumentationRevision.id == SemanticDocumentationEntry.published_revision_id,
|
||||
SemanticDocumentationRevision.entry_id == SemanticDocumentationEntry.id,
|
||||
SemanticDocumentationRevision.tenant_id == SemanticDocumentationEntry.tenant_id,
|
||||
SemanticDocumentationRevision.lifecycle_state == "published",
|
||||
),
|
||||
)
|
||||
.where(
|
||||
SemanticDocumentationEntry.tenant_id == request.tenant_id,
|
||||
@@ -105,15 +113,32 @@ class SemanticDocumentationSearchSource:
|
||||
if not isinstance(principal, ApiPrincipal) or not principal.has(DOCS_READ_SCOPE):
|
||||
return decisions
|
||||
db = _session(session)
|
||||
for item in requests:
|
||||
reference = item.reference
|
||||
valid_requests = [
|
||||
item
|
||||
for item in requests
|
||||
if (
|
||||
reference.tenant_id != principal.tenant_id
|
||||
or reference.module_id != "docs"
|
||||
or reference.resource_type != RESOURCE_TYPE
|
||||
):
|
||||
continue
|
||||
entry = db.get(SemanticDocumentationEntry, reference.resource_id)
|
||||
item.reference.tenant_id == principal.tenant_id
|
||||
and item.reference.module_id == "docs"
|
||||
and item.reference.resource_type == RESOURCE_TYPE
|
||||
)
|
||||
]
|
||||
ids = sorted({item.reference.resource_id for item in valid_requests})
|
||||
entries: dict[str, SemanticDocumentationEntry] = {}
|
||||
for offset in range(0, len(ids), 400):
|
||||
rows = db.scalars(
|
||||
select(SemanticDocumentationEntry).where(
|
||||
SemanticDocumentationEntry.id.in_(ids[offset:offset + 400]),
|
||||
SemanticDocumentationEntry.tenant_id == principal.tenant_id,
|
||||
SemanticDocumentationEntry.lifecycle_state.in_(("draft", "published")),
|
||||
)
|
||||
)
|
||||
entries.update((entry.id, entry) for entry in rows)
|
||||
revisions = prefetch_semantic_revisions(
|
||||
db, principal, entries=tuple(entries.values()), editor=False,
|
||||
)
|
||||
for item in valid_requests:
|
||||
reference = item.reference
|
||||
entry = entries.get(reference.resource_id)
|
||||
if (
|
||||
entry is None
|
||||
or entry.tenant_id != principal.tenant_id
|
||||
@@ -127,6 +152,7 @@ class SemanticDocumentationSearchSource:
|
||||
principal,
|
||||
entry=entry,
|
||||
editor=False,
|
||||
revisions=revisions,
|
||||
)
|
||||
decisions[reference.key] = bool(
|
||||
payload
|
||||
@@ -153,8 +179,12 @@ class SemanticDocumentationSearchSource:
|
||||
db = _session(session)
|
||||
entry = db.get(SemanticDocumentationEntry, event.resource.id)
|
||||
revision = (
|
||||
db.get(SemanticDocumentationRevision, entry.published_revision_id)
|
||||
if entry is not None and entry.published_revision_id
|
||||
published_revision(db, entry)
|
||||
if (
|
||||
entry is not None
|
||||
and entry.tenant_id == event.tenant.id
|
||||
and entry.lifecycle_state in {"draft", "published"}
|
||||
)
|
||||
else None
|
||||
)
|
||||
visible = bool(
|
||||
@@ -162,6 +192,9 @@ class SemanticDocumentationSearchSource:
|
||||
and entry.tenant_id == event.tenant.id
|
||||
and entry.lifecycle_state in {"draft", "published"}
|
||||
and revision is not None
|
||||
and revision.entry_id == entry.id
|
||||
and revision.tenant_id == entry.tenant_id
|
||||
and revision.lifecycle_state == "published"
|
||||
)
|
||||
cursor = event.event_id
|
||||
document = _document(entry, revision, change_cursor=cursor) if visible else None
|
||||
|
||||
@@ -396,13 +396,21 @@ def semantic_entry_history(
|
||||
def current_revision(
|
||||
session: Session,
|
||||
entry: SemanticDocumentationEntry,
|
||||
*,
|
||||
revisions: Mapping[str, SemanticDocumentationRevision] | None = None,
|
||||
) -> SemanticDocumentationRevision:
|
||||
revision = (
|
||||
session.get(SemanticDocumentationRevision, entry.current_revision_id)
|
||||
if entry.current_revision_id
|
||||
else None
|
||||
)
|
||||
if revision is None or revision.entry_id != entry.id:
|
||||
revision = None
|
||||
if entry.current_revision_id:
|
||||
revision = (
|
||||
revisions.get(entry.current_revision_id)
|
||||
if revisions is not None
|
||||
else session.get(SemanticDocumentationRevision, entry.current_revision_id)
|
||||
)
|
||||
if (
|
||||
revision is None
|
||||
or revision.entry_id != entry.id
|
||||
or revision.tenant_id != entry.tenant_id
|
||||
):
|
||||
raise SemanticDocumentationError(
|
||||
"Semantic documentation current revision is unavailable."
|
||||
)
|
||||
@@ -412,14 +420,60 @@ def current_revision(
|
||||
def published_revision(
|
||||
session: Session,
|
||||
entry: SemanticDocumentationEntry,
|
||||
*,
|
||||
revisions: Mapping[str, SemanticDocumentationRevision] | None = None,
|
||||
) -> SemanticDocumentationRevision | None:
|
||||
if not entry.published_revision_id:
|
||||
return None
|
||||
revision = session.get(
|
||||
SemanticDocumentationRevision,
|
||||
entry.published_revision_id,
|
||||
revision = (
|
||||
revisions.get(entry.published_revision_id)
|
||||
if revisions is not None
|
||||
else session.get(SemanticDocumentationRevision, entry.published_revision_id)
|
||||
)
|
||||
return revision if revision is not None and revision.entry_id == entry.id else None
|
||||
if (
|
||||
revision is None
|
||||
or revision.entry_id != entry.id
|
||||
or revision.tenant_id != entry.tenant_id
|
||||
or revision.lifecycle_state != "published"
|
||||
):
|
||||
return None
|
||||
return revision
|
||||
|
||||
|
||||
def prefetch_semantic_revisions(
|
||||
session: Session,
|
||||
principal: object,
|
||||
*,
|
||||
entries: Sequence[SemanticDocumentationEntry],
|
||||
editor: bool,
|
||||
) -> dict[str, SemanticDocumentationRevision]:
|
||||
"""Load only the revisions this request may project, in bounded SQL batches.
|
||||
|
||||
This is request-local data loading, never an authorization cache. Payload
|
||||
projection still validates each entry/revision pair and its owner subject.
|
||||
Readers have no reason to load the bodies of pending unpublished drafts.
|
||||
"""
|
||||
tenant_id = _principal_tenant_id(principal)
|
||||
revision_ids = sorted({
|
||||
revision_id
|
||||
for entry in entries
|
||||
if entry.tenant_id == tenant_id
|
||||
for revision_id in (
|
||||
entry.published_revision_id,
|
||||
entry.current_revision_id if editor else None,
|
||||
)
|
||||
if revision_id
|
||||
})
|
||||
revisions: dict[str, SemanticDocumentationRevision] = {}
|
||||
for offset in range(0, len(revision_ids), 400):
|
||||
rows = session.query(SemanticDocumentationRevision).filter(
|
||||
SemanticDocumentationRevision.tenant_id == tenant_id,
|
||||
SemanticDocumentationRevision.id.in_(revision_ids[offset:offset + 400]),
|
||||
)
|
||||
if not editor:
|
||||
rows = rows.filter(SemanticDocumentationRevision.lifecycle_state == "published")
|
||||
revisions.update((revision.id, revision) for revision in rows)
|
||||
return revisions
|
||||
|
||||
|
||||
def entry_subject_reference(
|
||||
@@ -487,12 +541,16 @@ def semantic_entry_payload(
|
||||
entry: SemanticDocumentationEntry,
|
||||
editor: bool,
|
||||
requested_locale: str | None = None,
|
||||
revisions: Mapping[str, SemanticDocumentationRevision] | None = None,
|
||||
) -> dict[str, object] | None:
|
||||
current = current_revision(session, entry)
|
||||
published = published_revision(session, entry)
|
||||
selected = current if editor else published
|
||||
if entry.tenant_id != _principal_tenant_id(principal):
|
||||
return None
|
||||
published = published_revision(session, entry, revisions=revisions)
|
||||
selected = current_revision(session, entry, revisions=revisions) if editor else published
|
||||
if selected is None:
|
||||
return None
|
||||
if not content_visible_to_principal(selected.content, principal):
|
||||
return None
|
||||
resolution = resolve_entry_subject(
|
||||
session,
|
||||
registry,
|
||||
@@ -505,8 +563,6 @@ def semantic_entry_payload(
|
||||
unavailable = resolution.availability == "temporarily_unavailable"
|
||||
if unavailable and not editor:
|
||||
return None
|
||||
if not content_visible_to_principal(selected.content, principal):
|
||||
return None
|
||||
subject = resolution.subject
|
||||
required_scopes = subject.required_scopes if subject is not None else ()
|
||||
if any(not _principal_has(principal, scope) for scope in required_scopes):
|
||||
@@ -523,7 +579,7 @@ def semantic_entry_payload(
|
||||
"lifecycle_state": entry.lifecycle_state,
|
||||
"effective_state": selected.lifecycle_state,
|
||||
"pending_draft": bool(
|
||||
published is not None and current.id != published.id
|
||||
published is not None and entry.current_revision_id != published.id
|
||||
),
|
||||
"current_revision": entry.current_revision,
|
||||
"selected_revision": selected.revision,
|
||||
@@ -965,6 +1021,7 @@ __all__ = [
|
||||
"entry_subject_reference",
|
||||
"get_semantic_entry",
|
||||
"list_semantic_entries",
|
||||
"prefetch_semantic_revisions",
|
||||
"publication_policy",
|
||||
"publish_semantic_entry",
|
||||
"published_revision",
|
||||
|
||||
Reference in New Issue
Block a user