Commit 8a980df8 authored by Sebastian's avatar Sebastian Committed by Ivan Tyagov

erp5_data_notebook: Add script for renderjs-extension

This commit adds server-side functionality for the [jupyter-renderjs-extension](https://lab.nexedi.com/Kreisel/jupyter_renderjs_extension). This has no interaction/impact with the existing functionality of the erp5_data_notebook BT.

@klaus mentioned that it is not common practice to return objects of a class (defined in the script). However, for usuability reasons there is an argument to do it here:
Usually when using a jupyter extension, a module is imported an functionality is provided by that module. For instance:
```python
In [1]:
import mymodule as mm
mm.someFunction()
mm.someOtherFunction()
```

To emulate this behavior **and** to be consistent with the [ipython-version (Python 2 Kernel)](https://lab.nexedi.com/Kreisel/jupyter_renderjs_extension/blob/master/renderjs_ipyextension/renderjs_ipyextension/renderjs_extension.py) of this extension I therefore use an object of the class `RJSExtension` which is returned by this script. I am then able to use
```python
rjs = Base_loadRenderJSExtension()
rjs.someFunction()
rjs.someOtherFunction()
```

instead of something like

```python
Base_rjsExtensionSomeFunction()
Base_rjsExtensionSomeOtherFunction()
```

which is much less user-friendly. If there is a better alternative I am not aware of, please comment. Also if anything else is amiss.

/reviewed-on !238
parent 8ec2d0cd
import json
# This script provides the backend-functionality of the Juypter Notebook RenderJS Extension.
# Due to the internal protocol of the ERP5Kernel and the ERP5-Jupyter-Backend messages between
# Javascript in the client and the ERP5Kernel are exchange via objections containing
# Javascript code as _repr_html_.
# Here a schematic overview of the messaging:
# 1. Extension-Function is called in the notebook (e.g. loadGadget("gadget", "https://someurl.com/gadget"))
# 2. Code is processed by the ERP5Kernel (client)
# 3. Code is sent to the ERP5-Backend
# 4. The required logic is handled by this script in the backend
# 5. This script returns an object with _repr_html_ containing the JS-response for the client
# 6. The ERP5-Backend sends a response to the ERP5Kernel (client)
# 7. The ERP5Kernel interprets the object with _repr_html_ as text/html message and injects it into the notebook
# 8. Now the Javascript code is executed as part of the (client) extension (e.g. a renderJS-gadget is loaded into the page)
class RJSExtension:
def __init__(self):
pass
# Create the original load_gadget with modified rsvp, renderjs
# Because jupyter notebook has already loaded when this can be called
# a manual initialization of the whole renderJS setup is required
#
# First the libs rsvp, renderjs-gadget-global and renderjs (patched)
# are injected into the page. The patch on renderjs itself is to enable
# the following manual bootstrap
# After the scripts are present, a div is appended containing the
# loading_gadget.
# After everything is inplace, rJS.manualBootstrap initializes the
# loading_gadget in exactly the same way as when rJS is normally initialized
# (on-load)
def initRenderJS(self):
script = '''
<script>
var loadingDiv = document.querySelector(".loading_gadget");
if(loadingDiv == null) {
console.log("~~ Initializing RenderJS!");
$.getScript("/nbextensions/renderjs_nbextension/rsvp-2.0.4.js", function() {
console.log("~~ loading_gadget: rsvp.js loaded");
$.getScript("/nbextensions/renderjs_nbextension/rjs_gadget_global_js.js", function() {
console.log("~~ loading_gadget: renderjs-gadget-global.js loaded");
$.getScript("/nbextensions/renderjs_nbextension/renderjs-latest.js", function() {
console.log("~~ loading_gadget: renderjs.js loaded");
$("#notebook-container").append('<div data-gadget-url="/nbextensions/renderjs_nbextension/loading_gadget.html" data-gadget-scope="public"></div>');
rJS.manualBootstrap();
});
});
});
} else {
console.log("~~ Renderjs seems to be initialized already!");
}
</script>'''
return RJSHtmlMessage(script)
# Load a gadget given a unique ref and URL to the HTML file of the gadget
# -> Fires an event which loading_gadget listens on and passes on the URL
def loadGadget(self, ref, gadgetUrl):
script = '''
<script>
var load_event = new CustomEvent("load_gadget",
{ "detail": { "url": "''' + gadgetUrl + '", "gadgetId": "' + ref + '''" }});
var loadingDiv = document.querySelector(".loading_gadget");
if(loadingDiv != null) {
loadingDiv.dispatchEvent(load_event);
} else {
console.log("~~ load: RenderJS init required first!");
}
</script>
'''
return RJSHtmlMessage(script)
# Fires an event with
# * the ref of the gadget
# * the name of the declared_method
# * the arguments to be passed to the declared_method
# The arguments are packed into a json string and passed to js as such
def callDeclaredMethod(self, ref, method_name, *args):
j_str = json.dumps(args)
script = '''
<script>
var call_event = new CustomEvent("call_gadget",
{ "detail": {
"gadgetId": "''' + ref + '''",
"methodName": "''' + method_name + '''",
"methodArgs": ''' + "'" + j_str + "'" + '''
}});
var loadingDiv = document.querySelector(".loading_gadget");
if(loadingDiv != null) {
loadingDiv.dispatchEvent(call_event);
} else {
console.log("~~ call: RenderJS init required first!");
}
</script>
'''
return RJSHtmlMessage(script)
# Fires an event to the destroy this gadget
# Only thing passed is the ref of the gadget
def destroyGadget(self, ref):
script = '''
<script>
var destroy_event = new CustomEvent("destroy_gadget",
{ "detail": { "gadgetId": "''' + ref + '''" }});
var loadingDiv = document.querySelector(".loading_gadget");
if(loadingDiv != null) {
loadingDiv.dispatchEvent(destroy_event);
} else {
console.log("~~ destroy: RenderJS init required first!");
}
</script>
'''
return RJSHtmlMessage(script)
class RJSHtmlMessage:
'''
Represents a HTML-injection into the frontend. Returning such an object from the ERP5
backend is sufficient, as the _repr_html_ will be called internally.
'''
def __init__(self, html):
self.html = html
def _repr_html_(self):
return self.html
obj = RJSExtension()
return obj
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Base_loadRenderJSExtension</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment