Module control.pages
Expand source code Browse git
from .flask import redirectStatus, renderTemplate, sendFile, getReferrer
class Pages:
def __init__(self, Settings, Viewers, Messages, Mongo, Content, Backup, Auth):
"""Making responses that can be displayed as web pages.
This class has methods that correspond to routes in the app,
for which they get the data (using `control.content.Content`),
which gets then wrapped in HTML.
It is instantiated by a singleton object.
Most methods generate a response that contains the content of a complete
page. For those methods we do not document the return value.
Some methods return something different.
If so, it the return value will be documented.
Parameters
----------
Settings: AttrDict
App-wide configuration data obtained from
`control.config.Config.Settings`.
Viewers: object
Singleton instance of `control.viewers.Viewers`.
Messages: object
Singleton instance of `control.messages.Messages`.
Mongo: object
Singleton instance of `control.mongo.Mongo`.
Content: object
Singleton instance of `control.content.Content`.
Auth: object
Singleton instance of `control.auth.Auth`.
"""
self.Settings = Settings
self.Viewers = Viewers
self.Messages = Messages
Messages.debugAdd(self)
self.Mongo = Mongo
self.Content = Content
self.Backup = Backup
self.Auth = Auth
def precheck(self, edition):
"""Check the articles of an edition prior to publishing.
Parameters
----------
edition: string
the edition
After the operation:
Goes back to the referrer url.
The check operation will have generated a table of contents for the
articles and media files, and these will be shown on the edition page.
Returns
-------
response
"""
Content = self.Content
good = Content.precheck(edition)
ref = getReferrer().removeprefix("/")
return redirectStatus(f"/{ref}", good)
def publish(self, edition, force):
"""Publish an edition as static pages.
Parameters
----------
edition: string
the edition
force: boolean
If True, ignore when some checks fail
After the operation:
* *success*: goes back to referrer url, good status
* *failure*: goes back to referrer url, error status
Returns
-------
response
"""
Content = self.Content
good = Content.publish(edition, force)
ref = getReferrer().removeprefix("/")
return redirectStatus(f"/{ref}", good)
def republish(self, edition, force):
"""Re-publish an edition as static pages.
Parameters
----------
edition: string
the edition
force: boolean
If True, ignore when some checks fail
After the operation:
* *success*: goes back to referrer url, good status
* *failure*: goes back to referrer url, error status
Returns
-------
response
"""
Content = self.Content
good = Content.republish(edition, force)
ref = getReferrer().removeprefix("/")
return redirectStatus(f"/{ref}", good)
def unpublish(self, edition):
"""Unpublish an edition from the static pages.
Parameters
----------
edition: string
the edition
After the operation:
* *success*: goes back to referrer url, good status
* *failure*: goes back to referrer url, error status
Returns
-------
response
"""
Content = self.Content
good = Content.unpublish(edition)
ref = getReferrer().removeprefix("/")
return redirectStatus(f"/{ref}", good)
def generate(self):
"""Regenerate the static HTML pages for the whole published site.
After the operation:
* *success*: goes back to referrer url, good status
* *failure*: goes back to referrer url, error status
Returns
-------
response
"""
Content = self.Content
good = Content.generate()
ref = getReferrer().removeprefix("/")
return redirectStatus(f"/{ref}", good)
def mkBackup(self, project=None):
"""Backup: Save file and database data in a backup directory.
Parameters
----------
project: string, optional None
If given, only backs up the given project.
After the operation:
* *success*: goes back to referrer url, good status
* *failure*: goes back to referrer url, error status
Returns
-------
response
"""
Messages = self.Messages
Backup = self.Backup
Auth = self.Auth
if not Auth.mayBackup(project=project):
Messages.warning(
msg="Making a backup is not allowed",
logmsg=("Making a backup is not allowed"),
)
ref = getReferrer().removeprefix("/")
return redirectStatus(f"/{ref}", False)
good = Backup.mkBackup(project=project)
ref = getReferrer().removeprefix("/")
return redirectStatus(f"/{ref}", good)
def restoreBackup(self, backup, project=None):
"""Restore from a backup. Make a new backup first.
After the operation:
* *success*:
* site-wide restore: goes to logout url, good status
* project-specific restore: goes to project url, good status
* *failure*: goes back to referrer url, error status
Parameters
----------
backup: string
The name of the backup as stored in the backups directory on the server.
project: string, optional None
If given, restores the given project.
Returns
-------
response
"""
Messages = self.Messages
Mongo = self.Mongo
Backup = self.Backup
Auth = self.Auth
(projectId, project) = Mongo.get("project", project)
if not Auth.mayBackup(project=project):
Messages.warning(
msg="Restoring from a backup is not allowed",
logmsg=("Restoring from a backup is not allowed"),
)
ref = getReferrer().removeprefix("/")
return redirectStatus(f"/{ref}", False)
good = Backup.restoreBackup(backup, project=project)
back = "/alogout" if project is None else f"/project/{projectId}"
return redirectStatus(back, good)
def delBackup(self, backup, project=None):
"""Deletes a backup.
After the operation:
* *success*: goes back to referrer url, good status
* *failure*: goes back to referrer url, error status
Parameters
----------
backup: string
The name of the backup as stored in the backups directory on the server.
project: string, optional None
If given, deletes a backup of the given project.
Returns
-------
response
"""
Messages = self.Messages
Mongo = self.Mongo
Backup = self.Backup
Auth = self.Auth
(projectId, project) = Mongo.get("project", project)
ref = getReferrer().removeprefix("/")
back = f"/{ref}"
if not Auth.mayBackup(project=project):
Messages.warning(
msg="Deleting a backup is not allowed",
logmsg=("Deleting a backup is not allowed"),
)
return redirectStatus(back, False)
good = Backup.delBackup(backup, project=project)
return redirectStatus(back, good)
def home(self):
"""The site-wide home page.
Returns
-------
response
"""
Content = self.Content
Auth = self.Auth
inPower = Auth.inPower()[0]
(table, recordId, record) = Content.relevant()
if recordId is None:
(left, right) = (None, None)
else:
mainFields = Content.getMetaFields("site", ("main", "narrative"), level=2)
left = Content.getValues(table, record, mainFields)
if inPower:
otherFields = Content.getMetaFields("site", "box", level=4)
right = Content.getValues(table, record, otherFields)
else:
right = ""
return self.page("home", left=left, right=right)
def projects(self):
"""The page with the list of projects.
Returns
-------
response
"""
Content = self.Content
(table, recordId, record) = Content.relevant()
if recordId is None:
left = None
else:
projects = Content.getProjects()
left = projects
return self.page("projects", left=left)
def admin(self):
"""The page with the controls on the relevant projects, editions, and users.
Returns
-------
response
"""
Content = self.Content
(table, recordId, record) = Content.relevant()
if recordId is None:
left = None
else:
(left, right) = Content.getAdmin()
return self.page("admin", left=left, right=right)
def createUser(self, user):
"""Creates a new test user.
After the operation:
* *success*: goes to admin url, good status
* *failure*: goes to admin url, error status
Returns
-------
response
"""
Messages = self.Messages
Content = self.Content
result = Content.createUser(user)
good = result.get("status", False)
if good:
user = result["name"]
Messages.info(logmsg=f"Created user {user}", msg=f"user {user} created")
else:
Messages.warning(
logmsg=f"Could not create new user {user}",
msg=f"failed to create new user {user}",
)
for kind, msg in result.get("messages", []):
Messages.message(kind, msg)
newUrl = "/admin"
return redirectStatus(newUrl, good)
def deleteUser(self, user):
"""Deletes a test user.
After the operation:
* *success*: goes to admin url, good status
* *failure*: goes to admin url, error status
Returns
-------
response
"""
Messages = self.Messages
Content = self.Content
result = Content.deleteUser(user)
good = result.get("status", False)
if good:
Messages.info(logmsg=f"Deleted user {user}", msg=f"user {user} deleted")
else:
Messages.warning(
logmsg=f"Could not delete user {user}",
msg=f"failed to delete new user {user}",
)
for kind, msg in result.get("messages", []):
Messages.message(kind, msg)
newUrl = "/admin"
return redirectStatus(newUrl, good)
def createProject(self, site):
"""Creates a project and shows the new project.
The current user is linked to this project as organiser.
After the operation:
* *success*: goes to new project url, good status
* *failure*: goes to all projects url, error status
Returns
-------
response
Returns
-------
response
"""
Messages = self.Messages
Content = self.Content
projectId = Content.createProject(site)
if projectId is None:
Messages.warning(
logmsg="Could not create new project",
msg="failed to create new project",
)
newUrl = "/project"
else:
Messages.info(
logmsg=f"Created project {projectId}", msg="new project created"
)
newUrl = f"/project/{projectId}"
return redirectStatus(newUrl, projectId is not None)
def project(self, project):
"""The landing page of a project.
Parameters
----------
project: string | ObjectId | AttrDict
The project in question.
Returns
-------
response
"""
Settings = self.Settings
H = Settings.H
Mongo = self.Mongo
Content = self.Content
Backup = self.Backup
runProd = Settings.runProd
(projectId, project) = Mongo.get("project", project)
publishInfo = Content.getPublishInfo("project", project)
actionHeading = H.h(3, "Actions")
downloadButton = Content.getDownload("project", project)
backups = "" if runProd else Backup.getBackups(project=project)
editionHeading = H.h(3, "Editions")
editions = Content.getEditions(project)
mainFields = Content.getMetaFields("project", "main", level=3)
otherFields = Content.getMetaFields("project", ["narrative", "box"], level=3)
icon = Content.getUpload(project, "iconProject")
left = (
icon
+ Content.getValues("project", project, mainFields)
+ publishInfo
+ actionHeading
+ downloadButton
+ backups
+ editionHeading
+ editions
)
right = Content.getValues("project", project, otherFields)
return self.page("projects", left=left, right=right)
def createEdition(self, project):
"""Inserts an edition into a project and shows the new edition.
The current user is linked to this edition as editor.
After the operation:
* *success*: goes to new edition url, good status
* *failure*: goes to project url, error status
Returns
-------
response
Parameters
----------
project: string | ObjectId | AttrDict
The project to which the edition belongs.
Returns
-------
response
"""
Messages = self.Messages
Mongo = self.Mongo
Content = self.Content
(projectId, project) = Mongo.get("project", project)
if projectId is None:
return redirectStatus("/home", False)
editionId = Content.createEdition(project)
if editionId is None:
Messages.warning(
logmsg="Could not create new edition",
msg="failed to create new edition",
)
newUrl = f"/project/{projectId}"
else:
Messages.info(
logmsg=f"Created edition {editionId}", msg="new edition created"
)
newUrl = f"/edition/{editionId}"
return redirectStatus(newUrl, editionId is not None)
def edition(self, edition, version=None, action=None):
"""The landing page of an edition, possibly with a scene marked as active.
An edition knows the scene it should display and the viewer that was
used to create the scene.
If action is not None, its value determines which viewer will be loaded
in the 3D viewer.
It is dependent on the parameters and/or defaults
in which viewer/version/mode.
If version is not None, this will override the default version.
Parameters
----------
edition: string | ObjectId | AttrDict
The editionin quesion.
From the edition record we can find the project too.
version: string, optional None
The viewer version to use.
action: string, optional None
The mode in which the viewer is to be used (`read` or `update`).
Returns
-------
response
"""
Settings = self.Settings
H = Settings.H
Viewers = self.Viewers
Content = self.Content
Mongo = self.Mongo
Auth = self.Auth
tocFile = Settings.tocFile
(editionId, edition) = Mongo.get("edition", edition)
if edition is None:
return redirectStatus("/project", True)
projectId = edition.projectId
(projectId, project) = Mongo.get("project", projectId)
if project is None:
return redirectStatus("/project", True)
(viewer, sceneFile) = Viewers.getViewInfo(edition)
breadCrumb = Content.breadCrumb(project)
publishButton = Content.getPublishInfo("edition", edition)
actionHeading = H.h(3, "Actions")
downloadButton = Content.getDownload("edition", edition)
if action is None:
action = "read"
action = Auth.makeSafe("edition", edition, action)
sceneHeading = H.h(3, "Scene")
sceneMaterial = (
""
if False and action is None
else Content.getScene(projectId, edition, version=version, action=action)
)
mainFields = Content.getMetaFields("edition", "main", level=4)
otherFields = Content.getMetaFields("edition", ["narrative", "box"], level=4)
icon = Content.getUpload(edition, "iconEdition")
left = (
breadCrumb
+ icon
+ Content.getValues("edition", edition, mainFields)
+ publishButton
+ actionHeading
+ downloadButton
+ sceneHeading
+ sceneMaterial
)
right = Content.getValues(
"edition", edition, otherFields
) + Content.getDataFile("edition", edition, tocFile, content=True, lenient=True)
return self.page("projects", left=left, right=right)
def fromPub(self, projectIdGiven, editionIdGiven):
"""Redirect to a project or edition or the home page.
If the edition or project does not exist, show a friendly message.
"""
Mongo = self.Mongo
Messages = self.Messages
Settings = self.Settings
backPrefix = Settings.backPrefix
authorLabel = Settings.authorLabel
homeUrl = "/"
if projectIdGiven is None and editionIdGiven is None:
return redirectStatus(homeUrl, True)
projectIdVerified = None
editionIdVerified = None
if editionIdGiven is not None:
(editionIdVerified, editionVerified) = Mongo.get("edition", editionIdGiven)
if editionVerified is not None:
projectIdVerifiedFromEdition = editionVerified.projectId
if projectIdGiven is not None:
(projectIdVerified, projectVerified) = Mongo.get("project", projectIdGiven)
if editionIdGiven is not None:
if editionIdVerified is None:
Messages.error(
msg=f"This edition no longer exists in {authorLabel}",
logmsg=f"{backPrefix}: Edition {editionIdGiven} no longer exists",
)
newUrl = (
homeUrl
if projectIdVerified is None
else f"/project/{projectIdVerified}"
)
else:
if projectIdGiven and projectIdVerified != projectIdVerifiedFromEdition:
Messages.warning(
msg=(
"Found the edition but in a different project "
f"in {authorLabel}"
),
logmsg=(
f"{backPrefix}: Edition {editionIdGiven} does not"
f"belong to project {projectIdGiven}"
),
)
else:
Messages.good(
msg=("Found the edition and project " f"in {authorLabel}"),
)
newUrl = f"/edition/{editionIdVerified}"
else: # now projectIdGiven is not None
if projectIdVerified is None:
Messages.error(
msg=f"This project no longer exists in {authorLabel}",
logmsg=f"{backPrefix}: Project {projectIdGiven} no longer exists",
)
newUrl = homeUrl
else:
Messages.good(
msg=("Found the project " f"in {authorLabel}"),
)
newUrl = f"/project/{projectIdVerified}"
return redirectStatus(newUrl, True)
def deleteItem(self, table, record):
"""Deletes an item, project or edition.
After the operation:
* *success*: goes to all-projects url or master project url, good status
* *failure*: goes to back referrer url, error status
Parameters
----------
table: string
The kind of item: `project` or `edition`.
record: string | ObjectId | AttrDict
The item in question.
Returns
-------
response
"""
Mongo = self.Mongo
Content = self.Content
ref = getReferrer().removeprefix("/")
back = f"/{ref}"
(recordId, record) = Mongo.get(table, record)
if recordId is None:
return redirectStatus(back, False)
result = Content.deleteItem(table, record)
if result:
back = "/project"
if table == "edition":
projectId = record.projectId
back += f"/{projectId}"
return redirectStatus(back, True)
def viewerFrame(self, edition, version, action, subMode):
"""The page loaded in an iframe where a 3D viewer operates.
Parameters
----------
edition: string | ObjectId | AttrDict
The edition that is shown.
version: string | None
The version to use.
action: string | None
The mode in which the viewer is to be used (`read` or `update`).
subMode: string | None
The sub mode in which the viewer is to be used (`update` or `create`).
Returns
-------
response
"""
Mongo = self.Mongo
Viewers = self.Viewers
Auth = self.Auth
(editionId, edition) = Mongo.get("edition", edition)
if editionId is None:
return renderTemplate("viewer", viewerCode="")
(viewer, sceneFile) = Viewers.getViewInfo(edition)
projectId = edition.projectId
urlBase = f"project/{projectId}/edition/{editionId}/"
action = Auth.makeSafe("edition", edition, action)
viewerCode = (
""
if action is None or sceneFile is None
else Viewers.genHtml(urlBase, sceneFile, viewer, version, action, subMode)
)
return renderTemplate("viewer", viewerCode=viewerCode)
def viewerResource(self, path):
"""Components requested by viewers.
This is the javascript code, the css, and other resources
that are part of the 3D viewer software.
Parameters
----------
path: string
Path on the file system under the viewers base directory
where the resource resides.
Returns
-------
response
The response consists of the contents of the
file plus headers derived from the path.
If the file does not exist, a 404 is returned.
"""
Content = self.Content
dataPath = Content.getViewerFile(path)
return sendFile(dataPath)
def fileData(self, path, project=None, edition=None):
"""Data content requested directly from the file repository.
This is
* the material requested by the viewers:
the scene json itself and additional resources,
that are part of the user contributed content that is under
control of the viewer: annotations, media, etc.
* icons for the site, projects, and editions
Parameters
----------
path: string
Path on the file system under the data directory
where the resource resides.
The path is relative to the project, and, if given, the edition.
project: string | ObjectId | AttrDict
The id of a project under which the resource is to be found.
If None, it is site-wide material.
edition: string | ObjectId | AttrDict
If not None, the name of an edition under which the resource
is to be found.
Returns
-------
response
The response consists of the contents of the
file plus headers derived from the path.
If the file does not exist, a 404 is returned.
"""
Content = self.Content
(table, recordId, record) = Content.relevant(project=project, edition=edition)
if recordId is None:
return ""
dataPath = Content.getDataFile(table, record, path)
return sendFile(dataPath)
def upload(self, record, key, path, targetFileName=None):
"""Upload a file.
Parameters
----------
record: string | ObjectId | AttrDict
The context record of the upload
key: string
The key of the upload
path: string
The save location for the file
targetFileName: string, optional None
The name of the file as which the uploaded file will be saved;
if is None, the file will be saved with the name from the request.
Returns
-------
response
With json data containing a status and a content member.
The content is new content to display the upload widget with.
"""
Content = self.Content
parts = path.rstrip("/").rsplit("/", 1)
fileName = parts[-1]
path = parts[0] if len(parts) == 2 else ""
return Content.saveFile(
record, key, path, fileName, targetFileName=targetFileName
)
def deleteFile(self, record, key, path, targetFileName=None):
"""Delete a file.
Parameters
----------
record: string | ObjectId | AttrDict
The context record of the upload.
key: string
The key of the upload.
path: string
The location of the file.
targetFileName: string, optional None
The name of the file.
Returns
-------
response
With json data containing a status, msg, and content members.
The content is new content to display the upload widget with.
"""
Content = self.Content
parts = path.rstrip("/").rsplit("/", 1)
fileName = parts[-1]
path = parts[0] if len(parts) == 2 else ""
return Content.deleteFile(
record, key, path, fileName, targetFileName=targetFileName
)
def authWebdav(self, project, edition, method, path, action):
"""Authorises a webdav request.
When a viewer makes a WebDAV request to the server,
that request is first checked here for authorisation.
See `control.webdavapp.dispatchWebdav()`.
Parameters
----------
project: string | ObjectId | AttrDict
The project in question.
edition: string | ObjectId | AttrDict
The edition in question.
path: string
The path relative to the directory of the edition.
action: string
The operation that the WebDAV request wants to do on the data
(`read` or `update`).
Returns
-------
boolean
Whether the action is permitted on ths data by the current user.
"""
Messages = self.Messages
Mongo = self.Mongo
Auth = self.Auth
User = Auth.myDetails()
user = User.user
(projectId, project) = Mongo.get("project", project)
if projectId is None:
return False
(editionId, edition) = Mongo.get("edition", edition)
if editionId is None:
return False
permitted = Auth.authorise("edition", record=edition, action=action)
if not permitted:
User = Auth.myDetails()
user = User.user
name = User.nickname
Messages.info(
logmsg=f"WEBDav unauthorised by user {name} ({user})"
f" on edition {editionId} path {path}"
)
return permitted
def remaining(self, path):
"""When the url of the request is not recognized.
Parameters
----------
path: string
The url (without leading /) that is not recognized.
Returns
-------
response
Either a redirect to the referred, for some
recognized urls that correspond to not-yet
implemented one. Or a redirect 303 for all other
cases.
"""
Messages = self.Messages
def splitUrl(url):
"""Auxiliary inner function."""
url = url.strip("/")
parts = url.rsplit("/", 1)
lastPart = parts[-1]
firstPart = parts[0] if len(parts) > 1 else ""
firstPart = f"/{firstPart}"
return (firstPart, lastPart)
(firstPath, lastPath) = splitUrl(path)
if lastPath in {"read", "update", "delete"}:
Messages.warning(
logmsg=f"Not yet implemented /{lastPath}: /{path}",
msg=f"Not yet implemented: /{lastPath}",
)
ref = getReferrer()
(firstRef, lastRef) = splitUrl(ref)
back = firstRef if lastRef in {"read", "update", "delete"} else f"/{ref}"
return redirectStatus(back, True)
Messages.warning(logmsg=f"Not found: /{path}")
return redirectStatus("/", False)
def page(self, url, left=None, right=None):
"""Workhorse function to get content on the page.
Parameters
----------
url: string
Initial part of the url that triggered the page function.
This part is used to make one of the tabs on the web page active.
left: string, optional None
Content for the left column of the page.
right: string, optional None
Content for the right column of the page.
"""
Settings = self.Settings
Content = self.Content
Backup = self.Backup
Auth = self.Auth
(siteTable, siteId, site) = Content.relevant()
blogUrl = Content.getValue(siteTable, site, "blog", manner="bare")
navigation = self.navigation(url, blogUrl)
(specialLoginWidget, loginWidget) = Auth.wrapLogin()
banner = Settings.banner
if Backup is not None:
banner = banner.replace("«backups»", Backup.getBackups(project=None))
(table, recordId, record) = Content.relevant()
if recordId is None:
return renderTemplate(
"index",
banner=banner,
versionInfo=Settings.versionInfo,
navigation=navigation,
materialLeft=left or "",
materialRight=right or "",
specialLoginWidget=specialLoginWidget,
loginWidget=loginWidget,
iconSite="",
)
iconSite = Content.getUpload(record, "iconSite")
return renderTemplate(
"index",
banner=banner,
versionInfo=Settings.versionInfo,
navigation=navigation,
materialLeft=left or "",
materialRight=right or "",
specialLoginWidget=specialLoginWidget,
loginWidget=loginWidget,
iconSite=iconSite,
)
def navigation(self, url, blogUrl):
"""Generates the navigation controls.
Especially the tab bar.
Parameters
----------
url: string
Initial part of the url on the basis of which one of the
tabs can be made active.
Returns
-------
string
The HTML of the navigation.
"""
Settings = self.Settings
H = Settings.H
pubUrl = Settings.pubUrl
published = Settings.published
# 1st column: url
# 2nd column: interface string
# 3rd column: True: enabled, False: disabled
# 4th column: implemented
TABS = (
("home", "Home", True, True),
("project", "3D Projects", True, True),
("admin", "My Work", True, True),
("advancedsearch", "Advanced Search", False, False),
(None, "|", True, True),
(pubUrl, "Published Projects ⌲", True, True),
(blogUrl, "Blog ⌲", True, True),
)
search = H.span(
[
H.input(
"",
"search",
name="search",
placeholder="search item",
cls="button disabled",
disabled="",
style="display:none",
),
H.input(
"Search",
"submit",
cls="button disabled",
disabled="",
style="display:none",
),
],
cls="search-bar",
)
divContent = []
for tab, label, enabled, implemented in TABS:
if not implemented:
continue
active = "active" if url == tab else ""
if tab is None:
elem = "span"
href = []
target = {}
fullCls = "large"
else:
if enabled:
elem = "a"
cls = active
href = [tab if "/" in tab else f"/{tab}"]
target = dict(target=published) if "/" in tab else {}
else:
elem = "span"
cls = "disabled"
href = []
target = {}
fullCls = f"button large {cls}"
divContent.append(H.elem(elem, label, *href, cls=fullCls, **target))
divContent.append(search)
return H.div(divContent, cls="tabs")
Classes
class Pages (Settings, Viewers, Messages, Mongo, Content, Backup, Auth)
-
Making responses that can be displayed as web pages.
This class has methods that correspond to routes in the app, for which they get the data (using
Content
), which gets then wrapped in HTML.It is instantiated by a singleton object.
Most methods generate a response that contains the content of a complete page. For those methods we do not document the return value.
Some methods return something different. If so, it the return value will be documented.
Parameters
Settings
:AttrDict
- App-wide configuration data obtained from
Config.Settings
. Viewers
:object
- Singleton instance of
Viewers
. Messages
:object
- Singleton instance of
Messages
. Mongo
:object
- Singleton instance of
Mongo
. Content
:object
- Singleton instance of
Content
. Auth
:object
- Singleton instance of
Auth
.
Expand source code Browse git
class Pages: def __init__(self, Settings, Viewers, Messages, Mongo, Content, Backup, Auth): """Making responses that can be displayed as web pages. This class has methods that correspond to routes in the app, for which they get the data (using `control.content.Content`), which gets then wrapped in HTML. It is instantiated by a singleton object. Most methods generate a response that contains the content of a complete page. For those methods we do not document the return value. Some methods return something different. If so, it the return value will be documented. Parameters ---------- Settings: AttrDict App-wide configuration data obtained from `control.config.Config.Settings`. Viewers: object Singleton instance of `control.viewers.Viewers`. Messages: object Singleton instance of `control.messages.Messages`. Mongo: object Singleton instance of `control.mongo.Mongo`. Content: object Singleton instance of `control.content.Content`. Auth: object Singleton instance of `control.auth.Auth`. """ self.Settings = Settings self.Viewers = Viewers self.Messages = Messages Messages.debugAdd(self) self.Mongo = Mongo self.Content = Content self.Backup = Backup self.Auth = Auth def precheck(self, edition): """Check the articles of an edition prior to publishing. Parameters ---------- edition: string the edition After the operation: Goes back to the referrer url. The check operation will have generated a table of contents for the articles and media files, and these will be shown on the edition page. Returns ------- response """ Content = self.Content good = Content.precheck(edition) ref = getReferrer().removeprefix("/") return redirectStatus(f"/{ref}", good) def publish(self, edition, force): """Publish an edition as static pages. Parameters ---------- edition: string the edition force: boolean If True, ignore when some checks fail After the operation: * *success*: goes back to referrer url, good status * *failure*: goes back to referrer url, error status Returns ------- response """ Content = self.Content good = Content.publish(edition, force) ref = getReferrer().removeprefix("/") return redirectStatus(f"/{ref}", good) def republish(self, edition, force): """Re-publish an edition as static pages. Parameters ---------- edition: string the edition force: boolean If True, ignore when some checks fail After the operation: * *success*: goes back to referrer url, good status * *failure*: goes back to referrer url, error status Returns ------- response """ Content = self.Content good = Content.republish(edition, force) ref = getReferrer().removeprefix("/") return redirectStatus(f"/{ref}", good) def unpublish(self, edition): """Unpublish an edition from the static pages. Parameters ---------- edition: string the edition After the operation: * *success*: goes back to referrer url, good status * *failure*: goes back to referrer url, error status Returns ------- response """ Content = self.Content good = Content.unpublish(edition) ref = getReferrer().removeprefix("/") return redirectStatus(f"/{ref}", good) def generate(self): """Regenerate the static HTML pages for the whole published site. After the operation: * *success*: goes back to referrer url, good status * *failure*: goes back to referrer url, error status Returns ------- response """ Content = self.Content good = Content.generate() ref = getReferrer().removeprefix("/") return redirectStatus(f"/{ref}", good) def mkBackup(self, project=None): """Backup: Save file and database data in a backup directory. Parameters ---------- project: string, optional None If given, only backs up the given project. After the operation: * *success*: goes back to referrer url, good status * *failure*: goes back to referrer url, error status Returns ------- response """ Messages = self.Messages Backup = self.Backup Auth = self.Auth if not Auth.mayBackup(project=project): Messages.warning( msg="Making a backup is not allowed", logmsg=("Making a backup is not allowed"), ) ref = getReferrer().removeprefix("/") return redirectStatus(f"/{ref}", False) good = Backup.mkBackup(project=project) ref = getReferrer().removeprefix("/") return redirectStatus(f"/{ref}", good) def restoreBackup(self, backup, project=None): """Restore from a backup. Make a new backup first. After the operation: * *success*: * site-wide restore: goes to logout url, good status * project-specific restore: goes to project url, good status * *failure*: goes back to referrer url, error status Parameters ---------- backup: string The name of the backup as stored in the backups directory on the server. project: string, optional None If given, restores the given project. Returns ------- response """ Messages = self.Messages Mongo = self.Mongo Backup = self.Backup Auth = self.Auth (projectId, project) = Mongo.get("project", project) if not Auth.mayBackup(project=project): Messages.warning( msg="Restoring from a backup is not allowed", logmsg=("Restoring from a backup is not allowed"), ) ref = getReferrer().removeprefix("/") return redirectStatus(f"/{ref}", False) good = Backup.restoreBackup(backup, project=project) back = "/alogout" if project is None else f"/project/{projectId}" return redirectStatus(back, good) def delBackup(self, backup, project=None): """Deletes a backup. After the operation: * *success*: goes back to referrer url, good status * *failure*: goes back to referrer url, error status Parameters ---------- backup: string The name of the backup as stored in the backups directory on the server. project: string, optional None If given, deletes a backup of the given project. Returns ------- response """ Messages = self.Messages Mongo = self.Mongo Backup = self.Backup Auth = self.Auth (projectId, project) = Mongo.get("project", project) ref = getReferrer().removeprefix("/") back = f"/{ref}" if not Auth.mayBackup(project=project): Messages.warning( msg="Deleting a backup is not allowed", logmsg=("Deleting a backup is not allowed"), ) return redirectStatus(back, False) good = Backup.delBackup(backup, project=project) return redirectStatus(back, good) def home(self): """The site-wide home page. Returns ------- response """ Content = self.Content Auth = self.Auth inPower = Auth.inPower()[0] (table, recordId, record) = Content.relevant() if recordId is None: (left, right) = (None, None) else: mainFields = Content.getMetaFields("site", ("main", "narrative"), level=2) left = Content.getValues(table, record, mainFields) if inPower: otherFields = Content.getMetaFields("site", "box", level=4) right = Content.getValues(table, record, otherFields) else: right = "" return self.page("home", left=left, right=right) def projects(self): """The page with the list of projects. Returns ------- response """ Content = self.Content (table, recordId, record) = Content.relevant() if recordId is None: left = None else: projects = Content.getProjects() left = projects return self.page("projects", left=left) def admin(self): """The page with the controls on the relevant projects, editions, and users. Returns ------- response """ Content = self.Content (table, recordId, record) = Content.relevant() if recordId is None: left = None else: (left, right) = Content.getAdmin() return self.page("admin", left=left, right=right) def createUser(self, user): """Creates a new test user. After the operation: * *success*: goes to admin url, good status * *failure*: goes to admin url, error status Returns ------- response """ Messages = self.Messages Content = self.Content result = Content.createUser(user) good = result.get("status", False) if good: user = result["name"] Messages.info(logmsg=f"Created user {user}", msg=f"user {user} created") else: Messages.warning( logmsg=f"Could not create new user {user}", msg=f"failed to create new user {user}", ) for kind, msg in result.get("messages", []): Messages.message(kind, msg) newUrl = "/admin" return redirectStatus(newUrl, good) def deleteUser(self, user): """Deletes a test user. After the operation: * *success*: goes to admin url, good status * *failure*: goes to admin url, error status Returns ------- response """ Messages = self.Messages Content = self.Content result = Content.deleteUser(user) good = result.get("status", False) if good: Messages.info(logmsg=f"Deleted user {user}", msg=f"user {user} deleted") else: Messages.warning( logmsg=f"Could not delete user {user}", msg=f"failed to delete new user {user}", ) for kind, msg in result.get("messages", []): Messages.message(kind, msg) newUrl = "/admin" return redirectStatus(newUrl, good) def createProject(self, site): """Creates a project and shows the new project. The current user is linked to this project as organiser. After the operation: * *success*: goes to new project url, good status * *failure*: goes to all projects url, error status Returns ------- response Returns ------- response """ Messages = self.Messages Content = self.Content projectId = Content.createProject(site) if projectId is None: Messages.warning( logmsg="Could not create new project", msg="failed to create new project", ) newUrl = "/project" else: Messages.info( logmsg=f"Created project {projectId}", msg="new project created" ) newUrl = f"/project/{projectId}" return redirectStatus(newUrl, projectId is not None) def project(self, project): """The landing page of a project. Parameters ---------- project: string | ObjectId | AttrDict The project in question. Returns ------- response """ Settings = self.Settings H = Settings.H Mongo = self.Mongo Content = self.Content Backup = self.Backup runProd = Settings.runProd (projectId, project) = Mongo.get("project", project) publishInfo = Content.getPublishInfo("project", project) actionHeading = H.h(3, "Actions") downloadButton = Content.getDownload("project", project) backups = "" if runProd else Backup.getBackups(project=project) editionHeading = H.h(3, "Editions") editions = Content.getEditions(project) mainFields = Content.getMetaFields("project", "main", level=3) otherFields = Content.getMetaFields("project", ["narrative", "box"], level=3) icon = Content.getUpload(project, "iconProject") left = ( icon + Content.getValues("project", project, mainFields) + publishInfo + actionHeading + downloadButton + backups + editionHeading + editions ) right = Content.getValues("project", project, otherFields) return self.page("projects", left=left, right=right) def createEdition(self, project): """Inserts an edition into a project and shows the new edition. The current user is linked to this edition as editor. After the operation: * *success*: goes to new edition url, good status * *failure*: goes to project url, error status Returns ------- response Parameters ---------- project: string | ObjectId | AttrDict The project to which the edition belongs. Returns ------- response """ Messages = self.Messages Mongo = self.Mongo Content = self.Content (projectId, project) = Mongo.get("project", project) if projectId is None: return redirectStatus("/home", False) editionId = Content.createEdition(project) if editionId is None: Messages.warning( logmsg="Could not create new edition", msg="failed to create new edition", ) newUrl = f"/project/{projectId}" else: Messages.info( logmsg=f"Created edition {editionId}", msg="new edition created" ) newUrl = f"/edition/{editionId}" return redirectStatus(newUrl, editionId is not None) def edition(self, edition, version=None, action=None): """The landing page of an edition, possibly with a scene marked as active. An edition knows the scene it should display and the viewer that was used to create the scene. If action is not None, its value determines which viewer will be loaded in the 3D viewer. It is dependent on the parameters and/or defaults in which viewer/version/mode. If version is not None, this will override the default version. Parameters ---------- edition: string | ObjectId | AttrDict The editionin quesion. From the edition record we can find the project too. version: string, optional None The viewer version to use. action: string, optional None The mode in which the viewer is to be used (`read` or `update`). Returns ------- response """ Settings = self.Settings H = Settings.H Viewers = self.Viewers Content = self.Content Mongo = self.Mongo Auth = self.Auth tocFile = Settings.tocFile (editionId, edition) = Mongo.get("edition", edition) if edition is None: return redirectStatus("/project", True) projectId = edition.projectId (projectId, project) = Mongo.get("project", projectId) if project is None: return redirectStatus("/project", True) (viewer, sceneFile) = Viewers.getViewInfo(edition) breadCrumb = Content.breadCrumb(project) publishButton = Content.getPublishInfo("edition", edition) actionHeading = H.h(3, "Actions") downloadButton = Content.getDownload("edition", edition) if action is None: action = "read" action = Auth.makeSafe("edition", edition, action) sceneHeading = H.h(3, "Scene") sceneMaterial = ( "" if False and action is None else Content.getScene(projectId, edition, version=version, action=action) ) mainFields = Content.getMetaFields("edition", "main", level=4) otherFields = Content.getMetaFields("edition", ["narrative", "box"], level=4) icon = Content.getUpload(edition, "iconEdition") left = ( breadCrumb + icon + Content.getValues("edition", edition, mainFields) + publishButton + actionHeading + downloadButton + sceneHeading + sceneMaterial ) right = Content.getValues( "edition", edition, otherFields ) + Content.getDataFile("edition", edition, tocFile, content=True, lenient=True) return self.page("projects", left=left, right=right) def fromPub(self, projectIdGiven, editionIdGiven): """Redirect to a project or edition or the home page. If the edition or project does not exist, show a friendly message. """ Mongo = self.Mongo Messages = self.Messages Settings = self.Settings backPrefix = Settings.backPrefix authorLabel = Settings.authorLabel homeUrl = "/" if projectIdGiven is None and editionIdGiven is None: return redirectStatus(homeUrl, True) projectIdVerified = None editionIdVerified = None if editionIdGiven is not None: (editionIdVerified, editionVerified) = Mongo.get("edition", editionIdGiven) if editionVerified is not None: projectIdVerifiedFromEdition = editionVerified.projectId if projectIdGiven is not None: (projectIdVerified, projectVerified) = Mongo.get("project", projectIdGiven) if editionIdGiven is not None: if editionIdVerified is None: Messages.error( msg=f"This edition no longer exists in {authorLabel}", logmsg=f"{backPrefix}: Edition {editionIdGiven} no longer exists", ) newUrl = ( homeUrl if projectIdVerified is None else f"/project/{projectIdVerified}" ) else: if projectIdGiven and projectIdVerified != projectIdVerifiedFromEdition: Messages.warning( msg=( "Found the edition but in a different project " f"in {authorLabel}" ), logmsg=( f"{backPrefix}: Edition {editionIdGiven} does not" f"belong to project {projectIdGiven}" ), ) else: Messages.good( msg=("Found the edition and project " f"in {authorLabel}"), ) newUrl = f"/edition/{editionIdVerified}" else: # now projectIdGiven is not None if projectIdVerified is None: Messages.error( msg=f"This project no longer exists in {authorLabel}", logmsg=f"{backPrefix}: Project {projectIdGiven} no longer exists", ) newUrl = homeUrl else: Messages.good( msg=("Found the project " f"in {authorLabel}"), ) newUrl = f"/project/{projectIdVerified}" return redirectStatus(newUrl, True) def deleteItem(self, table, record): """Deletes an item, project or edition. After the operation: * *success*: goes to all-projects url or master project url, good status * *failure*: goes to back referrer url, error status Parameters ---------- table: string The kind of item: `project` or `edition`. record: string | ObjectId | AttrDict The item in question. Returns ------- response """ Mongo = self.Mongo Content = self.Content ref = getReferrer().removeprefix("/") back = f"/{ref}" (recordId, record) = Mongo.get(table, record) if recordId is None: return redirectStatus(back, False) result = Content.deleteItem(table, record) if result: back = "/project" if table == "edition": projectId = record.projectId back += f"/{projectId}" return redirectStatus(back, True) def viewerFrame(self, edition, version, action, subMode): """The page loaded in an iframe where a 3D viewer operates. Parameters ---------- edition: string | ObjectId | AttrDict The edition that is shown. version: string | None The version to use. action: string | None The mode in which the viewer is to be used (`read` or `update`). subMode: string | None The sub mode in which the viewer is to be used (`update` or `create`). Returns ------- response """ Mongo = self.Mongo Viewers = self.Viewers Auth = self.Auth (editionId, edition) = Mongo.get("edition", edition) if editionId is None: return renderTemplate("viewer", viewerCode="") (viewer, sceneFile) = Viewers.getViewInfo(edition) projectId = edition.projectId urlBase = f"project/{projectId}/edition/{editionId}/" action = Auth.makeSafe("edition", edition, action) viewerCode = ( "" if action is None or sceneFile is None else Viewers.genHtml(urlBase, sceneFile, viewer, version, action, subMode) ) return renderTemplate("viewer", viewerCode=viewerCode) def viewerResource(self, path): """Components requested by viewers. This is the javascript code, the css, and other resources that are part of the 3D viewer software. Parameters ---------- path: string Path on the file system under the viewers base directory where the resource resides. Returns ------- response The response consists of the contents of the file plus headers derived from the path. If the file does not exist, a 404 is returned. """ Content = self.Content dataPath = Content.getViewerFile(path) return sendFile(dataPath) def fileData(self, path, project=None, edition=None): """Data content requested directly from the file repository. This is * the material requested by the viewers: the scene json itself and additional resources, that are part of the user contributed content that is under control of the viewer: annotations, media, etc. * icons for the site, projects, and editions Parameters ---------- path: string Path on the file system under the data directory where the resource resides. The path is relative to the project, and, if given, the edition. project: string | ObjectId | AttrDict The id of a project under which the resource is to be found. If None, it is site-wide material. edition: string | ObjectId | AttrDict If not None, the name of an edition under which the resource is to be found. Returns ------- response The response consists of the contents of the file plus headers derived from the path. If the file does not exist, a 404 is returned. """ Content = self.Content (table, recordId, record) = Content.relevant(project=project, edition=edition) if recordId is None: return "" dataPath = Content.getDataFile(table, record, path) return sendFile(dataPath) def upload(self, record, key, path, targetFileName=None): """Upload a file. Parameters ---------- record: string | ObjectId | AttrDict The context record of the upload key: string The key of the upload path: string The save location for the file targetFileName: string, optional None The name of the file as which the uploaded file will be saved; if is None, the file will be saved with the name from the request. Returns ------- response With json data containing a status and a content member. The content is new content to display the upload widget with. """ Content = self.Content parts = path.rstrip("/").rsplit("/", 1) fileName = parts[-1] path = parts[0] if len(parts) == 2 else "" return Content.saveFile( record, key, path, fileName, targetFileName=targetFileName ) def deleteFile(self, record, key, path, targetFileName=None): """Delete a file. Parameters ---------- record: string | ObjectId | AttrDict The context record of the upload. key: string The key of the upload. path: string The location of the file. targetFileName: string, optional None The name of the file. Returns ------- response With json data containing a status, msg, and content members. The content is new content to display the upload widget with. """ Content = self.Content parts = path.rstrip("/").rsplit("/", 1) fileName = parts[-1] path = parts[0] if len(parts) == 2 else "" return Content.deleteFile( record, key, path, fileName, targetFileName=targetFileName ) def authWebdav(self, project, edition, method, path, action): """Authorises a webdav request. When a viewer makes a WebDAV request to the server, that request is first checked here for authorisation. See `control.webdavapp.dispatchWebdav()`. Parameters ---------- project: string | ObjectId | AttrDict The project in question. edition: string | ObjectId | AttrDict The edition in question. path: string The path relative to the directory of the edition. action: string The operation that the WebDAV request wants to do on the data (`read` or `update`). Returns ------- boolean Whether the action is permitted on ths data by the current user. """ Messages = self.Messages Mongo = self.Mongo Auth = self.Auth User = Auth.myDetails() user = User.user (projectId, project) = Mongo.get("project", project) if projectId is None: return False (editionId, edition) = Mongo.get("edition", edition) if editionId is None: return False permitted = Auth.authorise("edition", record=edition, action=action) if not permitted: User = Auth.myDetails() user = User.user name = User.nickname Messages.info( logmsg=f"WEBDav unauthorised by user {name} ({user})" f" on edition {editionId} path {path}" ) return permitted def remaining(self, path): """When the url of the request is not recognized. Parameters ---------- path: string The url (without leading /) that is not recognized. Returns ------- response Either a redirect to the referred, for some recognized urls that correspond to not-yet implemented one. Or a redirect 303 for all other cases. """ Messages = self.Messages def splitUrl(url): """Auxiliary inner function.""" url = url.strip("/") parts = url.rsplit("/", 1) lastPart = parts[-1] firstPart = parts[0] if len(parts) > 1 else "" firstPart = f"/{firstPart}" return (firstPart, lastPart) (firstPath, lastPath) = splitUrl(path) if lastPath in {"read", "update", "delete"}: Messages.warning( logmsg=f"Not yet implemented /{lastPath}: /{path}", msg=f"Not yet implemented: /{lastPath}", ) ref = getReferrer() (firstRef, lastRef) = splitUrl(ref) back = firstRef if lastRef in {"read", "update", "delete"} else f"/{ref}" return redirectStatus(back, True) Messages.warning(logmsg=f"Not found: /{path}") return redirectStatus("/", False) def page(self, url, left=None, right=None): """Workhorse function to get content on the page. Parameters ---------- url: string Initial part of the url that triggered the page function. This part is used to make one of the tabs on the web page active. left: string, optional None Content for the left column of the page. right: string, optional None Content for the right column of the page. """ Settings = self.Settings Content = self.Content Backup = self.Backup Auth = self.Auth (siteTable, siteId, site) = Content.relevant() blogUrl = Content.getValue(siteTable, site, "blog", manner="bare") navigation = self.navigation(url, blogUrl) (specialLoginWidget, loginWidget) = Auth.wrapLogin() banner = Settings.banner if Backup is not None: banner = banner.replace("«backups»", Backup.getBackups(project=None)) (table, recordId, record) = Content.relevant() if recordId is None: return renderTemplate( "index", banner=banner, versionInfo=Settings.versionInfo, navigation=navigation, materialLeft=left or "", materialRight=right or "", specialLoginWidget=specialLoginWidget, loginWidget=loginWidget, iconSite="", ) iconSite = Content.getUpload(record, "iconSite") return renderTemplate( "index", banner=banner, versionInfo=Settings.versionInfo, navigation=navigation, materialLeft=left or "", materialRight=right or "", specialLoginWidget=specialLoginWidget, loginWidget=loginWidget, iconSite=iconSite, ) def navigation(self, url, blogUrl): """Generates the navigation controls. Especially the tab bar. Parameters ---------- url: string Initial part of the url on the basis of which one of the tabs can be made active. Returns ------- string The HTML of the navigation. """ Settings = self.Settings H = Settings.H pubUrl = Settings.pubUrl published = Settings.published # 1st column: url # 2nd column: interface string # 3rd column: True: enabled, False: disabled # 4th column: implemented TABS = ( ("home", "Home", True, True), ("project", "3D Projects", True, True), ("admin", "My Work", True, True), ("advancedsearch", "Advanced Search", False, False), (None, "|", True, True), (pubUrl, "Published Projects ⌲", True, True), (blogUrl, "Blog ⌲", True, True), ) search = H.span( [ H.input( "", "search", name="search", placeholder="search item", cls="button disabled", disabled="", style="display:none", ), H.input( "Search", "submit", cls="button disabled", disabled="", style="display:none", ), ], cls="search-bar", ) divContent = [] for tab, label, enabled, implemented in TABS: if not implemented: continue active = "active" if url == tab else "" if tab is None: elem = "span" href = [] target = {} fullCls = "large" else: if enabled: elem = "a" cls = active href = [tab if "/" in tab else f"/{tab}"] target = dict(target=published) if "/" in tab else {} else: elem = "span" cls = "disabled" href = [] target = {} fullCls = f"button large {cls}" divContent.append(H.elem(elem, label, *href, cls=fullCls, **target)) divContent.append(search) return H.div(divContent, cls="tabs")
Methods
def admin(self)
-
The page with the controls on the relevant projects, editions, and users.
Returns
response
def authWebdav(self, project, edition, method, path, action)
-
Authorises a webdav request.
When a viewer makes a WebDAV request to the server, that request is first checked here for authorisation.
See
dispatchWebdav()
.Parameters
project
:string | ObjectId | AttrDict
- The project in question.
edition
:string | ObjectId | AttrDict
- The edition in question.
path
:string
- The path relative to the directory of the edition.
action
:string
- The operation that the WebDAV request wants to do on the data
(
read
orupdate
).
Returns
boolean
- Whether the action is permitted on ths data by the current user.
def createEdition(self, project)
-
Inserts an edition into a project and shows the new edition.
The current user is linked to this edition as editor.
After the operation:
- success: goes to new edition url, good status
- failure: goes to project url, error status
Returns
response
Parameters
project
:string | ObjectId | AttrDict
- The project to which the edition belongs.
Returns
response
def createProject(self, site)
-
Creates a project and shows the new project.
The current user is linked to this project as organiser.
After the operation:
- success: goes to new project url, good status
- failure: goes to all projects url, error status
Returns
response
Returns
response
def createUser(self, user)
-
Creates a new test user.
After the operation:
- success: goes to admin url, good status
- failure: goes to admin url, error status
Returns
response
def delBackup(self, backup, project=None)
-
Deletes a backup.
After the operation:
- success: goes back to referrer url, good status
- failure: goes back to referrer url, error status
Parameters
backup
:string
- The name of the backup as stored in the backups directory on the server.
project
:string
, optionalNone
- If given, deletes a backup of the given project.
Returns
response
def deleteFile(self, record, key, path, targetFileName=None)
-
Delete a file.
Parameters
record
:string | ObjectId | AttrDict
- The context record of the upload.
key
:string
- The key of the upload.
path
:string
- The location of the file.
targetFileName
:string
, optionalNone
- The name of the file.
Returns
response
- With json data containing a status, msg, and content members. The content is new content to display the upload widget with.
def deleteItem(self, table, record)
-
Deletes an item, project or edition.
After the operation:
- success: goes to all-projects url or master project url, good status
- failure: goes to back referrer url, error status
Parameters
table
:string
- The kind of item:
project
oredition
. record
:string | ObjectId | AttrDict
- The item in question.
Returns
response
def deleteUser(self, user)
-
Deletes a test user.
After the operation:
- success: goes to admin url, good status
- failure: goes to admin url, error status
Returns
response
def edition(self, edition, version=None, action=None)
-
The landing page of an edition, possibly with a scene marked as active.
An edition knows the scene it should display and the viewer that was used to create the scene.
If action is not None, its value determines which viewer will be loaded in the 3D viewer. It is dependent on the parameters and/or defaults in which viewer/version/mode.
If version is not None, this will override the default version.
Parameters
edition
:string | ObjectId | AttrDict
- The editionin quesion. From the edition record we can find the project too.
version
:string
, optionalNone
- The viewer version to use.
action
:string
, optionalNone
- The mode in which the viewer is to be used (
read
orupdate
).
Returns
response
def fileData(self, path, project=None, edition=None)
-
Data content requested directly from the file repository.
This is
- the material requested by the viewers: the scene json itself and additional resources, that are part of the user contributed content that is under control of the viewer: annotations, media, etc.
- icons for the site, projects, and editions
Parameters
path
:string
- Path on the file system under the data directory where the resource resides. The path is relative to the project, and, if given, the edition.
project
:string | ObjectId | AttrDict
- The id of a project under which the resource is to be found. If None, it is site-wide material.
edition
:string | ObjectId | AttrDict
- If not None, the name of an edition under which the resource is to be found.
Returns
response
- The response consists of the contents of the file plus headers derived from the path. If the file does not exist, a 404 is returned.
def fromPub(self, projectIdGiven, editionIdGiven)
-
Redirect to a project or edition or the home page.
If the edition or project does not exist, show a friendly message.
def generate(self)
-
Regenerate the static HTML pages for the whole published site.
After the operation:
- success: goes back to referrer url, good status
- failure: goes back to referrer url, error status
Returns
response
def home(self)
-
The site-wide home page.
Returns
response
def mkBackup(self, project=None)
-
Backup: Save file and database data in a backup directory.
Parameters
project
:string
, optionalNone
- If given, only backs up the given project.
After the operation:
- success: goes back to referrer url, good status
- failure: goes back to referrer url, error status
Returns
response
-
Generates the navigation controls.
Especially the tab bar.
Parameters
url
:string
- Initial part of the url on the basis of which one of the tabs can be made active.
Returns
string
- The HTML of the navigation.
def page(self, url, left=None, right=None)
-
Workhorse function to get content on the page.
Parameters
url
:string
- Initial part of the url that triggered the page function. This part is used to make one of the tabs on the web page active.
left
:string
, optionalNone
- Content for the left column of the page.
right
:string
, optionalNone
- Content for the right column of the page.
def precheck(self, edition)
-
Check the articles of an edition prior to publishing.
Parameters
edition
:string
- the edition
After the operation:
Goes back to the referrer url. The check operation will have generated a table of contents for the articles and media files, and these will be shown on the edition page.
Returns
response
def project(self, project)
-
The landing page of a project.
Parameters
project
:string | ObjectId | AttrDict
- The project in question.
Returns
response
def projects(self)
-
The page with the list of projects.
Returns
response
def publish(self, edition, force)
-
Publish an edition as static pages.
Parameters
edition
:string
- the edition
force
:boolean
- If True, ignore when some checks fail
After the operation:
- success: goes back to referrer url, good status
- failure: goes back to referrer url, error status
Returns
response
def remaining(self, path)
-
When the url of the request is not recognized.
Parameters
path
:string
- The url (without leading /) that is not recognized.
Returns
response
- Either a redirect to the referred, for some recognized urls that correspond to not-yet implemented one. Or a redirect 303 for all other cases.
def republish(self, edition, force)
-
Re-publish an edition as static pages.
Parameters
edition
:string
- the edition
force
:boolean
- If True, ignore when some checks fail
After the operation:
- success: goes back to referrer url, good status
- failure: goes back to referrer url, error status
Returns
response
def restoreBackup(self, backup, project=None)
-
Restore from a backup. Make a new backup first.
After the operation:
- success:
- site-wide restore: goes to logout url, good status
- project-specific restore: goes to project url, good status
- failure: goes back to referrer url, error status
Parameters
backup
:string
- The name of the backup as stored in the backups directory on the server.
project
:string
, optionalNone
- If given, restores the given project.
Returns
response
- success:
def unpublish(self, edition)
-
Unpublish an edition from the static pages.
Parameters
edition
:string
- the edition
After the operation:
- success: goes back to referrer url, good status
- failure: goes back to referrer url, error status
Returns
response
def upload(self, record, key, path, targetFileName=None)
-
Upload a file.
Parameters
record
:string | ObjectId | AttrDict
- The context record of the upload
key
:string
- The key of the upload
path
:string
- The save location for the file
targetFileName
:string
, optionalNone
- The name of the file as which the uploaded file will be saved; if is None, the file will be saved with the name from the request.
Returns
response
- With json data containing a status and a content member. The content is new content to display the upload widget with.
def viewerFrame(self, edition, version, action, subMode)
-
The page loaded in an iframe where a 3D viewer operates.
Parameters
edition
:string | ObjectId | AttrDict
- The edition that is shown.
version
:string | None
- The version to use.
action
:string | None
- The mode in which the viewer is to be used (
read
orupdate
). subMode
:string | None
- The sub mode in which the viewer is to be used (
update
orcreate
).
Returns
response
def viewerResource(self, path)
-
Components requested by viewers.
This is the javascript code, the css, and other resources that are part of the 3D viewer software.
Parameters
path
:string
- Path on the file system under the viewers base directory where the resource resides.
Returns
response
- The response consists of the contents of the file plus headers derived from the path. If the file does not exist, a 404 is returned.