Merge PR #1628 into 12.0

Signed-off-by pedrobaeza
This commit is contained in:
OCA-git-bot
2020-12-04 16:50:29 +00:00
25 changed files with 1078 additions and 261 deletions

View File

@@ -31,6 +31,17 @@ Progressive Web Apps provide an installable, app-like experience on desktop and
They're web apps that are fast and reliable. And most importantly, they're web apps that work in any browser.
If you're building a web app today, you're already on the path towards building a Progressive Web App.
+ Developers Info.
The service worker is contructed using 'Odoo Class' to have the same class inheritance behaviour that in the 'user pages'. Be noticed
that 'Odoo Bootstrap' is not supported so, you can't use 'require' here.
All service worker content can be found in 'static/src/js/worker'. The management between 'user pages' and service worker is done in
'pwa_manager.js'.
The purpose of this module is give a base to make PWA applications.
**Table of contents**
.. contents::
@@ -63,25 +74,66 @@ In case you previously installed `web_pwa`, run the following steps with `odoo s
Configuration
=============
The following system parameters con be set to customize the appearance of the application
This module allows you to set the following parameters under settings to customize the appearance of the application
* pwa.manifest.name (defaults to "Odoo PWA")
* pwa.manifest.short_name (defaults to "Odoo PWA")
* pwa.manifest.icon128x128 (defaults to "/web_pwa_oca/static/img/icons/icon-128x128.png")
* pwa.manifest.icon144x144 (defaults to "/web_pwa_oca/static/img/icons/icon-144x144.png")
* pwa.manifest.icon152x152 (defaults to "/web_pwa_oca/static/img/icons/icon-152x152.png")
* pwa.manifest.icon192x192 (defaults to "/web_pwa_oca/static/img/icons/icon-192x192.png")
* pwa.manifest.icon256x256 (defaults to "/web_pwa_oca/static/img/icons/icon-256x256.png")
* pwa.manifest.icon512x512 (defaults to "/web_pwa_oca/static/img/icons/icon-512x512.png")
* PWA Name (defaults to "Odoo PWA")
* PWA Short Name (defaults to "Odoo PWA")
* PWA Icon (**SVG**) (defaults to "/web_pwa_oca/static/img/icons/odoo-logo.svg")
To configure your PWA:
#. Go to **Settings > General Settings > Progressive Web App**.
#. Set the parameters (*Note:* Icon **must be a SVG file**)
#. **Save**
Usage
=====
To use your PWA:
#. Open the Odoo web app using a supported browser (like Chrome/Chromium)
#. Install the PWA
Known issues / Roadmap
======================
* Evaluate to extend ``FILES_TO_CACHE``
* Evaluate to use a normal JS file for service worker and download data from a normal JSON controller
* Integrate `Notification API <https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification>`_
* Integrate `Web Share API <https://web.dev/web-share/>`_
* Create ``portal_pwa`` module, intended to be used by front-end users (customers, suppliers...)
* Current *John Resig's inheritance* implementation doesn't support ``async``
functions because ``this._super`` can't be called inside a promise. So we
need to use the following workaround:
- Natural 'async/await' example (This breaks "_super" call):
.. code-block:: javascript
var MyClass = OdooClass.extend({
myFunc: async function() {
const mydata = await ...do await stuff...
return mydata;
}
});
- Same code with the workaround:
.. code-block:: javascript
var MyClass = OdooClass.extend({
myFunc: function() {
return new Promise(async (resolve, reject) => {
const mydata = await ...do await stuff...
return resolve(mydata);
});
}
});
* Fix issue when trying to run in localhost with several databases. The browser
doesn't send the cookie and web manifest returns 404.
* Evaluate to support 'require' system.
* 'Install PWA' menu option disappears even if not installed. This is due to the
very nature of service workers, so they are running including when you close
the page tabs.
Bug Tracker
===========
@@ -100,6 +152,7 @@ Authors
~~~~~~~
* TAKOBI
* Tecnativa
Contributors
~~~~~~~~~~~~
@@ -108,6 +161,11 @@ Contributors
* Lorenzo Battistini
* `Tecnativa <https://tecnativa.com>`_:
* Alexandre D. Díaz
* João Marques
Maintainers
~~~~~~~~~~~

View File

@@ -1 +1,2 @@
from . import controllers
from . import models

View File

