Commit 4f530f85 authored by Romain Courteaud's avatar Romain Courteaud

erp5_web_renderjs_ui: Replace appcache by a service worker

Chrome will deprecate appcache in the coming weeks.
As appcache has been dropped on Firefox, this change will improve the speed on Firefox.

The service worker only provide a cache for all ERP5JS gadgets.
ERP5JS will still be usable on browser without service worker API.

The list of files to cache is directly added inside the service worker
code, in order to be able to skip the usage of the fetch method (which
does not yet cover all browser HTTP usage).

This list is configurable on the web site level.
No need to change the service worker code directly.

The service worker will be updated as soon as the web site modification date changes.

End user does not need to reload its browser tab.
Only browsing ERP5JS will trigger the service worker update.

When a new update is found, ERP5JS will automatically reload itself if
there is no risk of losing user data input.

An empty appcache is kept, to allow migration of existing client to the
worker automatically.
parent a32885bf
......@@ -61,12 +61,6 @@
</tuple>
</value>
</item>
<item>
<key> <string>categories</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>content_md5</string> </key>
<value>
......@@ -104,160 +98,9 @@
<item>
<key> <string>text_content</string> </key>
<value> <string>CACHE MANIFEST\n
# generated on Thu, 07 Jan 2020 00:00:00 GMT+0100\n
# XXX + fonts\n
# images/ajax-loader.gif\n
CACHE:\n
favicon.ico\n
font-awesome/font-awesome-webfont.eot\n
font-awesome/font-awesome-webfont.woff\n
font-awesome/font-awesome-webfont.woff2\n
font-awesome/font-awesome-webfont.ttf\n
font-awesome/font-awesome-webfont.svg\n
gadget_erp5_worklist_empty.svg?format=svg\n
erp5_launcher_nojqm.js\n
gadget_erp5_nojqm.css\n
gadget_erp5_configure_editor.html\n
gadget_erp5_configure_editor.js\n
gadget_erp5_editor_panel.html\n
gadget_erp5_editor_panel.js\n
gadget_erp5_field_checkbox.html\n
gadget_erp5_field_checkbox.js\n
gadget_erp5_field_datetime.html\n
gadget_erp5_field_datetime.js\n
gadget_erp5_field_editor.html\n
gadget_erp5_field_editor.js\n
gadget_erp5_field_email.html\n
gadget_erp5_field_email.js\n
gadget_erp5_field_file.html\n
gadget_erp5_field_file.js\n
gadget_erp5_field_float.html\n
gadget_erp5_field_float.js\n
gadget_erp5_field_formbox.html\n
gadget_erp5_field_formbox.js\n
gadget_erp5_field_gadget.html\n
gadget_erp5_field_gadget.js\n
gadget_erp5_field_image.html\n
gadget_erp5_field_image.js\n
gadget_erp5_field_integer.html\n
gadget_erp5_field_integer.js\n
gadget_erp5_field_list.html\n
gadget_erp5_field_list.js\n
gadget_erp5_field_lines.html\n
gadget_erp5_field_lines.js\n
gadget_erp5_field_listbox.html\n
gadget_erp5_field_listbox.js\n
gadget_erp5_field_matrixbox.html\n
gadget_erp5_field_matrixbox.js\n
gadget_erp5_field_multicheckbox.html\n
gadget_erp5_field_multicheckbox.js\n
gadget_erp5_field_multilist.html\n
gadget_erp5_field_multilist.js\n
gadget_erp5_field_multirelationstring.html\n
gadget_erp5_field_multirelationstring.js\n
gadget_erp5_field_radio.html\n
gadget_erp5_field_radio.js\n
gadget_erp5_field_readonly.html\n
gadget_erp5_field_readonly.js\n
gadget_erp5_field_relationstring.html\n
gadget_erp5_field_relationstring.js\n
gadget_erp5_field_string.html\n
gadget_erp5_field_string.js\n
gadget_erp5_field_password.html\n
gadget_erp5_field_password.js\n
gadget_erp5_field_textarea.html\n
gadget_erp5_field_textarea.js\n
gadget_erp5_form.html\n
gadget_erp5_form.js\n
gadget_erp5_header.html\n
gadget_erp5_header.js\n
gadget_erp5_jio.html\n
gadget_erp5_jio.js\n
gadget_erp5_label_field.html\n
gadget_erp5_label_field.js\n
gadget_erp5_notification.html\n
gadget_erp5_notification.js\n
gadget_erp5_page_action.html\n
gadget_erp5_page_action.js\n
gadget_erp5_page_export.html\n
gadget_erp5_page_export.js\n
gadget_erp5_page_form.html\n
gadget_erp5_page_form.js\n
gadget_erp5_page_front.html\n
gadget_erp5_page_front.js\n
gadget_erp5_page_history.html\n
gadget_erp5_page_history.js\n
gadget_erp5_page_jump.html\n
gadget_erp5_page_jump.js\n
gadget_erp5_page_language.html\n
gadget_erp5_page_language.js\n
gadget_erp5_page_logout.html\n
gadget_erp5_page_logout.js\n
gadget_erp5_page_preference.html\n
gadget_erp5_page_preference.js\n
gadget_erp5_page_relation_search.html\n
gadget_erp5_page_relation_search.js\n
gadget_erp5_page_search.html\n
gadget_erp5_page_search.js\n
gadget_erp5_page_tab.html\n
gadget_erp5_page_tab.js\n
gadget_erp5_page_worklist.html\n
gadget_erp5_page_worklist.js\n
gadget_erp5_panel.html\n
gadget_erp5_panel.js\n
gadget_erp5_panel.png?format=png\n
gadget_erp5_pt_embedded_form_render.html\n
gadget_erp5_pt_embedded_form_render.js\n
gadget_erp5_pt_form_dialog.html\n
gadget_erp5_pt_form_dialog.js\n
gadget_erp5_pt_form_jump.html\n
gadget_erp5_pt_form_jump.js\n
gadget_erp5_pt_form_python_action.html\n
gadget_erp5_pt_form_python_action.js\n
gadget_erp5_pt_form_list.html\n
gadget_erp5_pt_form_list.js\n
gadget_erp5_pt_form_view.html\n
gadget_erp5_pt_form_view.js\n
gadget_erp5_pt_form_view_editable.html\n
gadget_erp5_pt_form_view_editable.js\n
gadget_erp5_pt_report_view.html\n
gadget_erp5_pt_report_view.js\n
gadget_erp5_router.html\n
gadget_erp5_router.js\n
gadget_erp5_relation_input.html\n
gadget_erp5_relation_input.js\n
gadget_erp5_search_editor.html\n
gadget_erp5_search_editor.js\n
gadget_erp5_searchfield.html\n
gadget_erp5_searchfield.js\n
gadget_erp5_sort_editor.html\n
gadget_erp5_sort_editor.js\n
gadget_global.js\n
gadget_html5_element.html\n
gadget_html5_element.js\n
gadget_html5_input.html\n
gadget_html5_input.js\n
gadget_html5_textarea.html\n
gadget_html5_textarea.js\n
gadget_html5_select.html\n
gadget_html5_select.js\n
gadget_erp5_global.js\n
gadget_jio.html\n
gadget_jio.js\n
gadget_translation.html\n
gadget_translation.js\n
gadget_translation_data.js\n
gadget_editor.html\n
gadget_editor.js\n
gadget_button_maximize.html\n
gadget_button_maximize.js\n
handlebars.js\n
jiodev.js\n
renderjs.js\n
rsvp.js\n
NETWORK:\n
*\n
</string> </value>
*</string> </value>
</item>
<item>
<key> <string>title</string> </key>
......@@ -298,6 +141,12 @@ NETWORK:\n
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
<item>
<key> <string>processing_status_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAU=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
......@@ -343,7 +192,7 @@ NETWORK:\n
</tuple>
<state>
<tuple>
<float>1465381395.69</float>
<float>1583142612.59</float>
<string>UTC</string>
</tuple>
</state>
......@@ -392,7 +241,7 @@ NETWORK:\n
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>981.438.11963.4386</string> </value>
<value> <string>982.16557.65503.30958</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -410,7 +259,70 @@ NETWORK:\n
</tuple>
<state>
<tuple>
<float>1578416140.21</float>
<float>1583142606.91</float>
<string>UTC</string>
</tuple>
</state>
</object>
</value>
</item>
</dictionary>
</list>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="5" aka="AAAAAAAAAAU=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_log</string> </key>
<value>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>actor</string> </key>
<value> <string>zope</string> </value>
</item>
<item>
<key> <string>comment</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>error_message</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>external_processing_state</string> </key>
<value> <string>empty</string> </value>
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>0.0.0.0</string> </value>
</item>
<item>
<key> <string>time</string> </key>
<value>
<object>
<klass>
<global name="DateTime" module="DateTime.DateTime"/>
</klass>
<tuple>
<none/>
</tuple>
<state>
<tuple>
<float>1583142315.66</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="google" content="notranslate">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="favicon.ico">
<link rel="shortcut icon" href="${base_prefix}favicon.ico">
<meta name="theme-color" content="#085078">
<title>${application_title}</title>
......@@ -16,6 +16,7 @@
${webapp_manifest_full_link_tag}
<script data-renderjs-configuration="application_title" type="text/x-renderjs-configuration">${application_title}</script>
<script data-renderjs-configuration="base_prefix" type="text/x-renderjs-configuration">${base_prefix}</script>
<script data-renderjs-configuration="panel_gadget" type="text/x-renderjs-configuration">${panel_gadget}</script>
<script data-renderjs-configuration="action_view" type="text/x-renderjs-configuration">${action_view}</script>
<script data-renderjs-configuration="default_view_reference" type="text/x-renderjs-configuration">${default_view_reference}</script>
......@@ -27,16 +28,16 @@
<script data-renderjs-configuration="website_url_set" type="text/x-renderjs-configuration">${website_url_set}</script>
<script data-renderjs-configuration="service_worker_url" type="text/x-renderjs-configuration">${service_worker_url}</script>
<script src="rsvp.js"></script>
<script src="renderjs.js"></script>
<script src="erp5_launcher_nojqm.js"></script>
<script src="${base_prefix}rsvp.js"></script>
<script src="${base_prefix}renderjs.js"></script>
<script src="${base_prefix}erp5_launcher_nojqm.js"></script>
</head>
<body>
<div data-role="page">
<div data-gadget-url="gadget_jio.html"
<div data-gadget-url="${base_prefix}gadget_jio.html"
data-gadget-scope="setting_gadget"
data-gadget-sandbox="public"></div>
......@@ -44,7 +45,7 @@
data-gadget-scope="router"
data-gadget-sandbox="public"></div>
<div data-gadget-async-url="gadget_erp5_notification.html"
<div data-gadget-async-url="${base_prefix}gadget_erp5_notification.html"
data-gadget-scope="notification"
data-gadget-sandbox="public"></div>
......@@ -61,7 +62,7 @@
data-gadget-sandbox="public"></div>
<div data-gadget-async-url="gadget_erp5_editor_panel.html"
<div data-gadget-async-url="${base_prefix}gadget_erp5_editor_panel.html"
data-gadget-scope="editor_panel"
data-gadget-sandbox="public"></div>
......
......@@ -240,7 +240,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>981.59416.23022.53862</string> </value>
<value> <string>981.33187.14689.53538</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -258,7 +258,7 @@
</tuple>
<state>
<tuple>
<float>1581954760.5</float>
<float>1580399803.54</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -10,13 +10,25 @@
var MAIN_SCOPE = "m",
default_state_json_string = JSON.stringify({
panel_visible: false,
service_worker_claimed: false,
setting_id: "setting/" + document.head.querySelector(
'script[data-renderjs-configuration="application_title"]'
).textContent,
base_prefix: document.head.querySelector(
'script[data-renderjs-configuration="base_prefix"]'
).textContent
});
function renderMainGadget(gadget, url, options) {
var page_gadget;
if (gadget.state.base_prefix) {
// For now, we assume the url is always relative
// Until one router starts to return absolute url
url = new URL(
url,
new URL(gadget.state.base_prefix, window.location.href).href
).href;
}
return gadget.declareGadget(url, {
scope: MAIN_SCOPE
})
......@@ -305,7 +317,8 @@
throw error;
})
.push(function (doc) {
for (var key in setting_dict) {
var key;
for (key in setting_dict) {
if (setting_dict.hasOwnProperty(key)) {
if (!doc.hasOwnProperty(key) ||
doc[key] !== setting_dict[key]) {
......@@ -409,13 +422,7 @@
if (setting.hasOwnProperty('service_worker_url') &&
(setting.service_worker_url !== '')) {
if (navigator.serviceWorker !== undefined) {
// Check if a ServiceWorker already controls the site on load
if (!navigator.serviceWorker.controller) {
// Register the ServiceWorker
navigator.serviceWorker.register(setting.service_worker_url);
}
}
gadget.registerServiceWorker(setting.service_worker_url);
}
return setting_gadget.put(gadget.state.setting_id, setting);
......@@ -680,6 +687,18 @@
.allowPublicAcquisition("renderApplication", function renderApplication(
param_list
) {
if (this.state.service_worker_claimed &&
(this.state.notification_options === undefined)) {
// As a new service worker claimed the client,
// reload the page to ensure it uses the lastest gadget versions
// Do it only if there is no notification to display
return location.reload();
}
if (this.state.service_worker_registration) {
// Mimic service worker update rule
// when a navigation to an in-scope page
this.deferServiceWorkerUpdate(this.state.service_worker_registration);
}
return this.render.apply(this, param_list);
})
......@@ -912,7 +931,76 @@
wallpaper_url = null;
wallpaper_absolute_url = null;
}
});
})
/////////////////////////////////////////////////////////////////
// Service worker handling
/////////////////////////////////////////////////////////////////
.declareJob('registerServiceWorker',
function registerServiceWorker(service_worker_url) {
var gadget = this,
is_first_worker,
install_delay;
if (navigator.serviceWorker !== undefined) {
is_first_worker = (navigator.serviceWorker.controller === null);
install_delay = is_first_worker ? 20000 : 2000;
return RSVP.all([
// Delay service worker installation, so that:
// * it does not slow down the current page rendering
// * it is not triggered too often if user click on multiple links
// * it is triggered only if the user browse the site
RSVP.delay(install_delay)
.then(function () {
// Register the ServiceWorker
return navigator.serviceWorker
.register(service_worker_url)
.then(function (registration) {
gadget.state.service_worker_registration = registration;
})
.catch(function (error) {
// If service worker installation is rejected,
// do not prevent the site to be usable, even if slower
console.warn("Service worker registration failed", error);
});
}),
// Check when a new worker has been activated from another tab
// XXX Not promise based, but we do not want to add a new dependency
navigator.serviceWorker
.addEventListener('message',
function handleMessage(event) {
if (event.data === 'claim') {
if (is_first_worker) {
// Do not reload if the worker is created after the page
// is loaded, because they probably use the same
// gadgets
is_first_worker = false;
} else {
gadget.state.service_worker_claimed = true;
}
}
})
]);
}
})
.declareJob('deferServiceWorkerUpdate',
function deferServiceWorkerUpdate(registration) {
return new RSVP.Queue()
.push(function () {
// Delay service worker update, so that:
// * it does not slow down the current page rendering
// * it is not triggered too often if user click on multiple links
// * it is triggered only if the user browse the site
return RSVP.delay(20000);
})
.push(function () {
return registration
.update()
.catch(function (error) {
console.warn("Service worker update failed", error);
});
});
});
}(window, document, RSVP, rJS,
XMLHttpRequest, location, console, navigator, Event, URL));
\ No newline at end of file
......@@ -234,7 +234,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>980.43952.30132.41164</string> </value>
<value> <string>981.63449.54271.9625</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -252,7 +252,7 @@
</tuple>
<state>
<tuple>
<float>1577760448.33</float>
<float>1582196761.8</float>
<string>UTC</string>
</tuple>
</state>
......
/*jslint indent: 2*/
/*global self, caches, fetch*/
(function (self, caches, fetch) {
/*global self, caches, fetch, Promise, URL, location, JSON*/
(function (self, caches, fetch, Promise, URL, location, JSON) {
"use strict";
var CACHE_NAME = 'Thu, 12 July 2016 12:00:00 GMT',
// Files required to make this app work offline
REQUIRED_FILES = [
'./',
'https://netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css',
'https://netdna.bootstrapcdn.com/font-awesome/4.2.0/fonts/fontawesome-webfont.eot?v=4.2.0',
'https://netdna.bootstrapcdn.com/font-awesome/4.2.0/fonts/fontawesome-webfont.eot?#iefix&v=4.2.0',
'https://netdna.bootstrapcdn.com/font-awesome/4.2.0/fonts/fontawesome-webfont.woff?v=4.2.0',
'https://netdna.bootstrapcdn.com/font-awesome/4.2.0/fonts/fontawesome-webfont.ttf?v=4.2.0',
'https://netdna.bootstrapcdn.com/font-awesome/4.2.0/fonts/fontawesome-webfont.svg?v=4.2.0#fontawesomeregular',
'URI.js',
'erp5_launcher.js',
'gadget_erp5.css',
'gadget_erp5_editor_panel.html',
'gadget_erp5_editor_panel.js',
'gadget_erp5_field_checkbox.html',
'gadget_erp5_field_checkbox.js',
'gadget_erp5_field_datetime.html',
'gadget_erp5_field_datetime.js',
'gadget_erp5_field_email.html',
'gadget_erp5_field_email.js',
'gadget_erp5_field_file.html',
'gadget_erp5_field_file.js',
'gadget_erp5_field_float.html',
'gadget_erp5_field_float.js',
'gadget_erp5_field_gadget.html',
'gadget_erp5_field_gadget.js',
'gadget_erp5_field_image.html',
'gadget_erp5_field_image.js',
'gadget_erp5_field_integer.html',
'gadget_erp5_field_integer.js',
'gadget_erp5_field_list.html',
'gadget_erp5_field_list.js',
'gadget_erp5_field_listbox.html',
'gadget_erp5_field_listbox.js',
'gadget_erp5_field_multicheckbox.html',
'gadget_erp5_field_multicheckbox.js',
'gadget_erp5_field_multilist.html',
'gadget_erp5_field_multilist.js',
'gadget_erp5_field_multirelationstring.html',
'gadget_erp5_field_multirelationstring.js',
'gadget_erp5_field_radio.html',
'gadget_erp5_field_radio.js',
'gadget_erp5_field_readonly.html',
'gadget_erp5_field_readonly.js',
'gadget_erp5_field_relationstring.html',
'gadget_erp5_field_relationstring.js',
'gadget_erp5_field_string.html',
'gadget_erp5_field_string.js',
'gadget_erp5_field_password.html',
'gadget_erp5_field_password.js',
'gadget_erp5_field_textarea.html',
'gadget_erp5_field_textarea.js',
'gadget_erp5_form.html',
'gadget_erp5_form.js',
'gadget_erp5_header.html',
'gadget_erp5_header.js',
'gadget_erp5_jio.html',
'gadget_erp5_jio.js',
'gadget_erp5_page_action.html',
'gadget_erp5_page_action.js',
'gadget_erp5_page_form.html',
'gadget_erp5_page_form.js',
'gadget_erp5_page_front.html',
'gadget_erp5_page_front.js',
'gadget_erp5_page_history.html',
'gadget_erp5_page_history.js',
'gadget_erp5_page_jump.html',
'gadget_erp5_page_jump.js',
'gadget_erp5_page_logout.html',
'gadget_erp5_page_logout.js',
'gadget_erp5_page_preference.html',
'gadget_erp5_page_preference.js',
'gadget_erp5_page_relation_search.html',
'gadget_erp5_page_relation_search.js',
'gadget_erp5_page_search.html',
'gadget_erp5_page_search.js',
'gadget_erp5_page_tab.html',
'gadget_erp5_page_tab.js',
'gadget_erp5_page_worklist.html',
'gadget_erp5_page_worklist.js',
'gadget_erp5_panel.html',
'gadget_erp5_panel.js',
'gadget_erp5_pt_form_dialog.html',
'gadget_erp5_pt_form_dialog.js',
'gadget_erp5_pt_form_list.html',
'gadget_erp5_pt_form_list.js',
'gadget_erp5_pt_form_view.html',
'gadget_erp5_pt_form_view.js',
'gadget_erp5_pt_form_view_editable.html',
'gadget_erp5_pt_form_view_editable.js',
'gadget_erp5_pt_report_view.html',
'gadget_erp5_pt_report_view.js',
'gadget_erp5_router.html',
'gadget_erp5_router.js',
'gadget_erp5_relation_input.html',
'gadget_erp5_relation_input.js',
'gadget_erp5_search_editor.html',
'gadget_erp5_search_editor.js',
'gadget_erp5_searchfield.html',
'gadget_erp5_searchfield.js',
'gadget_erp5_sort_editor.html',
'gadget_erp5_sort_editor.js',
'gadget_global.js',
'gadget_erp5_global.js',
'gadget_jio.html',
'gadget_jio.js',
'gadget_translation.html',
'gadget_translation.js',
'gadget_translation_data.js',
'handlebars.js',
'i18next.js',
'jiodev.js',
'renderjs.js',
'rsvp.js'
];
var prefix = location.toString() + '_',
CACHE_NAME = prefix + '${modification_date}',
REQUIRED_FILES = JSON.parse('${required_url_list}'),
required_url_list = [],
i,
len = REQUIRED_FILES.length;
for (i = 0; i < len; i += 1) {
required_url_list.push(
new URL(REQUIRED_FILES[i], location.toString()).toString()
);
}
self.addEventListener('install', function (event) {
// Perform install step: loading each required file into cache
event.waitUntil(
caches.open(CACHE_NAME)
.then(function (cache) {
// Add all offline dependencies to the cache
return cache.addAll(REQUIRED_FILES);
var promise = Promise.resolve();
function append(url_to_cache) {
promise = promise
.then(function () {
// Use cache.add because safari does not support cache.addAll.
  • @romain according to https://developer.mozilla.org/fr/docs/Web/API/Cache/addAll Safari supports cache.addAll nowadays.

    I have a selenium test logging in on a renderjs app and waiting for condition selenium.browserbot.currentWindow.navigator.serviceWorker.controller !== null

    With this version it takes a total time of 3 minutes:

    image

    I tried to change to cache.addAll and it would take less than 1 minute:

    image

    A few line below, there's this comment:

                // Add all offline dependencies to the cache
                // One by one, to not hammer zopes

    but it might be worth reconsidering using cache.addAll if the main reason was the lack of support in safari.

    I ended up here because I was curious "why is this test so slow ?" "isn't chrome supposed to make these requests concurrently ?", but not sure how much all this has effect in real usage. I did not expect that the difference would be so big, so I guess it's good to know anyway :)

    Edited by Jérome Perrin
  • Dependencies are fetched on purpose one after the other, and not in parallel (by using .then) for multiple reasons:

    • not hammering zope with 200 requests from a single user
    • do not prevent user to navigate the site while (as haproxy will queue all requests) the service worker is filled (the first access leads to fetch most ressources twice: for the service worker and for the browser tab)
    • some browsers incorrectly do not trigger error in case of http error, which lead to a corrupted cache and so, unusable site (at least, in case of .addAll)

    Please also note that there is an explicit delay before registering the service worker for the same reasons.

    So, yes, service worker installation is slow. But I think it currently leads to a better user experience (except if you tell me some users complained).

    Of course, this slow down the test, but it is less critical.

  • Thanks for clarifying, so the main reason of not using cache.addAll is to fetch sequentially with a low priority. Nobody complained, it's just that I was surprised to see that this was so slow, so I looked a bit but it's probably OK to do this way.

    I suggest we remove this comment about safari, then there will be only this comment:

                // Add all offline dependencies to the cache
                // One by one, to not hammer zopes
  • Emoji reaction is displayed on the top post of the discussion. It is a bit confusing :/

    Anyway, yes, improving comments is always welcome

Please register or sign in to reply
return cache.add(url_to_cache);
});
}
len = required_url_list.length;
for (i = 0; i < len; i += 1) {
// Add all offline dependencies to the cache
// One by one, to not hammer zopes
append(required_url_list[i]);
}
return promise;
})
.then(function () {
// At this point everything has been cached
// When user accesses ERP5JS web site first time, service worker is
// installed but it is not activated yet, service worker is activated
// when the page is refreshed or when a new tab opens the site again.
// If user does not refresh the page and continue to use the site,
// user can't use cache, so everything becomes slow. We must avoid this
// situation.
// So, we want to activate the new service worker immediately if it was
// the first one.
return self.skipWaiting();
})
.catch(function (error) {
// Since we do not allow to override existing cache, if cache installation
// failed, we need to delete the cache completely.
caches.delete(CACHE_NAME);
throw error;
})
);
});
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request)
var url = new URL(event.request.url);
url.hash = '';
if ((event.request.method !== 'GET') ||
(required_url_list.indexOf(url.toString()) === -1)) {
// Try not to use the untrustable fetch function
// It can only be skip synchronously
return;
}
return event.respondWith(
caches.open(CACHE_NAME)
.then(function (cache) {
// Don't give request object itself. Firefox's Cache Storage
// does not work properly when VARY contains Accept-Language.
// Give URL string instead, then cache.match works on both Firefox and Chrome.
return cache.match(event.request.url);
})
.then(function (response) {
// Cache hit - return the response from the cached version
if (response) {
return response;
}
// Not in cache - return the result from the live server
// `fetch` is essentially a "fallback"
return fetch(event.request);
......@@ -168,7 +105,8 @@
.filter(function (key) {
// Filter by keys that don't start with the latest version prefix.
// return !key.startsWith(version);
return key !== CACHE_NAME;
return ((key !== CACHE_NAME) &&
key.startsWith(prefix));
})
.map(function (key) {
/* Return a promise that's fulfilled
......@@ -179,11 +117,21 @@
);
})
.then(function () {
self.clients.claim();
return self.clients.claim();
})
.then(function () {
return self.clients.matchAll();
})
.then(function (client_list) {
// Notify all clients that they can reload the page
var j,
client_len = client_list.length;
for (j = 0; j < client_len; j += 1) {
client_list[j].postMessage('claim');
}
})
);
});
}(self, caches, fetch));
}(self, caches, fetch, Promise, URL, location, JSON));
\ No newline at end of file
......@@ -279,6 +279,16 @@
<value> <string>string</string> </value>
</item>
</dictionary>
<dictionary>
<item>
<key> <string>id</string> </key>
<value> <string>configuration_service_worker_url</string> </value>
</item>
<item>
<key> <string>type</string> </key>
<value> <string>string</string> </value>
</item>
</dictionary>
</tuple>
</value>
</item>
......@@ -353,12 +363,18 @@
</item>
<item>
<key> <string>configuration_manifest_url</string> </key>
<value> <string>gadget_erp5.appcache</string> </value>
<value>
<none/>
</value>
</item>
<item>
<key> <string>configuration_panel_gadget_url</string> </key>
<value> <string>gadget_erp5_panel.html</string> </value>
</item>
<item>
<key> <string>configuration_service_worker_url</string> </key>
<value> <string>gadget_erp5_serviceworker.js</string> </value>
</item>
<item>
<key> <string>configuration_translation_gadget_url</string> </key>
<value> <string>gadget_translation.html</string> </value>
......@@ -600,7 +616,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>965.19244.13768.6758</string> </value>
<value> <string>973.33482.4166.8669</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -618,7 +634,7 @@
</tuple>
<state>
<tuple>
<float>1517843997.55</float>
<float>1553593373.77</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -10,7 +10,7 @@ attribute_filter_re = re.compile(r"""(data-i18n)=["']?((?:.(?!["']?\s+(?:\S+)=|[
translate_word = []
for web_page in portal.web_page_module.searchFolder(portal_type='Web Page',
reference=context.Base_getListFileFromAppcache(only_html=1)):
reference=context.Base_getTranslationSourceFileList(only_html=1)):
data = attribute_filter_re.findall(web_page.getTextContent())
for attribute in data:
a = re.sub(r'[{|}]', "", attribute[1])
......@@ -45,6 +45,9 @@ content += ";\n}(window));"
translation_data_file=context.web_page_module.searchFolder(portal_type='Web Script',reference=translation_data_file)[0]
translation_data_file.edit(text_content = content)
# Edit web section modification date
context.edit()
if batch_mode:
return 'done'
return context.Base_redirect('view', keep_items=dict(portal_status_message=Base_translateString("Translation Data Create")))
filename_list = context.WebSection_getPrecacheManifest()
file_list = []
translation_data_file = []
for filename in filename_list:
if filename.endswith('.html'):
file_list.append(filename)
continue
if filename.endswith('.js') and not only_html:
if filename.endswith('translation_data.js'):
translation_data_file = [filename]
continue
file_list.append(filename)
return translation_data_file + file_list
<?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>only_html=0</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Base_getTranslationSourceFileList</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
web_section = context
# Add all ERP5JS gadget
url_list = [
'favicon.ico',
'font-awesome/font-awesome-webfont.eot',
'font-awesome/font-awesome-webfont.woff',
'font-awesome/font-awesome-webfont.woff2',
'font-awesome/font-awesome-webfont.ttf',
'font-awesome/font-awesome-webfont.svg',
'gadget_erp5_worklist_empty.svg?format=svg',
'erp5_launcher_nojqm.js',
'gadget_erp5_nojqm.css',
'gadget_erp5_configure_editor.html',
'gadget_erp5_configure_editor.js',
'gadget_erp5_editor_panel.html',
'gadget_erp5_editor_panel.js',
'gadget_erp5_field_checkbox.html',
'gadget_erp5_field_checkbox.js',
'gadget_erp5_field_datetime.html',
'gadget_erp5_field_datetime.js',
'gadget_erp5_field_editor.html',
'gadget_erp5_field_editor.js',
'gadget_erp5_field_email.html',
'gadget_erp5_field_email.js',
'gadget_erp5_field_file.html',
'gadget_erp5_field_file.js',
'gadget_erp5_field_float.html',
'gadget_erp5_field_float.js',
'gadget_erp5_field_formbox.html',
'gadget_erp5_field_formbox.js',
'gadget_erp5_field_gadget.html',
'gadget_erp5_field_gadget.js',
'gadget_erp5_field_image.html',
'gadget_erp5_field_image.js',
'gadget_erp5_field_integer.html',
'gadget_erp5_field_integer.js',
'gadget_erp5_field_list.html',
'gadget_erp5_field_list.js',
'gadget_erp5_field_lines.html',
'gadget_erp5_field_lines.js',
'gadget_erp5_field_listbox.html',
'gadget_erp5_field_listbox.js',
'gadget_erp5_field_matrixbox.html',
'gadget_erp5_field_matrixbox.js',
'gadget_erp5_field_multicheckbox.html',
'gadget_erp5_field_multicheckbox.js',
'gadget_erp5_field_multilist.html',
'gadget_erp5_field_multilist.js',
'gadget_erp5_field_multirelationstring.html',
'gadget_erp5_field_multirelationstring.js',
'gadget_erp5_field_radio.html',
'gadget_erp5_field_radio.js',
'gadget_erp5_field_readonly.html',
'gadget_erp5_field_readonly.js',
'gadget_erp5_field_relationstring.html',
'gadget_erp5_field_relationstring.js',
'gadget_erp5_field_string.html',
'gadget_erp5_field_string.js',
'gadget_erp5_field_password.html',
'gadget_erp5_field_password.js',
'gadget_erp5_field_textarea.html',
'gadget_erp5_field_textarea.js',
'gadget_erp5_form.html',
'gadget_erp5_form.js',
'gadget_erp5_header.html',
# 'gadget_erp5_header.js',
'gadget_erp5_jio.html',
'gadget_erp5_jio.js',
'gadget_erp5_label_field.html',
'gadget_erp5_label_field.js',
'gadget_erp5_notification.html',
'gadget_erp5_notification.js',
'gadget_erp5_page_action.html',
'gadget_erp5_page_action.js',
'gadget_erp5_page_export.html',
'gadget_erp5_page_export.js',
'gadget_erp5_page_form.html',
'gadget_erp5_page_form.js',
'gadget_erp5_page_front.html',
'gadget_erp5_page_front.js',
'gadget_erp5_page_history.html',
'gadget_erp5_page_history.js',
'gadget_erp5_page_jump.html',
'gadget_erp5_page_jump.js',
'gadget_erp5_page_language.html',
'gadget_erp5_page_language.js',
'gadget_erp5_page_logout.html',
'gadget_erp5_page_logout.js',
'gadget_erp5_page_preference.html',
'gadget_erp5_page_preference.js',
'gadget_erp5_page_relation_search.html',
'gadget_erp5_page_relation_search.js',
'gadget_erp5_page_search.html',
'gadget_erp5_page_search.js',
'gadget_erp5_page_tab.html',
'gadget_erp5_page_tab.js',
'gadget_erp5_page_worklist.html',
'gadget_erp5_page_worklist.js',
'gadget_erp5_panel.html',
'gadget_erp5_panel.js',
'gadget_erp5_panel.png?format=png',
'gadget_erp5_pt_embedded_form_render.html',
'gadget_erp5_pt_embedded_form_render.js',
'gadget_erp5_pt_form_dialog.html',
'gadget_erp5_pt_form_dialog.js',
'gadget_erp5_pt_form_jump.html',
'gadget_erp5_pt_form_jump.js',
'gadget_erp5_pt_form_list.html',
'gadget_erp5_pt_form_list.js',
'gadget_erp5_pt_form_python_action.html',
'gadget_erp5_pt_form_python_action.js',
'gadget_erp5_pt_form_view.html',
'gadget_erp5_pt_form_view.js',
'gadget_erp5_pt_form_view_editable.html',
'gadget_erp5_pt_form_view_editable.js',
'gadget_erp5_pt_report_view.html',
'gadget_erp5_pt_report_view.js',
'gadget_erp5_router.html',
'gadget_erp5_router.js',
'gadget_erp5_relation_input.html',
'gadget_erp5_relation_input.js',
'gadget_erp5_search_editor.html',
'gadget_erp5_search_editor.js',
'gadget_erp5_searchfield.html',
'gadget_erp5_searchfield.js',
'gadget_erp5_sort_editor.html',
'gadget_erp5_sort_editor.js',
'gadget_global.js',
'gadget_html5_element.html',
'gadget_html5_element.js',
'gadget_html5_input.html',
'gadget_html5_input.js',
'gadget_html5_textarea.html',
'gadget_html5_textarea.js',
'gadget_html5_select.html',
'gadget_html5_select.js',
'gadget_erp5_global.js',
'gadget_jio.html',
'gadget_jio.js',
'gadget_translation.html',
'gadget_translation.js',
'gadget_translation_data.js',
'gadget_editor.html',
'gadget_editor.js',
'gadget_button_maximize.html',
'gadget_button_maximize.js',
'domsugar.js',
'jiodev.js',
'renderjs.js',
'rsvp.js',
]
# Add all root gadgets
default_url = './'
available_language_set = web_section.getLayoutProperty("available_language_set", default=['en'])
default_language = web_section.getLayoutProperty("default_available_language", default='en')
for language in available_language_set:
if language == default_language:
url_list.append(default_url)
else:
url_list.append('%s/' % language)
# Add all custom gadgets
url_list.extend([
web_section.getLayoutProperty("configuration_wallpaper_url", default=default_url),
web_section.getLayoutProperty("configuration_panel_gadget_url", default=default_url),
web_section.getLayoutProperty("configuration_router_gadget_url", default=default_url),
web_section.getLayoutProperty("configuration_header_gadget_url", default=default_url),
web_section.getLayoutProperty("configuration_jio_gadget_url", default=default_url),
web_section.getLayoutProperty("configuration_translation_gadget_url", default=default_url),
web_section.getLayoutProperty("configuration_stylesheet_url", default=default_url),
])
# Add all extra dependencies
precache_manifest_url_list = web_section.getLayoutProperty("configuration_precache_manifest_script_list", default='').splitlines()
for precache_manifest_script_id in precache_manifest_url_list:
url_list.extend(web_section.restrictedTraverse(precache_manifest_script_id)())
if REQUEST is not None:
import json
REQUEST.RESPONSE.setHeader('Content-Type', 'application/json')
return json.dumps(dict.fromkeys(url_list), indent=2)
return list(set(url_list))
<?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>_Cacheable__manager_id</string> </key>
<value> <string>must_revalidate_http_cache</string> </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>REQUEST=None</string> </value>
</item>
<item>
<key> <string>_proxy_roles</string> </key>
<value>
<tuple>
<string>Anonymous</string>
</tuple>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>WebSection_getPrecacheManifest</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -98,13 +98,15 @@
<value>
<list>
<string>my_configuration_panel_gadget_url</string>
<string>my_configuration_manifest_url</string>
<string>my_configuration_webapp_manifest_url</string>
<string>my_configuration_service_worker_url</string>
<string>my_configuration_jio_gadget_url</string>
<string>my_configuration_translation_gadget_url</string>
<string>my_configuration_router_gadget_url</string>
<string>my_configuration_header_gadget_url</string>
<string>my_configuration_manifest_url</string>
<string>my_configuration_webapp_manifest_url</string>
<string>my_configuration_service_worker_url</string>
<string>my_modification_date</string>
<string>my_configuration_precache_manifest_script_list</string>
</list>
</value>
</item>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list>
<string>title</string>
<string>width</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_configuration_precache_manifest_script_list</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_text_area_field</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Service Worker Precache Manifest Script List</string> </value>
</item>
<item>
<key> <string>width</string> </key>
<value> <int>40</int> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list>
<string>date_only</string>
<string>editable</string>
<string>title</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_modification_date</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>date_only</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>editable</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_date_time_field</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Service Worker Cache Key</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -54,6 +54,10 @@
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>first_item</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
......@@ -64,6 +68,10 @@
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
......@@ -107,7 +115,7 @@
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>python: [ (x,x) for x in here.Base_getListFileFromAppcache()]</string> </value>
<value> <string>python: [ (x,x) for x in here.Base_getTranslationSourceFileList()]</string> </value>
</item>
</dictionary>
</pickle>
......
......@@ -29,6 +29,12 @@ web_content = web_page.getTextContent()
# set headers depending on type of script
if (portal_type == "Web Script"):
response.setHeader('Content-Type', 'application/javascript; charset=utf-8')
if web_page.getTextContentSubstitutionMappingMethodId():
web_content = web_page.TextDocument_substituteTextContent(web_content, mapping_dict={
'modification_date': modification_date_string,
# Make JSLint happy for the service worker code
'required_url_list': json.dumps(web_section.WebSection_getPrecacheManifest())
})
elif (portal_type == "Web Style"):
response.setHeader('Content-Type', 'text/css; charset=utf-8')
......
import json
import re
from urlparse import urljoin
if REQUEST is None:
REQUEST = context.REQUEST
......@@ -31,23 +32,32 @@ view_as_web_method = default_web_page.getTypeBasedMethod(
fallback_script_id="WebPage_viewAsWeb"
)
selected_language = portal.Localizer.get_selected_language()
# base tag
if selected_language == default_language:
base_prefix = ""
else:
base_prefix = '../'
mapping_dict = {
"base_prefix": base_prefix,
"frontpage_gadget": web_section.getLayoutProperty("configuration_frontpage_gadget_url", default="worklist"),
"jio_document_page_gadget": web_section.getLayoutProperty("configuration_default_jio_document_page_gadget_url", default="form"),
"application_title": web_section.getLayoutProperty("configuration_application_title", default="ERP5"),
"action_view": web_section.getLayoutProperty("configuration_view_action_category", default="object_view"),
"default_view_reference": web_section.getLayoutProperty("configuration_default_view_action_reference", default="view"),
"hateoas_url": web_section.getLayoutProperty("configuration_hateoas_url", default="hateoas/"),
"panel_gadget": web_section.getLayoutProperty("configuration_panel_gadget_url", default="gadget_erp5_panel.html"),
"router_gadget": web_section.getLayoutProperty("configuration_router_gadget_url", default="gadget_erp5_router.html"),
"header_gadget": web_section.getLayoutProperty("configuration_header_gadget_url", default="gadget_erp5_header.html"),
"jio_gadget": web_section.getLayoutProperty("configuration_jio_gadget_url", default="gadget_jio.html"),
"translation_gadget": web_section.getLayoutProperty("configuration_translation_gadget_url", default="gadget_translation.html"),
"panel_gadget": urljoin(base_prefix, web_section.getLayoutProperty("configuration_panel_gadget_url", default="gadget_erp5_panel.html")),
"router_gadget": urljoin(base_prefix, web_section.getLayoutProperty("configuration_router_gadget_url", default="gadget_erp5_router.html")),
"header_gadget": urljoin(base_prefix, web_section.getLayoutProperty("configuration_header_gadget_url", default="gadget_erp5_header.html")),
"jio_gadget": urljoin(base_prefix, web_section.getLayoutProperty("configuration_jio_gadget_url", default="gadget_jio.html")),
"translation_gadget": urljoin(base_prefix, web_section.getLayoutProperty("configuration_translation_gadget_url", default="gadget_translation.html")),
"manifest_url": web_section.getLayoutProperty("configuration_manifest_url", default="gadget_erp5.appcache"),
"stylesheet_url": web_section.getLayoutProperty("configuration_stylesheet_url", default="gadget_erp5_nojqm.css"),
"service_worker_url": web_section.getLayoutProperty("configuration_service_worker_url", default=""),
"stylesheet_url": urljoin(base_prefix, web_section.getLayoutProperty("configuration_stylesheet_url", default="gadget_erp5_nojqm.css")),
"service_worker_url": urljoin(base_prefix, web_section.getLayoutProperty("configuration_service_worker_url", default="")),
"language_map": json.dumps({tmp['id']: portal.Base_translateString(tmp['title'], lang = tmp['id']) for tmp in portal.Localizer.get_languages_map() if tmp['id'] in available_language_set}),
"default_selected_language": portal.Localizer.get_selected_language(),
"default_selected_language": selected_language,
"website_url_set": json.dumps(website_url_set),
"site_description": web_section.getLayoutProperty("description", default=""),
"site_keywords": web_section.getLayoutProperty("subject", default=""),
......@@ -89,6 +99,6 @@ else:
background-attachment: fixed;
background-image: url("%s");
}
""" % wallpaper_url)
""" % urljoin(base_prefix, wallpaper_url))
return view_as_web_method(mapping_dict=mapping_dict)
......@@ -12,8 +12,8 @@
<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/init" />
<tal:block tal:define="check_configuration python: {'app_cache_reference': 'gadget_erp5.appcache',
'gadget_count': 65}">
<tal:block tal:define="check_configuration python: {'precache_reference': '../renderjs_runner/WebSection_getPrecacheManifest',
'gadget_count': 66}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUiInterface/macros/run_app_interface_check" />
</tal:block>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Zuite" module="Products.Zelenium.zuite"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_objects</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>renderjs_ui_service_worker_zuite</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ZopePageTemplate" module="Products.PageTemplates.ZopePageTemplate"/>
</pickle>
<pickle>
<dictionary>
<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_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/html</string> </value>
</item>
<item>
<key> <string>expand</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>testServiceWorkerInstallation</string> </value>
</item>
<item>
<key> <string>output_encoding</string> </key>
<value> <string>utf-8</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <unicode></unicode> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<html xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Test RenderJS UI</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr><td rowspan="1" colspan="3">Test RenderJS UI</td></tr>
</thead><tbody>
<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/init" />
<tr>
<td colspan="3"><b tal:content="python: 'Clone web site'"></b></td>
</tr>
<tr>
<td>open</td>
<td tal:content="python: '${base_url}/web_site_module/renderjs_runner?ignore_layout:int=1&editable_mode:int=1'"></td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>name=Base_createCloneDocument:method</td>
<td></td>
</tr>
<tr>
<td>clickAndWait</td>
<td>name=Base_createCloneDocument:method</td>
<td></td>
</tr>
<tr>
<td>verifyTextPresent</td>
<td>Created Clone Web Site</td>
<td></td>
</tr>
<tr>
<td>storeValue</td>
<td>name=field_my_id</td>
<td>install_url</td>
</tr>
<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/wait_for_activities" />
<tr>
<td>open</td>
<td>${base_url}/web_site_module/${install_url}/#/?page=test_service_worker</td>
<td></td>
</tr>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_app_loaded" />
<tr>
<td>assertTextPresent</td>
<td>Has SW: false</td>
<td></td>
</tr>
<tr>
<td>waitForTextPresent</td>
<td>Has SW: true</td>
<td></td>
</tr>
<tr>
<td>assertTextPresent</td>
<td>Has SW: true</td>
<td></td>
</tr>
<tal:block tal:define="click_configuration python: {'text': 'Views'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/click_on_header_link" />
</tal:block>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_content_loaded" />
<tr>
<td>waitForTextPresent</td>
<td>Has SW: true</td>
<td></td>
</tr>
</tbody></table>
</body>
</html>
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ZopePageTemplate" module="Products.PageTemplates.ZopePageTemplate"/>
</pickle>
<pickle>
<dictionary>
<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_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/html</string> </value>
</item>
<item>
<key> <string>expand</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>testServiceWorkerUpdate</string> </value>
</item>
<item>
<key> <string>output_encoding</string> </key>
<value> <string>utf-8</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <unicode></unicode> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<html xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Test RenderJS UI</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr><td rowspan="1" colspan="3">Test RenderJS UI</td></tr>
</thead><tbody>
<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/init" />
<tr>
<td>open</td>
<td>${base_url}/web_site_module/renderjs_runner/#/?page=test_service_worker</td>
<td></td>
</tr>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_app_loaded" />
<tr>
<td>waitForTextPresent</td>
<td>Has SW: true</td>
<td></td>
</tr>
<tr>
<td>assertTextPresent</td>
<td>Has SW: true</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>//button[text()='Proceed']</td>
<td></td>
</tr>
<tr>
<td>waitForTextPresent</td>
<td>SW changed on server</td>
<td></td>
</tr>
<tr>
<td>assertTextPresent</td>
<td>SW changed on server</td>
<td></td>
</tr>
<!-- Navigating ERP5JS should trigger service worker update -->
<tal:block tal:define="click_configuration python: {'text': 'Views'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/click_on_header_link" />
</tal:block>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_content_loaded" />
<tr>
<td>waitForTextPresent</td>
<td>Has SW: true</td>
<td></td>
</tr>
<tr>
<td>assertTextPresent</td>
<td>Has SW: true</td>
<td></td>
</tr>
<!-- Update is not triggered immediately -->
<tr>
<td>pause</td>
<td>30000</td>
<td></td>
</tr>
<tr>
<td>waitForTextPresent</td>
<td>New SW ready</td>
<td></td>
</tr>
<tr>
<td>assertTextPresent</td>
<td>New SW ready</td>
<td></td>
</tr>
<!-- ERP5JS should reload itself while user is browsing it... -->
<tal:block tal:define="click_configuration python: {'text': 'Views'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/click_on_header_link" />
</tal:block>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_app_loaded" />
<tr>
<td>waitForTextPresent</td>
<td>Has SW: true</td>
<td></td>
</tr>
<!-- ...But only once -->
<tal:block tal:define="click_configuration python: {'text': 'Views'}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/click_on_header_link" />
</tal:block>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_content_loaded" />
<tr>
<td>waitForTextPresent</td>
<td>Has SW: true</td>
<td></td>
</tr>
</tbody></table>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width" />
<title>ERP5 Test Service Worker</title>
<link rel="http://www.renderjs.org/rel/interface" href="interface_page.html">
<!-- renderjs -->
<script src="rsvp.js" type="text/javascript"></script>
<script src="renderjs.js" type="text/javascript"></script>
<script src="domsugar.js" type="text/javascript"></script>
<script src="jiodev.js" type="text/javascript"></script>
<script src="gadget_global.js" type="text/javascript"></script>
<script src="gadget_erp5_page_test_service_worker.js" type="text/javascript"></script>
</head>
<body>
</body>
</html>
\ No newline at end of file
/*global window, rJS, RSVP, domsugar, navigator, promiseEventListener, jIO */
/*jslint nomen: true, indent: 2, maxerr: 3, maxlen: 80 */
(function (window, rJS, RSVP, domsugar, navigator, promiseEventListener, jIO) {
"use strict";
rJS(window)
.declareAcquiredMethod("getUrlFor", "getUrlFor")
.declareAcquiredMethod("getUrlForList", "getUrlForList")
.declareAcquiredMethod("updateHeader", "updateHeader")
.declareMethod('render', function () {
var gadget = this;
// Wait a bit a allow the header loader to be displayed
return new RSVP.Queue(RSVP.delay(200))
.push(function () {
return gadget.getUrlForList([
// Home page
{command: 'display'},
// Soft reload the gadget
{command: 'change'}
]);
})
.push(function (url_list) {
gadget.checkServiceWorkerStatus();
return gadget.updateHeader({
page_title: 'Test Service Worker',
front_url: url_list[0],
tab_url: url_list[1],
submit_action: true
});
});
})
.declareMethod("triggerSubmit", function () {
var gadget = this;
return new RSVP.Queue(jIO.util.ajax({
type: 'POST',
url: './Base_changeModificationDateForTest'
}))
.push(function () {
domsugar(gadget.element, {
text: "SW changed on server"
});
});
})
.declareJob("checkServiceWorkerStatus", function () {
var gadget = this,
has_service_worker = (navigator.serviceWorker.controller !== null);
domsugar(gadget.element, {
text: "Has SW: " + has_service_worker.toString()
});
return new RSVP.Queue(navigator.serviceWorker.ready)
.push(function (worker_container) {
domsugar(gadget.element, {
text: "Has SW: true"
});
return promiseEventListener(worker_container.active, 'statechange');
})
.push(function () {
// Wait a bit until the message is propagated
return RSVP.delay(2000);
})
.push(function () {
domsugar(gadget.element, {
text: "New SW ready"
});
});
});
}(window, rJS, RSVP, domsugar, navigator, promiseEventListener, jIO));
\ No newline at end of file
<?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_changeModificationDateForTest</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -12,7 +12,7 @@
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/wait_for_listbox_loaded" />
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/triggle_filter_and" />
<tal:block tal:define="filter_section_configuration python: {'key': 'COLUMN_appcache_url', 'value': check_configuration['app_cache_reference'], 'index': 0}">
<tal:block tal:define="filter_section_configuration python: {'key': 'COLUMN_precache_url', 'value': check_configuration['precache_reference'], 'index': 0}">
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/set_filter_section" />
</tal:block>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/submit_filter" />
......
##############################################################################
#
# Copyright (c) 2011 Nexedi SARL and Contributors. All Rights Reserved.
# Kazuhiko <kazuhiko@nexedi.com>
# Rafael Monnerat <rafael@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
import unittest
from Products.ERP5Type.tests.ERP5TypeFunctionalTestCase import ERP5TypeFunctionalTestCase
class TestRenderJSUIServiceWorker(ERP5TypeFunctionalTestCase):
foreground = 0
run_only = "renderjs_ui_service_worker_zuite"
def getBusinessTemplateList(self):
return (
'erp5_web_renderjs_ui',
'erp5_web_renderjs_ui_test',
'erp5_ui_test_core',
'erp5_accounting',
'erp5_test_result',
)
def test_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestRenderJSUIServiceWorker))
return suite
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Test Component" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_recorded_property_dict</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>testFunctionalRJSServiceWorker</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>test.erp5.testFunctionalRJSServiceWorker</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Test Component</string> </value>
</item>
<item>
<key> <string>sid</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>text_content_error_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>text_content_warning_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>version</string> </key>
<value> <string>erp5</string> </value>
</item>
<item>
<key> <string>workflow_history</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary/>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary>
<item>
<key> <string>component_validation_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_log</string> </key>
<value>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>validate</string> </value>
</item>
<item>
<key> <string>validation_state</string> </key>
<value> <string>validated</string> </value>
</item>
</dictionary>
</list>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -44,8 +44,11 @@ portal_tests/renderjs_ui_relation_field_zuite
portal_tests/renderjs_ui_relation_field_zuite/**
portal_tests/renderjs_ui_router_zuite
portal_tests/renderjs_ui_router_zuite/**
portal_tests/renderjs_ui_service_worker_zuite
portal_tests/renderjs_ui_service_worker_zuite/**
portal_tests/renderjs_ui_translate_zuite
portal_tests/renderjs_ui_translate_zuite/**
portal_tests/renderjs_ui_zuite
portal_tests/renderjs_ui_zuite/**
web_page_module/gadget_erp5_page_router_test_*
\ No newline at end of file
web_page_module/gadget_erp5_page_router_test_*
web_page_module/gadget_erp5_page_service_worker_test_*
\ No newline at end of file
......@@ -24,4 +24,5 @@ test.erp5.testFunctionalRJSRecoverPassword
test.erp5.testFunctionalRJSInterfaceValidator
test.erp5.testFunctionalRJSDms
test.erp5.testRJSPortalType
test.erp5.testRJSUpgrader
\ No newline at end of file
test.erp5.testRJSUpgrader
test.erp5.testFunctionalRJSServiceWorker
\ No newline at end of file
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