Module control.viewers
Expand source code Browse git
import collections
import requests
from textwrap import dedent
from .generic import AttrDict, attResolve
from .files import listDirs
class Viewers:
def __init__(self, Settings, Messages, Mongo):
"""Knowledge of the installed 3D viewers.
This class knows which (versions of) viewers are installed,
and has the methods to invoke them.
It is instantiated by a singleton object.
Parameters
----------
Settings: AttrDict
App-wide configuration data obtained from
`control.config.Config.Settings`.
Messages: object
Singleton instance of `control.messages.Messages`.
"""
self.Settings = Settings
self.Mongo = Mongo
self.Messages = Messages
Messages.debugAdd(self)
self.viewers = Settings.viewers
self.viewerActions = Settings.viewerActions
self.viewerDefault = Settings.viewerDefault
def addAuth(self, Auth):
"""Give this object a handle to the Auth object.
The Viewers and Auth objects need each other, so one of them must
be given the handle to the other after initialization.
"""
self.Auth = Auth
def getVersions(self, viewer):
"""Reads the available versions of a viewer from the file system.
Keep in mind that the set of installed versions of a viewer may change
due to user actions. When there are multiple web workers active, they
will have different instantiations of the viewer class. So whenever
you need the set of versions, invoke this method to get it freshly.
Parameters
----------
viewer: string
The viewer for which we want to retrieve the versions
Returns
-------
tuple
The ordered list of versions, most recent first.
"""
Settings = self.Settings
versionKey = Settings.versionKey
dataDir = Settings.dataDir
viewerDir = f"{dataDir}/viewers"
viewerPath = f"{viewerDir}/{viewer}"
return list(reversed(sorted(listDirs(viewerPath), key=versionKey)))
def check(self, viewer, version):
"""Checks whether a viewer version exists.
Given a viewer and a version, it is looked up whether the code
is present.
If not, reasonable defaults returned instead by default.
Parameters
----------
viewer: string
The viewer in question.
version: string
The version of the viewer in question.
Returns
-------
string | void
The version is returned unmodified if that viewer
version is supported.
If the viewer is supported, but not the version, the default version
of that viewer is taken, if there is a default version,
otherwise the latest supported version.
If the viewer is not supported, None is returned.
"""
viewers = self.viewers
if viewer not in viewers:
return None
versions = self.getVersions(viewer)
defaultVersion = versions[0] if len(versions) else ""
if version not in versions:
version = defaultVersion
return version
def getViewInfo(self, edition):
"""Gets viewer-related info that an edition is made with.
Parameters
----------
edition: AttrDict
The edition record.
Returns
-------
tuple of string
* The name of the viewer
* The name of the scene
"""
viewerDefault = self.viewerDefault
editionId = edition._id
if editionId is None:
return (viewerDefault, None)
editionSettings = edition.settings or AttrDict()
authorTool = editionSettings.authorTool or AttrDict()
viewer = authorTool.name or viewerDefault
sceneFile = authorTool.sceneFile
return (viewer, sceneFile)
def getUsedVersions(self, viewer):
"""Produces a list of used versions of a viewer.
These versions have been used to create existing editions.
These versions may no longer exists in the system (which is undesirable).
Parameters
----------
viewer: string
The viewer for which we want to retrieve the versions
Returns
-------
dict
Keyed by version and valued by the number of editions that have
used this version while they were being created.
"""
Mongo = self.Mongo
viewers = self.viewers
viewerDefault = self.viewerDefault
if viewer not in viewers:
return ()
usedVersions = collections.Counter()
for edition in Mongo.getList("edition", {}):
editionSettings = edition.settings or AttrDict()
authorTool = editionSettings.authorTool or AttrDict()
thisViewer = authorTool.name or viewerDefault
if thisViewer != viewer:
continue
version = authorTool.version
usedVersions[version] += 1
return usedVersions
def getReleasedVoyagerVersions(self, contactGithub):
"""Get all installable versions of the Voyager viewer.
We fetch a list of releases from the github repo of the Voyager,
together with the urls to their zip balls.
The fetching is done using the GitHub API. That has a rate limit of 60 requests
an hour. In order to overcome that, we store the results in MongoDB.
Whenever we bump into the rate limit, we fish the data from MongoDb instead.
If the fetching fails, the result may not be complete and a warning
is issued.
Parameters
----------
contactGithub: boolean
If True, the list of installable voyager versions will be retrieved
from Github.
Otherwise, a cached version will be retrieved from MongoDB, if such a
version exists, otherwise Github will be contacted.
Returns
-------
list, dict
The list are the messages issued.
The dict is keyed by version and valued by the url to the zip file that
contains that version.
"""
Mongo = self.Mongo
Messages = self.Messages
messages = []
if not contactGithub:
results = Mongo.getRecord("voyager", {}).releases or {}
nResults = len(results)
if nResults:
messages.append(
(
"info",
f"Fetching from cache succeeded; giving {nResults} versions",
)
)
return (messages, results)
messages.append(
(
"info",
"No versions in cache, will contact Github",
)
)
BACKEND = "https://api.github.com/repos/{org}/{repo}/releases"
ORG = "Smithsonian"
REPO = "dpo-voyager"
HEADERS = dict(Accept="application/vnd.github+json")
fetchUrl = BACKEND.format(org=ORG, repo=REPO)
url = fetchUrl
rawResults = []
good = True
eMsg = ""
exceeded = False
try:
while True:
response = requests.get(url, headers=HEADERS)
if not response.ok:
try:
msg = response.json().get("message", "")
except Exception:
msg = ""
if "rate limit exceeded" in msg:
exceeded = True
break
good = False
break
theseResults = response.json()
rawResults.extend(theseResults)
links = response.links
nxt = links.get("next", None)
if not nxt:
break
nxtUrl = nxt.get("url", None)
if not nxtUrl:
break
url = nxtUrl
except Exception as e:
good = False
eMsg = str(e)
if exceeded or not good:
results = Mongo.getRecord("voyager", {}).releases or {}
nResults = len(results)
statusRep = "ok" if good else f"XX ({eMsg})"
cause = "temporarily blocked" if exceeded else "failed"
if nResults == 0:
messages.append(
(
"error",
f"Fetching from GitHub {cause}; no previously stored versions",
)
)
Messages.error(
logmsg=(
f"Fetching from Github: status={statusRep}; "
f"rate limit exceeded={exceeded}; "
"no previously stored versions"
),
)
else:
messages.append(
(
"warning",
f"Fetching from GitHub {cause}; "
f"will use {nResults} previously stored versions",
)
)
Messages.warning(
logmsg=(
f"Fetching from Github: status={statusRep}; "
f"rate limit exceeded={exceeded}; "
f"there are {nResults} previously stored versions"
),
)
else:
results = {}
for r in rawResults:
assets = r.get("assets", None)
if assets is None:
continue
found = False
assetUrl = None
for asset in assets:
name = asset["name"]
tp = asset["content_type"]
if "zip" not in tp or "zip" not in name or "voyager" not in name:
continue
found = True
assetUrl = asset["browser_download_url"]
break
if not found:
continue
results[r["tag_name"].lstrip("v")] = assetUrl
nResults = len(results)
messages.append(
(
"good",
f"Fetching from GitHub succeeded; giving {nResults} versions",
)
)
Messages.good(
logmsg=(
f"Fetching from Github succeeded; there are {nResults} versions"
),
)
Mongo.clearTable("voyager")
Mongo.insertRecord("voyager", dict(releases=results))
return (messages, results)
def getVoyagerVersionTable(self, contactGithub):
"""Produces a sorted table of Voyager versions with information per version.
The fields of information are:
* is the version released, and if so, what is the zip url
* is the version installed
* is the version used and if so, by how many editions
Parameters
----------
contactGithub: boolean
If True, the list of installable voyager versions will be retrieved
from Github.
Otherwise, a cached version will be retrieved from MongoDB, if such a
version exists, otherwise Github will be contacted.
Returns
-------
list
"""
Settings = self.Settings
versionKey = Settings.versionKey
usedVersions = self.getUsedVersions("voyager")
usedVersionSet = set(usedVersions)
installedVersionSet = set(self.getVersions("voyager"))
messages, releasedVersions = self.getReleasedVoyagerVersions(contactGithub)
releasedVersionSet = set(releasedVersions)
allVersions = reversed(
sorted(
usedVersionSet | installedVersionSet | releasedVersionSet,
key=versionKey,
)
)
rows = []
for version in allVersions:
zipUrl = releasedVersions.get(version, None)
isInstalled = version in installedVersionSet
nEditions = usedVersions.get(version, 0)
rows.append((version, zipUrl, isInstalled, nEditions))
return (messages, rows)
def getFrame(
self, edition, actions, viewer, versionActive, actionActive, sceneExists
):
"""Produces a set of buttons to launch 3D viewers for a scene.
Make sure that if there is no scene file present, no viewer will be opened.
Parameters
----------
edition: AttrDict
The edition in question.
actions: iterable of string
The actions for which we have to create buttons.
Typically `read` and possibly also `update`.
Actions that are not recognized as viewer actions
will be filtered out, such as `create` and `delete`.
viewer: string
The viewer in which the scene is currently loaded.
versionActive: string | void
The version of the viewer in which the scene is currently loaded,
if any, otherwise None
actionActive: string | void
The mode in which the scene is currently loaded in the viewer
(`read` or `update`),
if any, otherwise None
sceneExists: boolean
Whether the scene file exists.
Returns
-------
string
The HTML that represents the buttons.
"""
Settings = self.Settings
H = Settings.H
actionInfo = self.viewerActions
viewers = self.viewers
filteredActions = {a for a in actions if a in actionInfo and a != "create"}
versionActive = self.check(viewer, versionActive)
editionId = edition._id
if editionId is None:
return ("", "")
create = "/update" if sceneExists else "/create"
src = f"/viewer/{versionActive}/{actionActive}/{editionId}{create}"
frame = H.div(
H.div(H.iframe(src, cls="previewer"), cls="surround"), cls="model"
)
def getViewerButtons(vw):
"""Internal function.
Returns
-------
string
HTML for the buttons to launch a viewer.
"""
openAtt = vw == viewer
versions = self.getVersions(vw)
if len(versions) == 0:
return H.table(
[
("No versions", {}),
{},
],
[],
cls="vwv",
)
if len(versions) == 1:
version = versions[0]
return H.table(
[
getVersionButtons(
version,
version == versionActive,
versionAmount=len(versions),
withViewer=True,
withVersion=True,
# withViewer=not pilotMode,
# withVersion=not pilotMode,
)
],
[],
cls="vwv",
)
(latest, previous) = (versions[0:1], versions[1:])
openAtt = vw == viewer and len(previous) and versionActive in previous
return H.details(
H.table(
[
getVersionButtons(
version,
version == versionActive,
versionAmount=len(versions),
withViewer=True,
)
for version in latest
],
[],
cls="vwv",
),
H.table(
[],
[
getVersionButtons(
version, version == versionActive, withViewer=False
)
for version in previous
],
cls="vwv",
),
f"vwbuttons-{editionId}",
cls="vw",
open=openAtt,
)
def getVersionButtons(
version, active, versionAmount=None, withViewer=False, withVersion=True
):
"""Internal function.
Parameters
----------
version: string
The version of the viewer.
active: boolean
Whether that version of that viewer is currently active.
versionAmount: int, optional None
If passed, contains the number of versions and displays it.
withViewer: boolean, optional False
Whether to show the viewer name in the first column.
withVersion: boolean, optional True
Whether to show the version in the second column.
Returns
-------
string
HTML for the buttons to launch a specific version of a viewer.
"""
activeRowCls = "activer" if active else ""
plural = "" if versionAmount == 2 else "s"
title = (
f"click to show previous {versionAmount - 1} {viewer} version{plural}"
if versionAmount and versionAmount > 1
else f"no previous {viewer} versions"
)
return (
[
(viewer if withViewer else H.nbsp, dict(cls="vwc", title=title)),
(
version if withVersion else H.nbsp,
dict(cls="vvl vwc", title=title),
),
]
+ [
getActionButton(
version, action, disabled=active and action == actionActive
)
for action in sorted(filteredActions)
],
dict(cls=activeRowCls),
)
def getActionButton(version, action, disabled=False):
"""Internal function.
Parameters
----------
version: string
The version of the viewer.
action: string
Whether to launch the viewer for `read` or for `update`.
disabled: boolean, optional Fasle
Whether to show the button as disabled
Returns
-------
string
HTML for the buttons to launch a specific version of a viewer
for a specific action.
"""
atts = {}
href = None if disabled else f"/edition/{editionId}/{version}/{action}"
if action == "update":
viewerHref = f"/viewer/{version}/{action}/{editionId}{create}"
atts["onclick"] = dedent(
f"""
window.open(
'{viewerHref}',
'newwindow',
width=window.innerWidth,
height=window.innerHeight
);
return false;
"""
)
titleFragment = "a new window" if action == "update" else "place"
createMode = action == "update" and not sceneExists
action = "create" if createMode else action
thisActionInfo = actionInfo.get(action, AttrDict())
name = thisActionInfo.name
atts["title"] = f"{name} scene in {titleFragment}"
disabledCls = "disabled" if disabled else ""
activeCellCls = "activec" if disabled else ""
cls = f"button vwb {disabledCls}"
return (
H.iconx(action, text=name, href=href, cls=cls, **atts),
dict(cls=f"vwc {activeCellCls}"),
)
allButtons = H.div([getViewerButtons(vw) for vw in viewers])
return (frame if sceneExists else "", allButtons)
def genHtml(self, urlBase, sceneFile, viewer, version, action, subMode):
"""Generates the HTML for the viewer page that is loaded in an iframe.
When a scene is loaded in a viewer, it happens in an iframe.
Here we generate the complete HTML for such an iframe.
Parameters
----------
urlBase: string
The first part of the root url that is given to the viewer.
The viewer code uses this to retrieve additional information.
The root url will be completed with the `action` and the `viewer`.
sceneFile: string
The name of the scene file in the file system.
viewer: string
The chosen viewer.
version: string
The chosen version of the viewer.
action: string
The chosen mode in which the viewer is launched (`read` or `update`).
subMode: string | None
The sub mode in which the viewer is to be used (`update` or `create`).
Returns
-------
string
The HTML for the iframe.
"""
Settings = self.Settings
H = Settings.H
debugMode = Settings.debugMode
viewerUrlBase = Settings.viewerUrlBase
viewers = self.viewers
ext = "dev" if debugMode else "min"
viewerStaticRoot = self.getStaticRoot(viewerUrlBase, action, viewer, version)
viewerRoot = self.getRoot(urlBase, action, viewer)
if viewer == "voyager":
modes = viewers[viewer].modes
modeProps = modes[action]
element = modeProps.element
fileBase = modeProps.fileBase
subModes = modeProps.subModes or AttrDict()
atts = attResolve(subModes[subMode] or AttrDict(), version)
if subMode != "create":
atts["document"] = sceneFile
return H.content(
H.head(
[
H.meta(charset="utf-8"),
H.link(
"shortcut icon",
f"{viewerStaticRoot}/favicon.png",
tp="image/png",
),
H.link("stylesheet", f"{viewerStaticRoot}/fonts/fonts.css"),
(
H.link(
"stylesheet",
f"{viewerStaticRoot}/css/{fileBase}.{ext}.css",
)
if action == "update"
else ""
),
H.script(
"",
defer=True,
src=f"{viewerStaticRoot}/js/{fileBase}.{ext}.js",
),
]
),
H.body(
H.elem(
element,
"",
root=viewerRoot,
resourceroot=f"{viewerStaticRoot}/",
**atts,
)
),
)
else:
return H.content(
H.head(H.meta(charset="utf-8")),
H.body(H.p(f"Unsupported viewer: {viewer}")),
)
def getRoot(self, urlBase, action, viewer):
"""Composes the root url for a viewer.
The root url is passed to a viewer instance as the url that
the viewer can use to fetch its data.
It is not meant for the static data that is part of the viewer software,
but for the model related data that the viewer is going to display.
See `getStaticRoot()` for the url meant for getting parts of the
viewer software.
Parameters
----------
urlBase: string
The first part of the root url, depending
on the project and edition.
action: string
The mode in which the viewer is opened.
Depending on the mode, the viewer code may communicate with the server
with different urls.
For example, for the voyager,
the `read` mode (voyager-explorer) uses ordinary HTTP requests,
but the `update` mode (voyager-story) uses WebDAV requests.
So this app points voyager-explorer to a root url starting with `/data`,
and voyager-story to a root url starting with `/webdav`.
These prefixes of the urls can be configured per viewer
in the viewer configuration in `yaml/viewers.yml`.
"""
viewers = self.viewers
if viewer not in viewers:
return None
modes = viewers[viewer].modes
thisMode = modes[action] or modes.read
prefix = thisMode.prefix
return f"{prefix}/{urlBase}"
def getStaticRoot(self, viewerUrlBase, action, viewer, version):
"""Composes the static root url for a viewer.
The static root url is passed to a viewer instance as the url that the
viewer can use to fetch its assets.
It is not meant for the model related data, but for the parts of the
viewer software that it needs to get from the server.
See `getRoot()` for the url meant for getting model-related data.
Parameters
----------
viewerUrlBase: string
The first part of the root url, depending
on the project and edition.
action: string
The mode in which the viewer is opened.
Depending on the mode, the viewer code may communicate with the server
with different urls.
For example, for the voyager,
the `read` mode (voyager-explorer) uses ordinary HTTP requests,
but the `update` mode (voyager-story) uses WebDAV requests.
So this app points voyager-explorer to a root url starting with `/data`,
and voyager-story to a root url starting with `/webdav`.
These prefixes of the urls can be configured per viewer
in the viewer configuration in `yaml/viewers.yml`.
"""
if not self.check(viewer, version):
return None
return f"{viewerUrlBase}/{viewer}/{version}"
Classes
class Viewers (Settings, Messages, Mongo)-
Knowledge of the installed 3D viewers.
This class knows which (versions of) viewers are installed, and has the methods to invoke them.
It is instantiated by a singleton object.
Parameters
Settings:AttrDict- App-wide configuration data obtained from
Config.Settings. Messages:object- Singleton instance of
Messages.
Expand source code Browse git
class Viewers: def __init__(self, Settings, Messages, Mongo): """Knowledge of the installed 3D viewers. This class knows which (versions of) viewers are installed, and has the methods to invoke them. It is instantiated by a singleton object. Parameters ---------- Settings: AttrDict App-wide configuration data obtained from `control.config.Config.Settings`. Messages: object Singleton instance of `control.messages.Messages`. """ self.Settings = Settings self.Mongo = Mongo self.Messages = Messages Messages.debugAdd(self) self.viewers = Settings.viewers self.viewerActions = Settings.viewerActions self.viewerDefault = Settings.viewerDefault def addAuth(self, Auth): """Give this object a handle to the Auth object. The Viewers and Auth objects need each other, so one of them must be given the handle to the other after initialization. """ self.Auth = Auth def getVersions(self, viewer): """Reads the available versions of a viewer from the file system. Keep in mind that the set of installed versions of a viewer may change due to user actions. When there are multiple web workers active, they will have different instantiations of the viewer class. So whenever you need the set of versions, invoke this method to get it freshly. Parameters ---------- viewer: string The viewer for which we want to retrieve the versions Returns ------- tuple The ordered list of versions, most recent first. """ Settings = self.Settings versionKey = Settings.versionKey dataDir = Settings.dataDir viewerDir = f"{dataDir}/viewers" viewerPath = f"{viewerDir}/{viewer}" return list(reversed(sorted(listDirs(viewerPath), key=versionKey))) def check(self, viewer, version): """Checks whether a viewer version exists. Given a viewer and a version, it is looked up whether the code is present. If not, reasonable defaults returned instead by default. Parameters ---------- viewer: string The viewer in question. version: string The version of the viewer in question. Returns ------- string | void The version is returned unmodified if that viewer version is supported. If the viewer is supported, but not the version, the default version of that viewer is taken, if there is a default version, otherwise the latest supported version. If the viewer is not supported, None is returned. """ viewers = self.viewers if viewer not in viewers: return None versions = self.getVersions(viewer) defaultVersion = versions[0] if len(versions) else "" if version not in versions: version = defaultVersion return version def getViewInfo(self, edition): """Gets viewer-related info that an edition is made with. Parameters ---------- edition: AttrDict The edition record. Returns ------- tuple of string * The name of the viewer * The name of the scene """ viewerDefault = self.viewerDefault editionId = edition._id if editionId is None: return (viewerDefault, None) editionSettings = edition.settings or AttrDict() authorTool = editionSettings.authorTool or AttrDict() viewer = authorTool.name or viewerDefault sceneFile = authorTool.sceneFile return (viewer, sceneFile) def getUsedVersions(self, viewer): """Produces a list of used versions of a viewer. These versions have been used to create existing editions. These versions may no longer exists in the system (which is undesirable). Parameters ---------- viewer: string The viewer for which we want to retrieve the versions Returns ------- dict Keyed by version and valued by the number of editions that have used this version while they were being created. """ Mongo = self.Mongo viewers = self.viewers viewerDefault = self.viewerDefault if viewer not in viewers: return () usedVersions = collections.Counter() for edition in Mongo.getList("edition", {}): editionSettings = edition.settings or AttrDict() authorTool = editionSettings.authorTool or AttrDict() thisViewer = authorTool.name or viewerDefault if thisViewer != viewer: continue version = authorTool.version usedVersions[version] += 1 return usedVersions def getReleasedVoyagerVersions(self, contactGithub): """Get all installable versions of the Voyager viewer. We fetch a list of releases from the github repo of the Voyager, together with the urls to their zip balls. The fetching is done using the GitHub API. That has a rate limit of 60 requests an hour. In order to overcome that, we store the results in MongoDB. Whenever we bump into the rate limit, we fish the data from MongoDb instead. If the fetching fails, the result may not be complete and a warning is issued. Parameters ---------- contactGithub: boolean If True, the list of installable voyager versions will be retrieved from Github. Otherwise, a cached version will be retrieved from MongoDB, if such a version exists, otherwise Github will be contacted. Returns ------- list, dict The list are the messages issued. The dict is keyed by version and valued by the url to the zip file that contains that version. """ Mongo = self.Mongo Messages = self.Messages messages = [] if not contactGithub: results = Mongo.getRecord("voyager", {}).releases or {} nResults = len(results) if nResults: messages.append( ( "info", f"Fetching from cache succeeded; giving {nResults} versions", ) ) return (messages, results) messages.append( ( "info", "No versions in cache, will contact Github", ) ) BACKEND = "https://api.github.com/repos/{org}/{repo}/releases" ORG = "Smithsonian" REPO = "dpo-voyager" HEADERS = dict(Accept="application/vnd.github+json") fetchUrl = BACKEND.format(org=ORG, repo=REPO) url = fetchUrl rawResults = [] good = True eMsg = "" exceeded = False try: while True: response = requests.get(url, headers=HEADERS) if not response.ok: try: msg = response.json().get("message", "") except Exception: msg = "" if "rate limit exceeded" in msg: exceeded = True break good = False break theseResults = response.json() rawResults.extend(theseResults) links = response.links nxt = links.get("next", None) if not nxt: break nxtUrl = nxt.get("url", None) if not nxtUrl: break url = nxtUrl except Exception as e: good = False eMsg = str(e) if exceeded or not good: results = Mongo.getRecord("voyager", {}).releases or {} nResults = len(results) statusRep = "ok" if good else f"XX ({eMsg})" cause = "temporarily blocked" if exceeded else "failed" if nResults == 0: messages.append( ( "error", f"Fetching from GitHub {cause}; no previously stored versions", ) ) Messages.error( logmsg=( f"Fetching from Github: status={statusRep}; " f"rate limit exceeded={exceeded}; " "no previously stored versions" ), ) else: messages.append( ( "warning", f"Fetching from GitHub {cause}; " f"will use {nResults} previously stored versions", ) ) Messages.warning( logmsg=( f"Fetching from Github: status={statusRep}; " f"rate limit exceeded={exceeded}; " f"there are {nResults} previously stored versions" ), ) else: results = {} for r in rawResults: assets = r.get("assets", None) if assets is None: continue found = False assetUrl = None for asset in assets: name = asset["name"] tp = asset["content_type"] if "zip" not in tp or "zip" not in name or "voyager" not in name: continue found = True assetUrl = asset["browser_download_url"] break if not found: continue results[r["tag_name"].lstrip("v")] = assetUrl nResults = len(results) messages.append( ( "good", f"Fetching from GitHub succeeded; giving {nResults} versions", ) ) Messages.good( logmsg=( f"Fetching from Github succeeded; there are {nResults} versions" ), ) Mongo.clearTable("voyager") Mongo.insertRecord("voyager", dict(releases=results)) return (messages, results) def getVoyagerVersionTable(self, contactGithub): """Produces a sorted table of Voyager versions with information per version. The fields of information are: * is the version released, and if so, what is the zip url * is the version installed * is the version used and if so, by how many editions Parameters ---------- contactGithub: boolean If True, the list of installable voyager versions will be retrieved from Github. Otherwise, a cached version will be retrieved from MongoDB, if such a version exists, otherwise Github will be contacted. Returns ------- list """ Settings = self.Settings versionKey = Settings.versionKey usedVersions = self.getUsedVersions("voyager") usedVersionSet = set(usedVersions) installedVersionSet = set(self.getVersions("voyager")) messages, releasedVersions = self.getReleasedVoyagerVersions(contactGithub) releasedVersionSet = set(releasedVersions) allVersions = reversed( sorted( usedVersionSet | installedVersionSet | releasedVersionSet, key=versionKey, ) ) rows = [] for version in allVersions: zipUrl = releasedVersions.get(version, None) isInstalled = version in installedVersionSet nEditions = usedVersions.get(version, 0) rows.append((version, zipUrl, isInstalled, nEditions)) return (messages, rows) def getFrame( self, edition, actions, viewer, versionActive, actionActive, sceneExists ): """Produces a set of buttons to launch 3D viewers for a scene. Make sure that if there is no scene file present, no viewer will be opened. Parameters ---------- edition: AttrDict The edition in question. actions: iterable of string The actions for which we have to create buttons. Typically `read` and possibly also `update`. Actions that are not recognized as viewer actions will be filtered out, such as `create` and `delete`. viewer: string The viewer in which the scene is currently loaded. versionActive: string | void The version of the viewer in which the scene is currently loaded, if any, otherwise None actionActive: string | void The mode in which the scene is currently loaded in the viewer (`read` or `update`), if any, otherwise None sceneExists: boolean Whether the scene file exists. Returns ------- string The HTML that represents the buttons. """ Settings = self.Settings H = Settings.H actionInfo = self.viewerActions viewers = self.viewers filteredActions = {a for a in actions if a in actionInfo and a != "create"} versionActive = self.check(viewer, versionActive) editionId = edition._id if editionId is None: return ("", "") create = "/update" if sceneExists else "/create" src = f"/viewer/{versionActive}/{actionActive}/{editionId}{create}" frame = H.div( H.div(H.iframe(src, cls="previewer"), cls="surround"), cls="model" ) def getViewerButtons(vw): """Internal function. Returns ------- string HTML for the buttons to launch a viewer. """ openAtt = vw == viewer versions = self.getVersions(vw) if len(versions) == 0: return H.table( [ ("No versions", {}), {}, ], [], cls="vwv", ) if len(versions) == 1: version = versions[0] return H.table( [ getVersionButtons( version, version == versionActive, versionAmount=len(versions), withViewer=True, withVersion=True, # withViewer=not pilotMode, # withVersion=not pilotMode, ) ], [], cls="vwv", ) (latest, previous) = (versions[0:1], versions[1:]) openAtt = vw == viewer and len(previous) and versionActive in previous return H.details( H.table( [ getVersionButtons( version, version == versionActive, versionAmount=len(versions), withViewer=True, ) for version in latest ], [], cls="vwv", ), H.table( [], [ getVersionButtons( version, version == versionActive, withViewer=False ) for version in previous ], cls="vwv", ), f"vwbuttons-{editionId}", cls="vw", open=openAtt, ) def getVersionButtons( version, active, versionAmount=None, withViewer=False, withVersion=True ): """Internal function. Parameters ---------- version: string The version of the viewer. active: boolean Whether that version of that viewer is currently active. versionAmount: int, optional None If passed, contains the number of versions and displays it. withViewer: boolean, optional False Whether to show the viewer name in the first column. withVersion: boolean, optional True Whether to show the version in the second column. Returns ------- string HTML for the buttons to launch a specific version of a viewer. """ activeRowCls = "activer" if active else "" plural = "" if versionAmount == 2 else "s" title = ( f"click to show previous {versionAmount - 1} {viewer} version{plural}" if versionAmount and versionAmount > 1 else f"no previous {viewer} versions" ) return ( [ (viewer if withViewer else H.nbsp, dict(cls="vwc", title=title)), ( version if withVersion else H.nbsp, dict(cls="vvl vwc", title=title), ), ] + [ getActionButton( version, action, disabled=active and action == actionActive ) for action in sorted(filteredActions) ], dict(cls=activeRowCls), ) def getActionButton(version, action, disabled=False): """Internal function. Parameters ---------- version: string The version of the viewer. action: string Whether to launch the viewer for `read` or for `update`. disabled: boolean, optional Fasle Whether to show the button as disabled Returns ------- string HTML for the buttons to launch a specific version of a viewer for a specific action. """ atts = {} href = None if disabled else f"/edition/{editionId}/{version}/{action}" if action == "update": viewerHref = f"/viewer/{version}/{action}/{editionId}{create}" atts["onclick"] = dedent( f""" window.open( '{viewerHref}', 'newwindow', width=window.innerWidth, height=window.innerHeight ); return false; """ ) titleFragment = "a new window" if action == "update" else "place" createMode = action == "update" and not sceneExists action = "create" if createMode else action thisActionInfo = actionInfo.get(action, AttrDict()) name = thisActionInfo.name atts["title"] = f"{name} scene in {titleFragment}" disabledCls = "disabled" if disabled else "" activeCellCls = "activec" if disabled else "" cls = f"button vwb {disabledCls}" return ( H.iconx(action, text=name, href=href, cls=cls, **atts), dict(cls=f"vwc {activeCellCls}"), ) allButtons = H.div([getViewerButtons(vw) for vw in viewers]) return (frame if sceneExists else "", allButtons) def genHtml(self, urlBase, sceneFile, viewer, version, action, subMode): """Generates the HTML for the viewer page that is loaded in an iframe. When a scene is loaded in a viewer, it happens in an iframe. Here we generate the complete HTML for such an iframe. Parameters ---------- urlBase: string The first part of the root url that is given to the viewer. The viewer code uses this to retrieve additional information. The root url will be completed with the `action` and the `viewer`. sceneFile: string The name of the scene file in the file system. viewer: string The chosen viewer. version: string The chosen version of the viewer. action: string The chosen mode in which the viewer is launched (`read` or `update`). subMode: string | None The sub mode in which the viewer is to be used (`update` or `create`). Returns ------- string The HTML for the iframe. """ Settings = self.Settings H = Settings.H debugMode = Settings.debugMode viewerUrlBase = Settings.viewerUrlBase viewers = self.viewers ext = "dev" if debugMode else "min" viewerStaticRoot = self.getStaticRoot(viewerUrlBase, action, viewer, version) viewerRoot = self.getRoot(urlBase, action, viewer) if viewer == "voyager": modes = viewers[viewer].modes modeProps = modes[action] element = modeProps.element fileBase = modeProps.fileBase subModes = modeProps.subModes or AttrDict() atts = attResolve(subModes[subMode] or AttrDict(), version) if subMode != "create": atts["document"] = sceneFile return H.content( H.head( [ H.meta(charset="utf-8"), H.link( "shortcut icon", f"{viewerStaticRoot}/favicon.png", tp="image/png", ), H.link("stylesheet", f"{viewerStaticRoot}/fonts/fonts.css"), ( H.link( "stylesheet", f"{viewerStaticRoot}/css/{fileBase}.{ext}.css", ) if action == "update" else "" ), H.script( "", defer=True, src=f"{viewerStaticRoot}/js/{fileBase}.{ext}.js", ), ] ), H.body( H.elem( element, "", root=viewerRoot, resourceroot=f"{viewerStaticRoot}/", **atts, ) ), ) else: return H.content( H.head(H.meta(charset="utf-8")), H.body(H.p(f"Unsupported viewer: {viewer}")), ) def getRoot(self, urlBase, action, viewer): """Composes the root url for a viewer. The root url is passed to a viewer instance as the url that the viewer can use to fetch its data. It is not meant for the static data that is part of the viewer software, but for the model related data that the viewer is going to display. See `getStaticRoot()` for the url meant for getting parts of the viewer software. Parameters ---------- urlBase: string The first part of the root url, depending on the project and edition. action: string The mode in which the viewer is opened. Depending on the mode, the viewer code may communicate with the server with different urls. For example, for the voyager, the `read` mode (voyager-explorer) uses ordinary HTTP requests, but the `update` mode (voyager-story) uses WebDAV requests. So this app points voyager-explorer to a root url starting with `/data`, and voyager-story to a root url starting with `/webdav`. These prefixes of the urls can be configured per viewer in the viewer configuration in `yaml/viewers.yml`. """ viewers = self.viewers if viewer not in viewers: return None modes = viewers[viewer].modes thisMode = modes[action] or modes.read prefix = thisMode.prefix return f"{prefix}/{urlBase}" def getStaticRoot(self, viewerUrlBase, action, viewer, version): """Composes the static root url for a viewer. The static root url is passed to a viewer instance as the url that the viewer can use to fetch its assets. It is not meant for the model related data, but for the parts of the viewer software that it needs to get from the server. See `getRoot()` for the url meant for getting model-related data. Parameters ---------- viewerUrlBase: string The first part of the root url, depending on the project and edition. action: string The mode in which the viewer is opened. Depending on the mode, the viewer code may communicate with the server with different urls. For example, for the voyager, the `read` mode (voyager-explorer) uses ordinary HTTP requests, but the `update` mode (voyager-story) uses WebDAV requests. So this app points voyager-explorer to a root url starting with `/data`, and voyager-story to a root url starting with `/webdav`. These prefixes of the urls can be configured per viewer in the viewer configuration in `yaml/viewers.yml`. """ if not self.check(viewer, version): return None return f"{viewerUrlBase}/{viewer}/{version}"Methods
def addAuth(self, Auth)-
Give this object a handle to the Auth object.
The Viewers and Auth objects need each other, so one of them must be given the handle to the other after initialization.
Expand source code Browse git
def addAuth(self, Auth): """Give this object a handle to the Auth object. The Viewers and Auth objects need each other, so one of them must be given the handle to the other after initialization. """ self.Auth = Auth def check(self, viewer, version)-
Checks whether a viewer version exists.
Given a viewer and a version, it is looked up whether the code is present. If not, reasonable defaults returned instead by default.
Parameters
viewer:string- The viewer in question.
version:string- The version of the viewer in question.
Returns
string | void- The version is returned unmodified if that viewer version is supported. If the viewer is supported, but not the version, the default version of that viewer is taken, if there is a default version, otherwise the latest supported version. If the viewer is not supported, None is returned.
Expand source code Browse git
def check(self, viewer, version): """Checks whether a viewer version exists. Given a viewer and a version, it is looked up whether the code is present. If not, reasonable defaults returned instead by default. Parameters ---------- viewer: string The viewer in question. version: string The version of the viewer in question. Returns ------- string | void The version is returned unmodified if that viewer version is supported. If the viewer is supported, but not the version, the default version of that viewer is taken, if there is a default version, otherwise the latest supported version. If the viewer is not supported, None is returned. """ viewers = self.viewers if viewer not in viewers: return None versions = self.getVersions(viewer) defaultVersion = versions[0] if len(versions) else "" if version not in versions: version = defaultVersion return version def genHtml(self, urlBase, sceneFile, viewer, version, action, subMode)-
Generates the HTML for the viewer page that is loaded in an iframe.
When a scene is loaded in a viewer, it happens in an iframe. Here we generate the complete HTML for such an iframe.
Parameters
urlBase:string- The first part of the root url that is given to the viewer.
The viewer code uses this to retrieve additional information.
The root url will be completed with the
actionand theviewer. sceneFile:string- The name of the scene file in the file system.
viewer:string- The chosen viewer.
version:string- The chosen version of the viewer.
action:string- The chosen mode in which the viewer is launched (
readorupdate). subMode:string | None- The sub mode in which the viewer is to be used (
updateorcreate).
Returns
string- The HTML for the iframe.
Expand source code Browse git
def genHtml(self, urlBase, sceneFile, viewer, version, action, subMode): """Generates the HTML for the viewer page that is loaded in an iframe. When a scene is loaded in a viewer, it happens in an iframe. Here we generate the complete HTML for such an iframe. Parameters ---------- urlBase: string The first part of the root url that is given to the viewer. The viewer code uses this to retrieve additional information. The root url will be completed with the `action` and the `viewer`. sceneFile: string The name of the scene file in the file system. viewer: string The chosen viewer. version: string The chosen version of the viewer. action: string The chosen mode in which the viewer is launched (`read` or `update`). subMode: string | None The sub mode in which the viewer is to be used (`update` or `create`). Returns ------- string The HTML for the iframe. """ Settings = self.Settings H = Settings.H debugMode = Settings.debugMode viewerUrlBase = Settings.viewerUrlBase viewers = self.viewers ext = "dev" if debugMode else "min" viewerStaticRoot = self.getStaticRoot(viewerUrlBase, action, viewer, version) viewerRoot = self.getRoot(urlBase, action, viewer) if viewer == "voyager": modes = viewers[viewer].modes modeProps = modes[action] element = modeProps.element fileBase = modeProps.fileBase subModes = modeProps.subModes or AttrDict() atts = attResolve(subModes[subMode] or AttrDict(), version) if subMode != "create": atts["document"] = sceneFile return H.content( H.head( [ H.meta(charset="utf-8"), H.link( "shortcut icon", f"{viewerStaticRoot}/favicon.png", tp="image/png", ), H.link("stylesheet", f"{viewerStaticRoot}/fonts/fonts.css"), ( H.link( "stylesheet", f"{viewerStaticRoot}/css/{fileBase}.{ext}.css", ) if action == "update" else "" ), H.script( "", defer=True, src=f"{viewerStaticRoot}/js/{fileBase}.{ext}.js", ), ] ), H.body( H.elem( element, "", root=viewerRoot, resourceroot=f"{viewerStaticRoot}/", **atts, ) ), ) else: return H.content( H.head(H.meta(charset="utf-8")), H.body(H.p(f"Unsupported viewer: {viewer}")), ) def getFrame(self, edition, actions, viewer, versionActive, actionActive, sceneExists)-
Produces a set of buttons to launch 3D viewers for a scene.
Make sure that if there is no scene file present, no viewer will be opened.
Parameters
edition:AttrDict- The edition in question.
actions:iterableofstring- The actions for which we have to create buttons.
Typically
readand possibly alsoupdate. Actions that are not recognized as viewer actions will be filtered out, such ascreateanddelete. viewer:string- The viewer in which the scene is currently loaded.
versionActive:string | void- The version of the viewer in which the scene is currently loaded, if any, otherwise None
actionActive:string | void- The mode in which the scene is currently loaded in the viewer
(
readorupdate), if any, otherwise None sceneExists:boolean- Whether the scene file exists.
Returns
string- The HTML that represents the buttons.
Expand source code Browse git
def getFrame( self, edition, actions, viewer, versionActive, actionActive, sceneExists ): """Produces a set of buttons to launch 3D viewers for a scene. Make sure that if there is no scene file present, no viewer will be opened. Parameters ---------- edition: AttrDict The edition in question. actions: iterable of string The actions for which we have to create buttons. Typically `read` and possibly also `update`. Actions that are not recognized as viewer actions will be filtered out, such as `create` and `delete`. viewer: string The viewer in which the scene is currently loaded. versionActive: string | void The version of the viewer in which the scene is currently loaded, if any, otherwise None actionActive: string | void The mode in which the scene is currently loaded in the viewer (`read` or `update`), if any, otherwise None sceneExists: boolean Whether the scene file exists. Returns ------- string The HTML that represents the buttons. """ Settings = self.Settings H = Settings.H actionInfo = self.viewerActions viewers = self.viewers filteredActions = {a for a in actions if a in actionInfo and a != "create"} versionActive = self.check(viewer, versionActive) editionId = edition._id if editionId is None: return ("", "") create = "/update" if sceneExists else "/create" src = f"/viewer/{versionActive}/{actionActive}/{editionId}{create}" frame = H.div( H.div(H.iframe(src, cls="previewer"), cls="surround"), cls="model" ) def getViewerButtons(vw): """Internal function. Returns ------- string HTML for the buttons to launch a viewer. """ openAtt = vw == viewer versions = self.getVersions(vw) if len(versions) == 0: return H.table( [ ("No versions", {}), {}, ], [], cls="vwv", ) if len(versions) == 1: version = versions[0] return H.table( [ getVersionButtons( version, version == versionActive, versionAmount=len(versions), withViewer=True, withVersion=True, # withViewer=not pilotMode, # withVersion=not pilotMode, ) ], [], cls="vwv", ) (latest, previous) = (versions[0:1], versions[1:]) openAtt = vw == viewer and len(previous) and versionActive in previous return H.details( H.table( [ getVersionButtons( version, version == versionActive, versionAmount=len(versions), withViewer=True, ) for version in latest ], [], cls="vwv", ), H.table( [], [ getVersionButtons( version, version == versionActive, withViewer=False ) for version in previous ], cls="vwv", ), f"vwbuttons-{editionId}", cls="vw", open=openAtt, ) def getVersionButtons( version, active, versionAmount=None, withViewer=False, withVersion=True ): """Internal function. Parameters ---------- version: string The version of the viewer. active: boolean Whether that version of that viewer is currently active. versionAmount: int, optional None If passed, contains the number of versions and displays it. withViewer: boolean, optional False Whether to show the viewer name in the first column. withVersion: boolean, optional True Whether to show the version in the second column. Returns ------- string HTML for the buttons to launch a specific version of a viewer. """ activeRowCls = "activer" if active else "" plural = "" if versionAmount == 2 else "s" title = ( f"click to show previous {versionAmount - 1} {viewer} version{plural}" if versionAmount and versionAmount > 1 else f"no previous {viewer} versions" ) return ( [ (viewer if withViewer else H.nbsp, dict(cls="vwc", title=title)), ( version if withVersion else H.nbsp, dict(cls="vvl vwc", title=title), ), ] + [ getActionButton( version, action, disabled=active and action == actionActive ) for action in sorted(filteredActions) ], dict(cls=activeRowCls), ) def getActionButton(version, action, disabled=False): """Internal function. Parameters ---------- version: string The version of the viewer. action: string Whether to launch the viewer for `read` or for `update`. disabled: boolean, optional Fasle Whether to show the button as disabled Returns ------- string HTML for the buttons to launch a specific version of a viewer for a specific action. """ atts = {} href = None if disabled else f"/edition/{editionId}/{version}/{action}" if action == "update": viewerHref = f"/viewer/{version}/{action}/{editionId}{create}" atts["onclick"] = dedent( f""" window.open( '{viewerHref}', 'newwindow', width=window.innerWidth, height=window.innerHeight ); return false; """ ) titleFragment = "a new window" if action == "update" else "place" createMode = action == "update" and not sceneExists action = "create" if createMode else action thisActionInfo = actionInfo.get(action, AttrDict()) name = thisActionInfo.name atts["title"] = f"{name} scene in {titleFragment}" disabledCls = "disabled" if disabled else "" activeCellCls = "activec" if disabled else "" cls = f"button vwb {disabledCls}" return ( H.iconx(action, text=name, href=href, cls=cls, **atts), dict(cls=f"vwc {activeCellCls}"), ) allButtons = H.div([getViewerButtons(vw) for vw in viewers]) return (frame if sceneExists else "", allButtons) def getReleasedVoyagerVersions(self, contactGithub)-
Get all installable versions of the Voyager viewer.
We fetch a list of releases from the github repo of the Voyager, together with the urls to their zip balls.
The fetching is done using the GitHub API. That has a rate limit of 60 requests an hour. In order to overcome that, we store the results in MongoDB.
Whenever we bump into the rate limit, we fish the data from MongoDb instead.
If the fetching fails, the result may not be complete and a warning is issued.
Parameters
contactGithub:boolean- If True, the list of installable voyager versions will be retrieved from Github. Otherwise, a cached version will be retrieved from MongoDB, if such a version exists, otherwise Github will be contacted.
Returns
list, dict- The list are the messages issued. The dict is keyed by version and valued by the url to the zip file that contains that version.
Expand source code Browse git
def getReleasedVoyagerVersions(self, contactGithub): """Get all installable versions of the Voyager viewer. We fetch a list of releases from the github repo of the Voyager, together with the urls to their zip balls. The fetching is done using the GitHub API. That has a rate limit of 60 requests an hour. In order to overcome that, we store the results in MongoDB. Whenever we bump into the rate limit, we fish the data from MongoDb instead. If the fetching fails, the result may not be complete and a warning is issued. Parameters ---------- contactGithub: boolean If True, the list of installable voyager versions will be retrieved from Github. Otherwise, a cached version will be retrieved from MongoDB, if such a version exists, otherwise Github will be contacted. Returns ------- list, dict The list are the messages issued. The dict is keyed by version and valued by the url to the zip file that contains that version. """ Mongo = self.Mongo Messages = self.Messages messages = [] if not contactGithub: results = Mongo.getRecord("voyager", {}).releases or {} nResults = len(results) if nResults: messages.append( ( "info", f"Fetching from cache succeeded; giving {nResults} versions", ) ) return (messages, results) messages.append( ( "info", "No versions in cache, will contact Github", ) ) BACKEND = "https://api.github.com/repos/{org}/{repo}/releases" ORG = "Smithsonian" REPO = "dpo-voyager" HEADERS = dict(Accept="application/vnd.github+json") fetchUrl = BACKEND.format(org=ORG, repo=REPO) url = fetchUrl rawResults = [] good = True eMsg = "" exceeded = False try: while True: response = requests.get(url, headers=HEADERS) if not response.ok: try: msg = response.json().get("message", "") except Exception: msg = "" if "rate limit exceeded" in msg: exceeded = True break good = False break theseResults = response.json() rawResults.extend(theseResults) links = response.links nxt = links.get("next", None) if not nxt: break nxtUrl = nxt.get("url", None) if not nxtUrl: break url = nxtUrl except Exception as e: good = False eMsg = str(e) if exceeded or not good: results = Mongo.getRecord("voyager", {}).releases or {} nResults = len(results) statusRep = "ok" if good else f"XX ({eMsg})" cause = "temporarily blocked" if exceeded else "failed" if nResults == 0: messages.append( ( "error", f"Fetching from GitHub {cause}; no previously stored versions", ) ) Messages.error( logmsg=( f"Fetching from Github: status={statusRep}; " f"rate limit exceeded={exceeded}; " "no previously stored versions" ), ) else: messages.append( ( "warning", f"Fetching from GitHub {cause}; " f"will use {nResults} previously stored versions", ) ) Messages.warning( logmsg=( f"Fetching from Github: status={statusRep}; " f"rate limit exceeded={exceeded}; " f"there are {nResults} previously stored versions" ), ) else: results = {} for r in rawResults: assets = r.get("assets", None) if assets is None: continue found = False assetUrl = None for asset in assets: name = asset["name"] tp = asset["content_type"] if "zip" not in tp or "zip" not in name or "voyager" not in name: continue found = True assetUrl = asset["browser_download_url"] break if not found: continue results[r["tag_name"].lstrip("v")] = assetUrl nResults = len(results) messages.append( ( "good", f"Fetching from GitHub succeeded; giving {nResults} versions", ) ) Messages.good( logmsg=( f"Fetching from Github succeeded; there are {nResults} versions" ), ) Mongo.clearTable("voyager") Mongo.insertRecord("voyager", dict(releases=results)) return (messages, results) def getRoot(self, urlBase, action, viewer)-
Composes the root url for a viewer.
The root url is passed to a viewer instance as the url that the viewer can use to fetch its data. It is not meant for the static data that is part of the viewer software, but for the model related data that the viewer is going to display.
See
getStaticRoot()for the url meant for getting parts of the viewer software.Parameters
urlBase:string- The first part of the root url, depending on the project and edition.
action:string-
The mode in which the viewer is opened. Depending on the mode, the viewer code may communicate with the server with different urls. For example, for the voyager, the
readmode (voyager-explorer) uses ordinary HTTP requests, but theupdatemode (voyager-story) uses WebDAV requests.So this app points voyager-explorer to a root url starting with
/data, and voyager-story to a root url starting with/webdav.These prefixes of the urls can be configured per viewer in the viewer configuration in
yaml/viewers.yml.
Expand source code Browse git
def getRoot(self, urlBase, action, viewer): """Composes the root url for a viewer. The root url is passed to a viewer instance as the url that the viewer can use to fetch its data. It is not meant for the static data that is part of the viewer software, but for the model related data that the viewer is going to display. See `getStaticRoot()` for the url meant for getting parts of the viewer software. Parameters ---------- urlBase: string The first part of the root url, depending on the project and edition. action: string The mode in which the viewer is opened. Depending on the mode, the viewer code may communicate with the server with different urls. For example, for the voyager, the `read` mode (voyager-explorer) uses ordinary HTTP requests, but the `update` mode (voyager-story) uses WebDAV requests. So this app points voyager-explorer to a root url starting with `/data`, and voyager-story to a root url starting with `/webdav`. These prefixes of the urls can be configured per viewer in the viewer configuration in `yaml/viewers.yml`. """ viewers = self.viewers if viewer not in viewers: return None modes = viewers[viewer].modes thisMode = modes[action] or modes.read prefix = thisMode.prefix return f"{prefix}/{urlBase}" def getStaticRoot(self, viewerUrlBase, action, viewer, version)-
Composes the static root url for a viewer.
The static root url is passed to a viewer instance as the url that the viewer can use to fetch its assets. It is not meant for the model related data, but for the parts of the viewer software that it needs to get from the server.
See
getRoot()for the url meant for getting model-related data.Parameters
viewerUrlBase:string- The first part of the root url, depending on the project and edition.
action:string-
The mode in which the viewer is opened. Depending on the mode, the viewer code may communicate with the server with different urls. For example, for the voyager, the
readmode (voyager-explorer) uses ordinary HTTP requests, but theupdatemode (voyager-story) uses WebDAV requests.So this app points voyager-explorer to a root url starting with
/data, and voyager-story to a root url starting with/webdav.These prefixes of the urls can be configured per viewer in the viewer configuration in
yaml/viewers.yml.
Expand source code Browse git
def getStaticRoot(self, viewerUrlBase, action, viewer, version): """Composes the static root url for a viewer. The static root url is passed to a viewer instance as the url that the viewer can use to fetch its assets. It is not meant for the model related data, but for the parts of the viewer software that it needs to get from the server. See `getRoot()` for the url meant for getting model-related data. Parameters ---------- viewerUrlBase: string The first part of the root url, depending on the project and edition. action: string The mode in which the viewer is opened. Depending on the mode, the viewer code may communicate with the server with different urls. For example, for the voyager, the `read` mode (voyager-explorer) uses ordinary HTTP requests, but the `update` mode (voyager-story) uses WebDAV requests. So this app points voyager-explorer to a root url starting with `/data`, and voyager-story to a root url starting with `/webdav`. These prefixes of the urls can be configured per viewer in the viewer configuration in `yaml/viewers.yml`. """ if not self.check(viewer, version): return None return f"{viewerUrlBase}/{viewer}/{version}" def getUsedVersions(self, viewer)-
Produces a list of used versions of a viewer.
These versions have been used to create existing editions. These versions may no longer exists in the system (which is undesirable).
Parameters
viewer:string- The viewer for which we want to retrieve the versions
Returns
dict- Keyed by version and valued by the number of editions that have used this version while they were being created.
Expand source code Browse git
def getUsedVersions(self, viewer): """Produces a list of used versions of a viewer. These versions have been used to create existing editions. These versions may no longer exists in the system (which is undesirable). Parameters ---------- viewer: string The viewer for which we want to retrieve the versions Returns ------- dict Keyed by version and valued by the number of editions that have used this version while they were being created. """ Mongo = self.Mongo viewers = self.viewers viewerDefault = self.viewerDefault if viewer not in viewers: return () usedVersions = collections.Counter() for edition in Mongo.getList("edition", {}): editionSettings = edition.settings or AttrDict() authorTool = editionSettings.authorTool or AttrDict() thisViewer = authorTool.name or viewerDefault if thisViewer != viewer: continue version = authorTool.version usedVersions[version] += 1 return usedVersions def getVersions(self, viewer)-
Reads the available versions of a viewer from the file system.
Keep in mind that the set of installed versions of a viewer may change due to user actions. When there are multiple web workers active, they will have different instantiations of the viewer class. So whenever you need the set of versions, invoke this method to get it freshly.
Parameters
viewer:string- The viewer for which we want to retrieve the versions
Returns
tuple- The ordered list of versions, most recent first.
Expand source code Browse git
def getVersions(self, viewer): """Reads the available versions of a viewer from the file system. Keep in mind that the set of installed versions of a viewer may change due to user actions. When there are multiple web workers active, they will have different instantiations of the viewer class. So whenever you need the set of versions, invoke this method to get it freshly. Parameters ---------- viewer: string The viewer for which we want to retrieve the versions Returns ------- tuple The ordered list of versions, most recent first. """ Settings = self.Settings versionKey = Settings.versionKey dataDir = Settings.dataDir viewerDir = f"{dataDir}/viewers" viewerPath = f"{viewerDir}/{viewer}" return list(reversed(sorted(listDirs(viewerPath), key=versionKey))) def getViewInfo(self, edition)-
Gets viewer-related info that an edition is made with.
Parameters
edition:AttrDict- The edition record.
Returns
tupleofstring-
- The name of the viewer
- The name of the scene
Expand source code Browse git
def getViewInfo(self, edition): """Gets viewer-related info that an edition is made with. Parameters ---------- edition: AttrDict The edition record. Returns ------- tuple of string * The name of the viewer * The name of the scene """ viewerDefault = self.viewerDefault editionId = edition._id if editionId is None: return (viewerDefault, None) editionSettings = edition.settings or AttrDict() authorTool = editionSettings.authorTool or AttrDict() viewer = authorTool.name or viewerDefault sceneFile = authorTool.sceneFile return (viewer, sceneFile) def getVoyagerVersionTable(self, contactGithub)-
Produces a sorted table of Voyager versions with information per version.
The fields of information are:
- is the version released, and if so, what is the zip url
- is the version installed
- is the version used and if so, by how many editions
Parameters
contactGithub:boolean- If True, the list of installable voyager versions will be retrieved from Github. Otherwise, a cached version will be retrieved from MongoDB, if such a version exists, otherwise Github will be contacted.
Returns
list
Expand source code Browse git
def getVoyagerVersionTable(self, contactGithub): """Produces a sorted table of Voyager versions with information per version. The fields of information are: * is the version released, and if so, what is the zip url * is the version installed * is the version used and if so, by how many editions Parameters ---------- contactGithub: boolean If True, the list of installable voyager versions will be retrieved from Github. Otherwise, a cached version will be retrieved from MongoDB, if such a version exists, otherwise Github will be contacted. Returns ------- list """ Settings = self.Settings versionKey = Settings.versionKey usedVersions = self.getUsedVersions("voyager") usedVersionSet = set(usedVersions) installedVersionSet = set(self.getVersions("voyager")) messages, releasedVersions = self.getReleasedVoyagerVersions(contactGithub) releasedVersionSet = set(releasedVersions) allVersions = reversed( sorted( usedVersionSet | installedVersionSet | releasedVersionSet, key=versionKey, ) ) rows = [] for version in allVersions: zipUrl = releasedVersions.get(version, None) isInstalled = version in installedVersionSet nEditions = usedVersions.get(version, 0) rows.append((version, zipUrl, isInstalled, nEditions)) return (messages, rows)