@@ -1,4 +1,6 @@
# Copyright 2020 Lorenzo Battistini @ TAKOBI
# Copyright 2020 Tecnativa - Alexandre D. Díaz
# Copyright 2020 Tecnativa - João Marques
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
{
@@ -8,7 +10,7 @@
"development_status": "Beta",
"category": "Website",
"website": "https://github.com/OCA/web",
"author": "TAKOBI, Odoo Community Association (OCA)",
"author": "TAKOBI, Tecnativa, Odoo Community Association (OCA)",
"maintainers": ["eLBati"],
"license": "LGPL-3",
"application": True,
@@ -18,7 +20,9 @@
'mail',
],
"data": [
"views/webclient_templates.xml",
"templates/assets.xml",
"templates/service_worker.xml",
"views/res_config_settings_views.xml",
],
'qweb': [
'static/src/xml/pwa_install.xml',

View File

@@ -1 +1,3 @@
# Copyright 2020 Lorenzo Battistini @ TAKOBI
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
from . import main

View File

@@ -1,75 +1,120 @@
# Copyright 2020 Lorenzo Battistini @ TAKOBI
# Copyright 2020 Tecnativa - Alexandre D. Díaz
# Copyright 2020 Tecnativa - João Marques
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
import json
from odoo.http import request, Controller, route
class PWA(Controller):
def _get_pwa_scripts(self):
"""Scripts to be imported in the service worker (Order is important)"""
return [
"/web/static/lib/underscore/underscore.js",
"/web_pwa_oca/static/src/js/worker/libs/class.js",
"/web_pwa_oca/static/src/js/worker/pwa.js",
]
def get_asset_urls(self, asset_xml_id):
qweb = request.env['ir.qweb'].sudo()
assets = qweb._get_asset_nodes(asset_xml_id, {}, True, True)
urls = []
for asset in assets:
if asset[0] == 'link':
urls.append(asset[1]['href'])
if asset[0] == 'script':
urls.append(asset[1]['src'])
return urls
@route("/service-worker.js", type="http", auth="public")
def render_service_worker(self):
"""Route to register the service worker in the 'main' scope ('/')"""
return request.render(
"web_pwa_oca.service_worker",
{
"pwa_scripts": self._get_pwa_scripts(),
"pwa_params": self._get_pwa_params(),
},
headers=[("Content-Type", "text/javascript;charset=utf-8")],
)
@route('/service-worker.js', type='http', auth="public")
def service_worker(self):
qweb = request.env['ir.qweb'].sudo()
urls = []
urls.extend(self.get_asset_urls("web.assets_common"))
urls.extend(self.get_asset_urls("web.assets_backend"))
version_list = []
for url in urls:
version_list.append(url.split('/')[3])
cache_version = '-'.join(version_list)
mimetype = 'text/javascript;charset=utf-8'
content = qweb.render('web_pwa_oca.service_worker', {
'pwa_cache_name': cache_version,
'pwa_files_to_cache': urls,
})
return request.make_response(content, [('Content-Type', mimetype)])
def _get_pwa_params(self):
"""Get javascript PWA class initialzation params"""
return {}
@route('/web_pwa_oca/manifest.json', type='http', auth="public")
def manifest(self):
qweb = request.env['ir.qweb'].sudo()
config_param = request.env['ir.config_parameter'].sudo()
pwa_name = config_param.get_param("pwa.manifest.name", "Odoo PWA")
pwa_short_name = config_param.get_param("pwa.manifest.short_name", "Odoo PWA")
icon128x128 = config_param.get_param(
"pwa.manifest.icon128x128",
"/web_pwa_oca/static/img/icons/icon-128x128.png")
icon144x144 = config_param.get_param(
"pwa.manifest.icon144x144",
"/web_pwa_oca/static/img/icons/icon-144x144.png")
icon152x152 = config_param.get_param(
"pwa.manifest.icon152x152",
"/web_pwa_oca/static/img/icons/icon-152x152.png")
icon192x192 = config_param.get_param(
"pwa.manifest.icon192x192",
"/web_pwa_oca/static/img/icons/icon-192x192.png")
icon256x256 = config_param.get_param(
"pwa.manifest.icon256x256",
"/web_pwa_oca/static/img/icons/icon-256x256.png")
icon512x512 = config_param.get_param(
"pwa.manifest.icon512x512",
"/web_pwa_oca/static/img/icons/icon-512x512.png")
background_color = config_param.get_param(
"pwa.manifest.background_color", "#2E69B5")
theme_color = config_param.get_param(
"pwa.manifest.theme_color", "#2E69B5")
mimetype = 'application/json;charset=utf-8'
content = qweb.render('web_pwa_oca.manifest', {
'pwa_name': pwa_name,
'pwa_short_name': pwa_short_name,
'icon128x128': icon128x128,
'icon144x144': icon144x144,
'icon152x152': icon152x152,
'icon192x192': icon192x192,
'icon256x256': icon256x256,
'icon512x512': icon512x512,
'background_color': background_color,
'theme_color': theme_color,
})
return request.make_response(content, [('Content-Type', mimetype)])
def _get_pwa_manifest_icons(self, pwa_icon):
icons = []
if not pwa_icon:
for size in [
(128, 128),
(144, 144),
(152, 152),
(192, 192),
(256, 256),
(512, 512),
]:
icons.append(
{
"src": "/web_pwa_oca/static/img/icons/icon-%sx%s.png"
% (str(size[0]), str(size[1])),
"sizes": "%sx%s" % (str(size[0]), str(size[1])),
"type": "image/png",
}
)
elif not pwa_icon.mimetype.startswith("image/svg"):
all_icons = (
request.env["ir.attachment"]
.sudo()
.search(
[
("url", "like", "/web_pwa_oca/icon"),
(
"url",
"not like",
"/web_pwa_oca/icon.",
), # Get only resized icons
]
)
)
for icon in all_icons:
icon_size_name = icon.url.split("/")[-1].lstrip("icon").split(".")[0]
icons.append(
{
"src": icon.url,
"sizes": icon_size_name,
"type": icon.mimetype,
}
)
else:
icons = [
{
"src": pwa_icon.url,
"sizes": "128x128 144x144 152x152 192x192 256x256 512x512",
"type": pwa_icon.mimetype,
}
]
return icons
def _get_pwa_manifest(self):
"""Webapp manifest"""
config_param_sudo = request.env["ir.config_parameter"].sudo()
pwa_name = config_param_sudo.get_param("pwa.manifest.name", "Odoo PWA")
pwa_short_name = config_param_sudo.get_param(
"pwa.manifest.short_name", "Odoo PWA"
)
pwa_icon = (
request.env["ir.attachment"]
.sudo()
.search([("url", "like", "/web_pwa_oca/icon.")])
)
background_color = config_param_sudo.get_param(
"pwa.manifest.background_color", "#2E69B5"
)
theme_color = config_param_sudo.get_param("pwa.manifest.theme_color", "#2E69B5")
return {
"name": pwa_name,
"short_name": pwa_short_name,
"icons": self._get_pwa_manifest_icons(pwa_icon),
"start_url": '/web',
"display": "standalone",
"background_color": background_color,
"theme_color": theme_color,
}
@route("/web_pwa_oca/manifest.webmanifest", type="http", auth="public")
def pwa_manifest(self):
"""Returns the manifest used to install the page as app"""
return request.make_response(
json.dumps(self._get_pwa_manifest()),
headers=[("Content-Type", "application/json;charset=utf-8")],
)

View File

@@ -0,0 +1 @@
from . import res_config_settings

View File

@@ -0,0 +1,162 @@
# Copyright 2020 Tecnativa - João Marques
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
import sys
import base64
import io
from PIL import Image
from odoo import api, exceptions, fields, models, _
from odoo.tools.mimetypes import guess_mimetype
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
_pwa_icon_url_base = "/web_pwa_oca/icon"
pwa_name = fields.Char(
"Progressive Web App Name", help="Name of the Progressive Web Application"
)
pwa_short_name = fields.Char(
"Progressive Web App Short Name",
help="Short Name of the Progressive Web Application",
)
pwa_icon = fields.Binary("Icon", readonly=False)
pwa_background_color = fields.Char("Background Color")
pwa_theme_color = fields.Char("Theme Color")
@api.model
def get_values(self):
config_parameter_obj_sudo = self.env["ir.config_parameter"].sudo()
res = super(ResConfigSettings, self).get_values()
res["pwa_name"] = (
config_parameter_obj_sudo.get_param(
"pwa.manifest.name", default="Odoo PWA")
)
res["pwa_short_name"] = (
config_parameter_obj_sudo.get_param(
"pwa.manifest.short_name", default="Odoo")
)
pwa_icon_ir_attachment = (
self.env["ir.attachment"]
.sudo()
.search([("url", "like", self._pwa_icon_url_base + ".")])
)
res["pwa_icon"] = (
pwa_icon_ir_attachment.datas if pwa_icon_ir_attachment else False
)
res["pwa_background_color"] = (
config_parameter_obj_sudo.get_param(
"pwa.manifest.background_color", default="#2E69B5")
)
res["pwa_theme_color"] = (
config_parameter_obj_sudo.get_param(
"pwa.manifest.theme_color", default="#2E69B5")
)
return res
def _unpack_icon(self, icon):
# Wrap decoded_icon in BytesIO object
decoded_icon = base64.b64decode(icon)
icon_bytes = io.BytesIO(decoded_icon)
return Image.open(icon_bytes)
def _write_icon_to_attachment(self, extension, mimetype, size=None):
url = self._pwa_icon_url_base + extension
icon = self.pwa_icon
# Resize image
if size:
image = self._unpack_icon(icon)
resized_image = image.resize(size)
icon_bytes_output = io.BytesIO()
resized_image.save(icon_bytes_output, format=extension.lstrip(".").upper())
icon = base64.b64encode(icon_bytes_output.getvalue())
url = "%s%sx%s%s" % (
self._pwa_icon_url_base,
str(size[0]),
str(size[1]),
extension,
)
# Retreive existing attachment
existing_attachment = (
self.env["ir.attachment"].sudo().search([("url", "like", url)])
)
# Write values to ir_attachment
values = {
"datas": icon,
"db_datas": icon,
"url": url,
"name": url,
"type": "binary",
"mimetype": mimetype,
}
# Rewrite if exists, else create
if existing_attachment:
existing_attachment.sudo().write(values)
else:
self.env["ir.attachment"].sudo().create(values)
@api.model
def set_values(self):
config_parameter_obj_sudo = self.env["ir.config_parameter"].sudo()
res = super(ResConfigSettings, self).set_values()
config_parameter_obj_sudo.set_param(
"pwa.manifest.name", self.pwa_name
)
config_parameter_obj_sudo.set_param(
"pwa.manifest.short_name", self.pwa_short_name
)
config_parameter_obj_sudo.set_param(
"pwa.manifest.background_color", self.pwa_background_color
)
config_parameter_obj_sudo.set_param(
"pwa.manifest.theme_color", self.pwa_theme_color
)
# Retrieve previous value for pwa_icon from ir_attachment
pwa_icon_ir_attachments = (
self.env["ir.attachment"]
.sudo()
.search([("url", "like", self._pwa_icon_url_base)])
)
# Delete or ignore if no icon provided
if not self.pwa_icon:
if pwa_icon_ir_attachments:
pwa_icon_ir_attachments.unlink()
return res
# Fail if icon provided is larger than 2mb
if sys.getsizeof(self.pwa_icon) > 2196608:
raise exceptions.UserError(
_("You can't upload a file with more than 2 MB.")
)
# Confirm if the pwa_icon binary content is an SVG or PNG
# and process accordingly
decoded_pwa_icon = base64.b64decode(self.pwa_icon)
# Full mimetype detection
pwa_icon_mimetype = guess_mimetype(decoded_pwa_icon)
pwa_icon_extension = "." + pwa_icon_mimetype.split("/")[-1].split("+")[0]
if not pwa_icon_mimetype.startswith(
"image/svg"
) and not pwa_icon_mimetype.startswith("image/png"):
raise exceptions.UserError(_("You can only upload SVG or PNG files"))
# Delete all previous records if we are writting new ones
if pwa_icon_ir_attachments:
pwa_icon_ir_attachments.unlink()
self._write_icon_to_attachment(pwa_icon_extension, pwa_icon_mimetype)
# write multiple sizes if not SVG
if pwa_icon_extension != ".svg":
# Fail if provided PNG is smaller than 512x512
if self._unpack_icon(self.pwa_icon).size < (512, 512):
raise exceptions.UserError(
_("You can only upload PNG files bigger than 512x512")
)
for size in [
(128, 128),
(144, 144),
(152, 152),
(192, 192),
(256, 256),
(512, 512),
]:
self._write_icon_to_attachment(
pwa_icon_extension, pwa_icon_mimetype, size=size
)

View File

@@ -1,10 +1,11 @@
The following system parameters con be set to customize the appearance of the application
This module allows you to set the following parameters under settings to customize the appearance of the application
* pwa.manifest.name (defaults to "Odoo PWA")
* pwa.manifest.short_name (defaults to "Odoo PWA")
* pwa.manifest.icon128x128 (defaults to "/web_pwa_oca/static/img/icons/icon-128x128.png")
* pwa.manifest.icon144x144 (defaults to "/web_pwa_oca/static/img/icons/icon-144x144.png")
* pwa.manifest.icon152x152 (defaults to "/web_pwa_oca/static/img/icons/icon-152x152.png")
* pwa.manifest.icon192x192 (defaults to "/web_pwa_oca/static/img/icons/icon-192x192.png")
* pwa.manifest.icon256x256 (defaults to "/web_pwa_oca/static/img/icons/icon-256x256.png")
* pwa.manifest.icon512x512 (defaults to "/web_pwa_oca/static/img/icons/icon-512x512.png")
* PWA Name (defaults to "Odoo PWA")
* PWA Short Name (defaults to "Odoo PWA")
* PWA Icon (**SVG**) (defaults to "/web_pwa_oca/static/img/icons/odoo-logo.svg")
To configure your PWA:
#. Go to **Settings > General Settings > Progressive Web App**.
#. Set the parameters (*Note:* Icon **must be a SVG file**)
#. **Save**

View File

@@ -1,3 +1,8 @@
* `TAKOBI <https://takobi.online>`_:
* Lorenzo Battistini
* `Tecnativa <https://tecnativa.com>`_:
* Alexandre D. Díaz
* João Marques

View File

@@ -3,3 +3,14 @@ Make Odoo an installable Progressive Web Application.
Progressive Web Apps provide an installable, app-like experience on desktop and mobile that are built and delivered directly via the web.
They're web apps that are fast and reliable. And most importantly, they're web apps that work in any browser.
If you're building a web app today, you're already on the path towards building a Progressive Web App.
+ Developers Info.
The service worker is contructed using 'Odoo Class' to have the same class inheritance behaviour that in the 'user pages'. Be noticed
that 'Odoo Bootstrap' is not supported so, you can't use 'require' here.
All service worker content can be found in 'static/src/js/worker'. The management between 'user pages' and service worker is done in
'pwa_manager.js'.
The purpose of this module is give a base to make PWA applications.

View File

@@ -1,5 +1,37 @@
* Evaluate to extend ``FILES_TO_CACHE``
* Evaluate to use a normal JS file for service worker and download data from a normal JSON controller
* Integrate `Notification API <https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification>`_
* Integrate `Web Share API <https://web.dev/web-share/>`_
* Create ``portal_pwa`` module, intended to be used by front-end users (customers, suppliers...)
* Current *John Resig's inheritance* implementation doesn't support ``async``
functions because ``this._super`` can't be called inside a promise. So we
need to use the following workaround:
- Natural 'async/await' example (This breaks "_super" call):
.. code-block:: javascript
var MyClass = OdooClass.extend({
myFunc: async function() {
const mydata = await ...do await stuff...
return mydata;
}
});
- Same code with the workaround:
.. code-block:: javascript
var MyClass = OdooClass.extend({
myFunc: function() {
return new Promise(async (resolve, reject) => {
const mydata = await ...do await stuff...
return resolve(mydata);
});
}
});
* Fix issue when trying to run in localhost with several databases. The browser
doesn't send the cookie and web manifest returns 404.
* Evaluate to support 'require' system.
* 'Install PWA' menu option disappears even if not installed. This is due to the
very nature of service workers, so they are running including when you close
the page tabs.

View File

@@ -0,0 +1,4 @@
To use your PWA:
#. Open the Odoo web app using a supported browser (like Chrome/Chromium)
#. Install the PWA

View File

@@ -372,17 +372,26 @@ ul.auto-toc {
<p>Progressive Web Apps provide an installable, app-like experience on desktop and mobile that are built and delivered directly via the web.
Theyre web apps that are fast and reliable. And most importantly, theyre web apps that work in any browser.
If youre building a web app today, youre already on the path towards building a Progressive Web App.</p>
<ul class="simple">
<li>Developers Info.</li>
</ul>
<p>The service worker is contructed using Odoo Class to have the same class inheritance behaviour that in the user pages. Be noticed
that Odoo Bootstrap is not supported so, you cant use require here.</p>
<p>All service worker content can be found in static/src/js/worker. The management between user pages and service worker is done in
pwa_manager.js.</p>
<p>The purpose of this module is give a base to make PWA applications.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#installation" id="id1">Installation</a></li>
<li><a class="reference internal" href="#configuration" id="id2">Configuration</a></li>
<li><a class="reference internal" href="#known-issues-roadmap" id="id3">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id4">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id5">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id6">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id7">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id8">Maintainers</a></li>
<li><a class="reference internal" href="#usage" id="id3">Usage</a></li>
<li><a class="reference internal" href="#known-issues-roadmap" id="id4">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id5">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id6">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id7">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id8">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id9">Maintainers</a></li>
</ul>
</li>
</ul>
@@ -406,30 +415,77 @@ And like all other installed apps, its a top level app in the task switcher.<
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#id2">Configuration</a></h1>
<p>The following system parameters con be set to customize the appearance of the application</p>
<p>This module allows you to set the following parameters under settings to customize the appearance of the application</p>
<ul class="simple">
<li>pwa.manifest.name (defaults to “Odoo PWA”)</li>
<li>pwa.manifest.short_name (defaults to “Odoo PWA”)</li>
<li>pwa.manifest.icon128x128 (defaults to “/web_pwa_oca/static/img/icons/icon-128x128.png”)</li>
<li>pwa.manifest.icon144x144 (defaults to “/web_pwa_oca/static/img/icons/icon-144x144.png”)</li>
<li>pwa.manifest.icon152x152 (defaults to “/web_pwa_oca/static/img/icons/icon-152x152.png”)</li>
<li>pwa.manifest.icon192x192 (defaults to “/web_pwa_oca/static/img/icons/icon-192x192.png”)</li>
<li>pwa.manifest.icon256x256 (defaults to “/web_pwa_oca/static/img/icons/icon-256x256.png”)</li>
<li>pwa.manifest.icon512x512 (defaults to “/web_pwa_oca/static/img/icons/icon-512x512.png”)</li>
<li>PWA Name (defaults to “Odoo PWA”)</li>
<li>PWA Short Name (defaults to “Odoo PWA”)</li>
<li>PWA Icon (<strong>SVG</strong>) (defaults to “/web_pwa_oca/static/img/icons/odoo-logo.svg”)</li>
</ul>
<p>To configure your PWA:</p>
<ol class="arabic simple">
<li>Go to <strong>Settings &gt; General Settings &gt; Progressive Web App</strong>.</li>
<li>Set the parameters (<em>Note:</em> Icon <strong>must be a SVG file</strong>)</li>
<li><strong>Save</strong></li>
</ol>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#id3">Usage</a></h1>
<p>To use your PWA:</p>
<ol class="arabic simple">
<li>Open the Odoo web app using a supported browser (like Chrome/Chromium)</li>
<li>Install the PWA</li>
</ol>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#id3">Known issues / Roadmap</a></h1>
<ul class="simple">
<li>Evaluate to extend <tt class="docutils literal">FILES_TO_CACHE</tt></li>
<li>Evaluate to use a normal JS file for service worker and download data from a normal JSON controller</li>
<li>Integrate <a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification">Notification API</a></li>
<li>Integrate <a class="reference external" href="https://web.dev/web-share/">Web Share API</a></li>
<li>Create <tt class="docutils literal">portal_pwa</tt> module, intended to be used by front-end users (customers, suppliers…)</li>
<h1><a class="toc-backref" href="#id4">Known issues / Roadmap</a></h1>
<ul>
<li><p class="first">Integrate <a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification">Notification API</a></p>
</li>
<li><p class="first">Integrate <a class="reference external" href="https://web.dev/web-share/">Web Share API</a></p>
</li>
<li><p class="first">Create <tt class="docutils literal">portal_pwa</tt> module, intended to be used by front-end users (customers, suppliers…)</p>
</li>
<li><p class="first">Current <em>John Resigs inheritance</em> implementation doesnt support <tt class="docutils literal">async</tt>
functions because <tt class="docutils literal">this._super</tt> cant be called inside a promise. So we
need to use the following workaround:</p>
<ul>
<li><p class="first">Natural async/await example (This breaks “_super” call):</p>
<pre class="code javascript literal-block">
<span class="kd">var</span> <span class="nx">MyClass</span> <span class="o">=</span> <span class="nx">OdooClass</span><span class="p">.</span><span class="nx">extend</span><span class="p">({</span>
<span class="nx">myFunc</span><span class="o">:</span> <span class="nx">async</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="kr">const</span> <span class="nx">mydata</span> <span class="o">=</span> <span class="nx">await</span> <span class="p">...</span><span class="k">do</span> <span class="nx">await</span> <span class="nx">stuff</span><span class="p">...</span>
<span class="k">return</span> <span class="nx">mydata</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">});</span>
</pre>
</li>
<li><p class="first">Same code with the workaround:</p>
<pre class="code javascript literal-block">
<span class="kd">var</span> <span class="nx">MyClass</span> <span class="o">=</span> <span class="nx">OdooClass</span><span class="p">.</span><span class="nx">extend</span><span class="p">({</span>
<span class="nx">myFunc</span><span class="o">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">(</span><span class="nx">async</span> <span class="p">(</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
<span class="kr">const</span> <span class="nx">mydata</span> <span class="o">=</span> <span class="nx">await</span> <span class="p">...</span><span class="k">do</span> <span class="nx">await</span> <span class="nx">stuff</span><span class="p">...</span>
<span class="k">return</span> <span class="nx">resolve</span><span class="p">(</span><span class="nx">mydata</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="p">});</span>
</pre>
</li>
</ul>
</li>
<li><p class="first">Fix issue when trying to run in localhost with several databases. The browser
doesnt send the cookie and web manifest returns 404.</p>
</li>
<li><p class="first">Evaluate to support require system.</p>
</li>
<li><p class="first">Install PWA menu option disappears even if not installed. This is due to the
very nature of service workers, so they are running including when you close
the page tabs.</p>
</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id4">Bug Tracker</a></h1>
<h1><a class="toc-backref" href="#id5">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/web/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
@@ -437,24 +493,30 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#id5">Credits</a></h1>
<h1><a class="toc-backref" href="#id6">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id6">Authors</a></h2>
<h2><a class="toc-backref" href="#id7">Authors</a></h2>
<ul class="simple">
<li>TAKOBI</li>
<li>Tecnativa</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id7">Contributors</a></h2>
<h2><a class="toc-backref" href="#id8">Contributors</a></h2>
<ul class="simple">
<li><a class="reference external" href="https://takobi.online">TAKOBI</a>:<ul>
<li>Lorenzo Battistini</li>
</ul>
</li>
<li><a class="reference external" href="https://tecnativa.com">Tecnativa</a>:<ul>
<li>Alexandre D. Díaz</li>
<li>João Marques</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id8">Maintainers</a></h2>
<h2><a class="toc-backref" href="#id9">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="919" height="495" viewBox="0 0 919 495"><g fill="none"><path fill="#8F8F8F" d="M695 346c-41.421 0-75-33.579-75-75s33.579-75 75-75 75 33.579 75 75-33.579 75-75 75zm0-31c24.3 0 44-19.7 44-44s-19.7-44-44-44-44 19.7-44 44 19.7 44 44 44zm-157 31c-41.421 0-75-33.579-75-75s33.579-75 75-75 75 33.579 75 75-33.579 75-75 75zm0-31c24.3 0 44-19.7 44-44s-19.7-44-44-44-44 19.7-44 44 19.7 44 44 44zm-82-45c0 41.935-33.592 76-75.009 76C339.575 346 306 312.005 306 270.07c0-41.936 30.5-74.07 74.991-74.07 16.442 0 31.647 3.496 44.007 12.58l.002-43.49c0-8.334 7.27-15.09 15.5-15.09 8.228 0 15.5 6.762 15.5 15.09V270zm-75 45c24.3 0 44-19.7 44-44s-19.7-44-44-44-44 19.7-44 44 19.7 44 44 44z"/><path fill="#875A7B" d="M224 346c-41.421 0-75-33.579-75-75s33.579-75 75-75 75 33.579 75 75-33.579 75-75 75zm0-31c24.3 0 44-19.7 44-44s-19.7-44-44-44-44 19.7-44 44 19.7 44 44 44z"/></g><script xmlns=""/><script xmlns="" type="text/javascript"/><script xmlns="" type="text/javascript"/></svg>

After

Width:  |  Height:  |  Size: 1012 B

View File

@@ -1,45 +1,40 @@
/* Copyright 2020 Lorenzo Battistini @ TAKOBI
Copyright 2020 Tecnativa - Alexandre D. Díaz
* License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */
odoo.define('web_pwa_oca.systray.install', function (require) {
"use strict";
"use strict";
var core = require('web.core');
var session = require('web.session');
var UserMenu = require('web.UserMenu');
var UserMenu = require('web.UserMenu');
var WebClientObj = require("web.web_client");
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('/service-worker.js')
.then(function (reg) {
console.log('Service worker registered.', reg);
});
});
}
var deferredInstallPrompt = null;
UserMenu.include({
UserMenu.include({
start: function () {
window.addEventListener('beforeinstallprompt', this.saveBeforeInstallPromptEvent);
return this._super.apply(this, arguments);
},
saveBeforeInstallPromptEvent: function(evt) {
deferredInstallPrompt = evt;
this.$.find('#pwa_install_button')[0].removeAttribute('hidden');
},
_onMenuInstallpwa: function () {
deferredInstallPrompt.prompt();
// Hide the install button, it can't be called twice.
this.el.setAttribute('hidden', true);
// Log user response to prompt.
deferredInstallPrompt.userChoice
.then(function (choice) {
if (choice.outcome === 'accepted') {
console.log('User accepted the A2HS prompt', choice);
} else {
console.log('User dismissed the A2HS prompt', choice);
}
deferredInstallPrompt = null;
/**
* We can't control if the UserMenu is loaded berfore PWA manager...
* So check if need unhide the user menu options to install the PWA.
*
* @override
*/
start: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
if (WebClientObj.pwa_manager.canBeInstalled()) {
self.$el.find('#pwa_install_button')[0]
.removeAttribute('hidden');
}
});
},
});
},
/**
* Handle 'Install PWA' user menu option click
*
* @private
*/
_onMenuInstallpwa: function () {
WebClientObj.pwa_manager.install();
},
});
});

View File

@@ -0,0 +1,95 @@
/* Copyright 2020 Tecnativa - Alexandre D. Díaz
* License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */
odoo.define("web_pwa_oca.PWAManager", function (require) {
"use strict";
var Widget = require("web.Widget");
var PWAManager = Widget.extend({
_deferredInstallPrompt: null,
/**
* @override
*/
init: function () {
this._super.apply(this, arguments);
if (!('serviceWorker' in navigator)) {
throw new Error(
"This browser is not compatible with service workers");
}
this._service_worker = navigator.serviceWorker;
this.registerServiceWorker('/service-worker.js');
window.addEventListener(
'beforeinstallprompt', this._onBeforeInstallPrompt.bind(this));
},
/**
* @param {String} sw_script
* @returns {Promise}
*/
registerServiceWorker: function (sw_script) {
return this._service_worker.register(sw_script)
.then(this._onRegisterServiceWorker)
.catch(function (error) {
console.log('[ServiceWorker] Registration failed: ', error);
});
},
install: function () {
if (!this._deferredInstallPrompt) {
return;
}
var self = this;
var systray_menu = this.getParent().menu.systray_menu;
this._deferredInstallPrompt.prompt();
// Log user response to prompt.
this._deferredInstallPrompt.userChoice
.then(function (choice) {
if (choice.outcome === 'accepted') {
// Hide the install button, it can't be called twice.
systray_menu.$el.find('#pwa_install_button')
.attr('hidden', true);
self._deferredInstallPrompt = null;
console.log('User accepted the A2HS prompt', choice);
} else {
console.log('User dismissed the A2HS prompt', choice);
}
});
},
canBeInstalled: function () {
return !_.isNull(this._deferredInstallPrompt);
},
/**
* Handle PWA installation flow
*
* @private
* @param {BeforeInstallPromptEvent} evt
*/
_onBeforeInstallPrompt: function (evt) {
evt.preventDefault();
this._deferredInstallPrompt = evt;
// UserMenu can be loaded after this module
var menu = this.getParent().menu;
if (menu && menu.systray_menu) {
menu.systray_menu.$el.find('#pwa_install_button')[0]
.removeAttribute('hidden');
}
},
/**
* Need register some extra API? override this!
*
* @private
* @param {ServiceWorkerRegistration} registration
*/
_onRegisterServiceWorker: function (registration) {
console.log('[ServiceWorker] Registered:', registration);
},
});
return PWAManager;
});

View File

@@ -0,0 +1,20 @@
/* Copyright 2020 Tecnativa - Alexandre D. Díaz
* License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */
odoo.define("web_pwa_oca.webclient", function (require) {
"use strict";
var WebClient = require("web.WebClient");
var PWAManager = require("web_pwa_oca.PWAManager");
WebClient.include({
/**
* @override
*/
show_application: function () {
this.pwa_manager = new PWAManager(this);
return this._super.apply(this, arguments);
},
});
});

View File

@@ -0,0 +1,150 @@
/**
* Improved John Resig's inheritance, based on:
*
* Simple JavaScript Inheritance
* By John Resig http://ejohn.org/
* MIT Licensed.
*
* Adds "include()"
*
* Defines The Class object. That object can be used to define and inherit classes using
* the extend() method.
*
* Example::
*
* var Person = Class.extend({
* init: function(isDancing){
* this.dancing = isDancing;
* },
* dance: function(){
* return this.dancing;
* }
* });
*
* The init() method act as a constructor. This class can be instanced this way::
*
* var person = new Person(true);
* person.dance();
*
* The Person class can also be extended again:
*
* var Ninja = Person.extend({
* init: function(){
* this._super( false );
* },
* dance: function(){
* // Call the inherited version of dance()
* return this._super();
* },
* swingSword: function(){
* return true;
* }
* });
*
* When extending a class, each re-defined method can use this._super() to call the previous
* implementation of that method.
*
* @class Class
*/
function OdooClass(){}
var initializing = false;
var fnTest = /xyz/.test(function(){xyz();}) ? /\b_super\b/ : /.*/;
/**
* Subclass an existing class
*
* @param {Object} prop class-level properties (class attributes and instance methods) to set on the new class
*/
OdooClass.extend = function() {
var _super = this.prototype;
// Support mixins arguments
var args = _.toArray(arguments);
args.unshift({});
var prop = _.extend.apply(_,args);
// Instantiate a web class (but only create the instance,
// don't run the init constructor)
initializing = true;
var This = this;
var prototype = new This();
initializing = false;
// Copy the properties over onto the new prototype
_.each(prop, function(val, name) {
// Check if we're overwriting an existing function
prototype[name] = typeof prop[name] == "function" &&
fnTest.test(prop[name]) ?
(function(name, fn) {
return function() {
var tmp = this._super;
// Add a new ._super() method that is the same
// method but on the super-class
this._super = _super[name];
// The method only need to be bound temporarily, so
// we remove it when we're done executing
var ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
};
})(name, prop[name]) :
prop[name];
});
// The dummy class constructor
function Class() {
if(this.constructor !== OdooClass){
throw new Error("You can only instanciate objects with the 'new' operator");
}
// All construction is actually done in the init method
this._super = null;
if (!initializing && this.init) {
var ret = this.init.apply(this, arguments);
if (ret) { return ret; }
}
return this;
}
Class.include = function (properties) {
_.each(properties, function(val, name) {
if (typeof properties[name] !== 'function'
|| !fnTest.test(properties[name])) {
prototype[name] = properties[name];
} else if (typeof prototype[name] === 'function'
&& prototype.hasOwnProperty(name)) {
prototype[name] = (function (name, fn, previous) {
return function () {
var tmp = this._super;
this._super = previous;
var ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
};
})(name, properties[name], prototype[name]);
} else if (typeof _super[name] === 'function') {
prototype[name] = (function (name, fn) {
return function () {
var tmp = this._super;
this._super = _super[name];
var ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
};
})(name, properties[name]);
}
});
};
// Populate our constructed prototype object
Class.prototype = prototype;
// Enforce the constructor to be what we expect
Class.constructor = Class;
// And make this class extendable
Class.extend = this.extend;
return Class;
};

View File

@@ -0,0 +1,35 @@
"use strict";
/* eslint strict: ["error", "global"] */
/* eslint-disable no-undef, no-empty-function, no-implicit-globals,
no-unused-vars */
/* Copyright 2020 Tecnativa - Alexandre D. Díaz
* License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */
/**
* Services workers are a piece of software separated from the user page.
* Here can't use 'Odoo Bootstrap', so we can't work with 'require' system.
* When the service worker is called to be installed from the "pwa_manager"
* this class is instantiated.
*/
var PWA = OdooClass.extend({
// eslint-disable-next-line
init: function (params) {
// To be overridden
},
/**
* @returns {Promise}
*/
installWorker: function () {
return Promise.resolve();
},
/**
* @returns {Promise}
*/
activateWorker: function () {
return Promise.resolve();
},
});

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="web_layout_pwa" name="Web layout PWA" inherit_id="web.layout">
<xpath expr="//meta[@name='viewport']" position="after">
<!-- Add link rel manifest -->
<link rel="manifest" t-attf-href="/web_pwa_oca/manifest.webmanifest"/>
<!-- Add iOS meta tags and icons -->
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
<t t-set="pwa_name" t-value="request.env['ir.config_parameter'].sudo().get_param('pwa.manifest.name')"/>
<meta name="apple-mobile-web-app-title" t-att-content="pwa_name"/>
<link rel="apple-touch-icon" href="/web_pwa_oca/static/img/icons/icon-152x152.png"/>
<!-- Add meta theme-color -->
<t t-set="pwa_theme_color" t-value="request.env['ir.config_parameter'].sudo().get_param('pwa.manifest.theme_color')"/>
<meta name="theme-color" t-att-content="pwa_theme_color" />
</xpath>
</template>
<template id="assets_backend" name="web service worker assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/web_pwa_oca/static/src/js/pwa_manager.js"></script>
<script type="text/javascript" src="/web_pwa_oca/static/src/js/pwa_install.js"></script>
<script type="text/javascript" src="/web_pwa_oca/static/src/js/webclient.js"></script>
</xpath>
</template>
</odoo>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="pwa_init" name="PWA Initialization">
const oca_pwa = new PWA(<t t-esc="str(pwa_params)"/>);
</template>
<template id="pwa_core_events" name="PWA Core Events">
<t t-set="evt_install">
evt.waitUntil(oca_pwa.installWorker());
self.skipWaiting();
</t>
self.addEventListener('install', evt =&gt; {
console.log('[ServiceWorker] Installing...');
<t t-raw="evt_install" />
});
<!-- This is necessary to get PWA installable.
Other modules can add logic using 'evt_fetch' -->
<t t-set="evt_fetch" />
self.addEventListener('fetch', evt =&gt; {
<t t-raw="evt_fetch" />
});
<t t-set="evt_activate">
console.log('[ServiceWorker] Activating...');
evt.waitUntil(oca_pwa.activateWorker());
self.clients.claim();
</t>
self.addEventListener('activate', evt =&gt; {
<t t-raw="evt_activate" />
});
</template>
<template id="service_worker" name="PWA Service Worker">
self.importScripts(...<t t-esc="str(pwa_scripts)" />);
<t t-call="web_pwa_oca.pwa_init" />
<t t-call="web_pwa_oca.pwa_core_events" />
</template>
</odoo>

View File

@@ -0,0 +1 @@
from . import test_web_pwa_oca_controller

View File

@@ -0,0 +1,125 @@
# Copyright 2020 João Marques
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import json
import base64
import odoo.tests
from odoo import exceptions
from odoo.modules.module import get_resource_path
class TestUi(odoo.tests.HttpCase):
def setUp(self):
super().setUp()
self.user = self.env.ref("base.user_admin")
self.res_config_settings_obj = (
self.env["res.config.settings"].sudo(self.user.id).create({})
)
def test_manifest_valid_json(self):
# Call the manifest controller
manifest_data = self.url_open("/web_pwa_oca/manifest.webmanifest")
# should be valid json
manifest_content_str = manifest_data.content.decode("utf-8")
json.loads(manifest_content_str)
def test_manifest_correct_paramenters(self):
# Set PWA parameters in settings
self.res_config_settings_obj.pwa_name = "Test PWA"
self.res_config_settings_obj.pwa_short_name = "Test"
# icon should remain the default one
self.res_config_settings_obj.pwa_icon = False
self.res_config_settings_obj.set_values()
# Call the manifest controller
manifest_data = self.url_open("/web_pwa_oca/manifest.webmanifest")
manifest_content_str = manifest_data.content.decode("utf-8")
manifest_content = json.loads(manifest_content_str)
self.assertEquals(manifest_content["name"], "Test PWA")
self.assertEquals(manifest_content["short_name"], "Test")
# icon should remain the default one
self.assertEquals(
manifest_content["icons"][0]["src"],
"/web_pwa_oca/static/img/icons/icon-128x128.png",
)
self.assertEquals(manifest_content["icons"][0]["sizes"], "128x128")
self.assertTrue(manifest_content["icons"][0]["type"].startswith("image/png"))
def test_manifest_logo_upload(self):
with open(
"%s/static/img/icons/odoo_logo.svg" % get_resource_path("web_pwa_oca"),
"rb"
) as fi:
icon_to_send = base64.b64encode(fi.read())
# Set PWA icon in settings
self.res_config_settings_obj.pwa_icon = icon_to_send
self.res_config_settings_obj.set_values()
# Call the manifest controller
manifest_data = self.url_open("/web_pwa_oca/manifest.webmanifest")
manifest_content_str = manifest_data.content.decode("utf-8")
manifest_content = json.loads(manifest_content_str)
self.assertEquals(manifest_content["icons"][0]["src"], "/web_pwa_oca/icon.svg")
self.assertTrue(manifest_content["icons"][0]["type"].startswith("image/svg"))
self.assertEquals(
manifest_content["icons"][0]["sizes"],
"128x128 144x144 152x152 192x192 256x256 512x512")
# Get the icon and compare it
icon_data = self.url_open("/web_pwa_oca/icon.svg")
icon_data_bytes = base64.b64encode(icon_data.content)
self.assertEquals(icon_data_bytes, icon_to_send)
def test_png_logo_upload(self):
with open(
"%s/static/img/icons/icon-512x512.png" % get_resource_path("web_pwa_oca"),
"rb"
) as fi:
icon_to_send = base64.b64encode(fi.read())
# Set PWA icon in settings
self.res_config_settings_obj.pwa_icon = icon_to_send
self.res_config_settings_obj.set_values()
# Call the manifest controller
manifest_data = self.url_open("/web_pwa_oca/manifest.webmanifest")
manifest_content_str = manifest_data.content.decode("utf-8")
manifest_content = json.loads(manifest_content_str)
expected_vals = {
"src": "/web_pwa_oca/icon512x512.png",
"sizes": "512x512",
"type": "image/png",
}
self.assertTrue(expected_vals in manifest_content["icons"])
def test_manifest_logo_upload_big(self):
# Set PWA icon in settings
with self.assertRaises(exceptions.UserError):
# Image with more than 2MB
self.res_config_settings_obj.pwa_icon = b"a" * 3000000
self.res_config_settings_obj.set_values()
def test_manifest_logo_upload_extension(self):
with self.assertRaises(exceptions.UserError):
# Image that is not SVG or PNG
self.res_config_settings_obj.pwa_icon = b"a" * 1000
self.res_config_settings_obj.set_values()
def test_manifest_logo_upload_small(self):
icon_to_send = None
with open(
"%s/static/img/icons/icon-128x128.png" % get_resource_path("web_pwa_oca"),
"rb"
) as fi:
icon_to_send = base64.b64encode(fi.read())
# Set PWA icon in settings
with self.assertRaises(exceptions.UserError):
# Image smaller than 512X512
self.res_config_settings_obj.pwa_icon = icon_to_send
self.res_config_settings_obj.set_values()

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.pwa</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base_setup.res_config_settings_view_form"/>
<field name="arch" type="xml">
<div id="emails" position='after'>
<h2>Progressive Web App</h2>
<div class="row mt16 o_settings_container" id="pwa_settings">
<div class="col-12 col-lg-6 o_setting_box" id="domain_setting">
<div class="o_setting_right_pane">
<label for="pwa_name" string="PWA Title"/>
<span class="fa fa-lg fa-globe"/>
<div class="text-muted">
Name and icon of your PWA
</div>
<div class="content-group">
<div class="row mt16">
<label class="col-lg-3 o_light_label" string="Name" for="pwa_name"/>
<field name="pwa_name"/>
</div>
<div class="row mt16">
<label class="col-lg-3 o_light_label" string="Short Name" for="pwa_short_name"/>
<field name="pwa_short_name"/>
</div>
<div class="row">
<label class="col-lg-3 o_light_label" for="pwa_background_color"/>
<field name="pwa_background_color"/>
</div>
<div class="row">
<label class="col-lg-3 o_light_label" for="pwa_theme_color"/>
<field name="pwa_theme_color"/>
</div>
<div class="row">
<label class="col-lg-3 o_light_label" for="pwa_icon" />
<field name="pwa_icon" widget="image" class="float-left oe_avatar"/>
</div>
</div>
</div>
</div>
</div>
</div>
</field>
</record>
</data>
</odoo>

View File

@@ -1,105 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="web_layout_pwa" name="Web layout PWA" inherit_id="web.layout">
<xpath expr="//meta[@name='viewport']" position="after">
<!-- Add link rel manifest -->
<link rel="manifest" href="/web_pwa_oca/manifest.json"/>
<!-- Add iOS meta tags and icons -->
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
<meta name="apple-mobile-web-app-title" content="Odoo PWA"/>
<link rel="apple-touch-icon" href="/web_pwa_oca/static/img/icons/icon-152x152.png"/>
<!-- Add meta theme-color -->
<meta name="theme-color" content="#2E69B5" />
</xpath>
</template>
<template id="assets_backend" name="PWA assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/web_pwa_oca/static/src/js/pwa_install.js"/>
</xpath>
</template>
<template id="service_worker" name="PWA service worker">
'use strict';
const CACHE_NAME = '<t t-esc="pwa_cache_name"/>';
const FILES_TO_CACHE = [
<t t-foreach="pwa_files_to_cache" t-as="file_to_cache">
'<t t-esc="file_to_cache"/>',
</t>
];
self.addEventListener('install', function (evt) {
console.log('[ServiceWorker] Install');
evt.waitUntil(
caches.open(CACHE_NAME).then(function (cache) {
console.log('[ServiceWorker] Pre-caching offline page');
return cache.addAll(FILES_TO_CACHE);
})
);
self.skipWaiting();
});
self.addEventListener('activate', function(evt) {
console.log('[ServiceWorker] Activate');
evt.waitUntil(
caches.keys().then(function(keyList) {
return Promise.all(keyList.map(function(key) {
if (key !== CACHE_NAME) {
console.log('[ServiceWorker] Removing old cache', key);
return caches.delete(key);
}
}));
})
);
self.clients.claim();
});
self.addEventListener('fetch', function(evt) {
if (evt.request.cache === 'only-if-cached' &amp;&amp; evt.request.mode !== 'same-origin') {
return;
}
console.log('[ServiceWorker] Fetch', evt.request.url);
evt.respondWith(
caches.open(CACHE_NAME).then(function(cache) {
return cache.match(evt.request)
.then(function(response) {
return response || fetch(evt.request);
});
})
);
});
</template>
<template id="manifest" name="PWA manifest">
{
"name": "<t t-esc="pwa_name"/>",
"short_name": "<t t-esc="pwa_short_name"/>",
"icons": [{
"src": "<t t-esc="icon128x128"/>",
"sizes": "128x128",
"type": "image/png"
}, {
"src": "<t t-esc="icon144x144"/>",
"sizes": "144x144",
"type": "image/png"
}, {
"src": "<t t-esc="icon152x152"/>",
"sizes": "152x152",
"type": "image/png"
}, {
"src": "<t t-esc="icon192x192"/>",
"sizes": "192x192",
"type": "image/png"
}, {
"src": "<t t-esc="icon256x256"/>",
"sizes": "256x256",
"type": "image/png"
}, {
"src": "<t t-esc="icon512x512"/>",
"sizes": "512x512",
"type": "image/png"
}],
"start_url": "/web",
"display": "standalone",
"background_color": "<t t-esc="background_color"/>",
"theme_color": "<t t-esc="theme_color"/>"
}
</template>
</odoo>