Files
shopdb-flask/plugins/guides/plugin.py
cproudlock bd0e9695e5 Document the guides API, and move Guides beside Slides
The repo already enforces that every served route appears in
docs/api-inventory.json, and the guides endpoints did not - so the parity test
was failing. They are now listed with auth, params and a working example each,
beside knowledgebase because the two get read for the same reason.

docs/GUIDES.md covers what the inventory cannot: the block schema and why it is
blocks rather than HTML, the id rule that makes a tick rememberable, what the
server refuses and why validation happens at the door, the working-copy /
revision / published-pointer split that stops a live procedure changing under
somebody following it, and a worked example of creating and publishing a guide
from a script.

It also states plainly what does not exist yet - no importer, orphaned images
never pruned - and what converting existing documents will actually be like:
structured HTML and Markdown map onto block types nearly one for one, while a
plain-text runbook lands as paragraphs and needs marking up by hand afterwards.
Generated ids have to stay stable across re-imports, or every re-import resets
readers' progress.

Guides moves to position 8 in the sidebar, beside Slides, rather than down among
the reference lists in Information. A guide is something somebody goes and does
something with, not a catalogue they look things up in.
2026-08-26 12:39:42 -04:00

93 lines
3.4 KiB
Python

"""Guides plugin.
Step-by-step procedures written and read inside ShopDB: images, working
checklists, dark mode and printable output, with drafts and revision history.
Distinct from the knowledgebase plugin, which is a LINK REGISTRY - a
description, a URL and keywords pointing at a document held somewhere else.
This holds the document. They share a tag vocabulary on purpose, so one search
can cover both.
"""
import json
import logging
from pathlib import Path
from typing import List, Dict, Optional, Type
from flask import Flask, Blueprint
from shopdb.plugins.base import BasePlugin, PluginMeta
from .models import Guide, GuideRevision, GuideImage
from .api import guides_bp
logger = logging.getLogger(__name__)
class GuidesPlugin(BasePlugin):
def __init__(self):
self._manifest = self._load_manifest()
def _load_manifest(self) -> Dict:
manifest_path = Path(__file__).parent / 'manifest.json'
if manifest_path.exists():
with open(manifest_path, 'r') as fh:
return json.load(fh)
return {}
@property
def meta(self) -> PluginMeta:
return PluginMeta(
name=self._manifest.get('name', 'guides'),
version=self._manifest.get('version', '1.0.0'),
description=self._manifest.get('description', 'Step-by-step guides'),
author=self._manifest.get('author', 'ShopDB Team'),
dependencies=self._manifest.get('dependencies', []),
core_version=self._manifest.get('core_version', '>=0.1.0,<1.0.0'),
api_prefix=self._manifest.get('api_prefix', '/api/guides'),
)
def get_blueprint(self) -> Optional[Blueprint]:
return guides_bp
def get_models(self) -> List[Type]:
return [Guide, GuideRevision, GuideImage]
def get_permissions(self) -> List:
"""Five permissions, and the split between the last two is the point.
publish is separate from edit so a technician can write up a procedure
while somebody else decides it is fit for the floor. Without that split,
anyone who can fix a typo can also put an unreviewed procedure in front
of an operator.
create without edit is a working combination on purpose: an author may
edit their OWN guide while it is unpublished, and needs guides.edit once
other people are following it (see _mayedit in the API).
"""
return [
('guides.view', 'View guides', 'guides'),
('guides.create', 'Create guides and edit your own drafts', 'guides'),
('guides.edit', 'Edit any guide, including published ones', 'guides'),
('guides.publish', 'Publish or withdraw a guide', 'guides'),
('guides.delete', 'Delete guides', 'guides'),
]
def get_navigation_items(self) -> List[Dict]:
# Position 8, beside Slides (7), rather than down in the Information
# section with the reference lists. A guide is something you go and DO
# something with, like a slide deck you put on a screen - not a
# catalogue you look things up in.
return [
{
'name': 'Guides',
'icon': 'book-open',
'route': '/guides',
'position': 8,
},
]
def init_app(self, app: Flask, db_instance) -> None:
logger.info('Guides plugin initialized (v%s)', self.meta.version)