pull/3103/merge
jwaes 2025-04-23 20:31:03 +02:00 committed by GitHub
commit daba291893
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 4128 additions and 27 deletions

View File

@ -43,7 +43,7 @@ repos:
hooks:
- id: whool-init
- repo: https://github.com/oca/maintainer-tools
rev: bf9ecb9938b6a5deca0ff3d870fbd3f33341fded
rev: 1a451eff05ce754b25428acc5d542e44050545b0
hooks:
# update the NOT INSTALLABLE ADDONS section above
- id: oca-update-pre-commit-excluded-addons
@ -60,7 +60,7 @@ repos:
- --convert-fragments-to-markdown
- id: oca-gen-external-dependencies
- repo: https://github.com/OCA/odoo-pre-commit-hooks
rev: v0.0.33
rev: v0.1.1
hooks:
- id: oca-checks-odoo-module
- id: oca-checks-po
@ -97,7 +97,7 @@ repos:
- "eslint-plugin-jsdoc@50.3.1"
- "globals@16.0.0"
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- id: trailing-whitespace
# exclude autogenerated files
@ -119,13 +119,13 @@ repos:
- id: mixed-line-ending
args: ["--fix=lf"]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.8
rev: v0.9.7
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
- repo: https://github.com/OCA/pylint-odoo
rev: v9.1.3
rev: v9.3.2
hooks:
- id: pylint_odoo
name: pylint with optional checks

View File

@ -6,10 +6,7 @@
{
"name": "Custom shortcut icon",
"version": "18.0.1.0.0",
"author": "Therp BV, "
"Tecnativa, "
"OERP Canada,"
"Odoo Community Association (OCA)",
"author": "Therp BV, Tecnativa, OERP Canada,Odoo Community Association (OCA)",
"license": "AGPL-3",
"category": "Website",
"summary": "Allows to set a custom shortcut icon (aka favicon)",

View File

@ -0,0 +1,101 @@
===========
Web Iconify
===========
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:6eadd39cc54620c27d3acb20fb2d6a05543446e8a7e66fa53253a519afd19096
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github
:target: https://github.com/OCA/web/tree/18.0/web_iconify
:alt: OCA/web
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/web-18-0/web-18-0-web_iconify
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/web&target_branch=18.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
This module integrates the Iconify Icon Web Component into Odoo,
allowing developers to easily use a wide variety of icons in their
modules. It uses Iconify's on-demand loading feature.
**Table of contents**
.. contents::
:local:
Usage
=====
To use icons in your Odoo views, use the following syntax:
.. code:: html
<iconify-icon icon="prefix:name"></iconify-icon>
Replace prefix:name with the desired icon name. For example, for
Material Design Icons, you would use mdi:home. You can find a list of
available icons and icon sets on the Iconify website:
https://iconify.design/
Changelog
=========
18.0.1.0.0 (2025-02-23)
-----------------------
- Initial version.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/web/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/web/issues/new?body=module:%20web_iconify%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
-------
* jaco.tech
Contributors
------------
- Jaco Waes <jaco@jaco.tech>
Maintainers
-----------
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/web <https://github.com/OCA/web/tree/18.0/web_iconify>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

View File

@ -0,0 +1,23 @@
# Copyright 2025 jaco.tech
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "Web Iconify",
"version": "18.0.1.0.0",
"category": "Web",
"summary": "Provides core Iconify integration for Odoo. This module "
"integrates the Iconify SVG framework into Odoo, allowing developers "
"to easily use a wide variety of icons in their modules.",
"author": "jaco.tech, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/web",
"depends": ["web"],
"license": "AGPL-3",
"assets": {
"web.assets_backend": [
"/web_iconify/static/lib/iconify/iconify-icon.js",
"/web_iconify/static/src/css/web_iconify.css",
],
},
"installable": True,
"auto_install": False,
}

View File

@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

View File

@ -0,0 +1 @@
- Jaco Waes \<jaco@jaco.tech\>

View File

@ -0,0 +1,3 @@
This module integrates the Iconify Icon Web Component into Odoo,
allowing developers to easily use a wide variety of icons in their
modules. It uses Iconify's on-demand loading feature.

View File

@ -0,0 +1,3 @@
## 18.0.1.0.0 (2025-02-23)
- Initial version.

View File

@ -0,0 +1,11 @@
To use icons in your Odoo views, use the following syntax:
``` html
<iconify-icon icon="prefix:name"></iconify-icon>
```
Replace prefix:name with the desired icon name. For example, for
Material Design Icons, you would use mdi:home. You can find a list of
available icons and icon sets on the Iconify website:
<https://iconify.design/>

View File

@ -0,0 +1,450 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Web Iconify</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="web-iconify">
<h1 class="title">Web Iconify</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:6eadd39cc54620c27d3acb20fb2d6a05543446e8a7e66fa53253a519afd19096
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/web/tree/18.0/web_iconify"><img alt="OCA/web" src="https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/web-18-0/web-18-0-web_iconify"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/web&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module integrates the Iconify Icon Web Component into Odoo,
allowing developers to easily use a wide variety of icons in their
modules. It uses Iconifys on-demand loading feature.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
<li><a class="reference internal" href="#changelog" id="toc-entry-2">Changelog</a><ul>
<li><a class="reference internal" href="#section-1" id="toc-entry-3">18.0.1.0.0 (2025-02-23)</a></li>
</ul>
</li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-4">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-5">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-6">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-7">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-8">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-1">Usage</a></h1>
<p>To use icons in your Odoo views, use the following syntax:</p>
<pre class="code html literal-block">
<span class="p">&lt;</span><span class="nt">iconify-icon</span> <span class="na">icon</span><span class="o">=</span><span class="s">&quot;prefix:name&quot;</span><span class="p">&gt;&lt;/</span><span class="nt">iconify-icon</span><span class="p">&gt;</span>
</pre>
<p>Replace prefix:name with the desired icon name. For example, for
Material Design Icons, you would use mdi:home. You can find a list of
available icons and icon sets on the Iconify website:</p>
<p><a class="reference external" href="https://iconify.design/">https://iconify.design/</a></p>
</div>
<div class="section" id="changelog">
<h1><a class="toc-backref" href="#toc-entry-2">Changelog</a></h1>
<div class="section" id="section-1">
<h2><a class="toc-backref" href="#toc-entry-3">18.0.1.0.0 (2025-02-23)</a></h2>
<ul class="simple">
<li>Initial version.</li>
</ul>
</div>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-4">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 to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/web/issues/new?body=module:%20web_iconify%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<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="#toc-entry-5">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-6">Authors</a></h2>
<ul class="simple">
<li>jaco.tech</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-7">Contributors</a></h2>
<ul class="simple">
<li>Jaco Waes &lt;<a class="reference external" href="mailto:jaco&#64;jaco.tech">jaco&#64;jaco.tech</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-8">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
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/web/tree/18.0/web_iconify">OCA/web</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,5 @@
iconify-icon {
display: inline-block;
width: 1em;
height: 1em;
}

View File

@ -0,0 +1,120 @@
=================
Web Iconify Proxy
=================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:540c039653c052dd03103f8763a28c639f23b6e3a74191b78a2dd984352b5127
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github
:target: https://github.com/OCA/web/tree/18.0/web_iconify_proxy
:alt: OCA/web
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/web-18-0/web-18-0-web_iconify_proxy
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/web&target_branch=18.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
This module acts as a proxy for the Iconify API, allowing Odoo to fetch
icons through the Odoo server rather than directly from the Iconify API.
This improves performance by caching the icons locally using Odoo's
ir.attachment model.
**Table of contents**
.. contents::
:local:
Usage
=====
This module works in conjunction with web_iconify. Once installed, icons
will be served through the proxy and cached locally.
SVG Icon Parameters
-------------------
You can customize SVG icons by adding query parameters to the URL. The
format is:
``/web_iconify_proxy/<prefix>/<icon>.svg?param1=value1&param2=value2...``
Available parameters:
- **color**: Icon color (e.g., ``color=red``, ``color=%23ff0000``).
- **width**: Icon width (e.g., ``width=50``, ``width=50px``).
- **height**: Icon height (e.g., ``height=50``, ``height=50px``). If
only one dimension is specified, the other will be calculated
automatically to maintain aspect ratio.
- **flip**: Flip the icon. Possible values: ``horizontal``,
``vertical``, or both (e.g., ``flip=horizontal``, ``flip=vertical``,
``flip=horizontal,vertical``).
- **rotate**: Rotate the icon by 90, 180, or 270 degrees (e.g.,
``rotate=90``, ``rotate=180``).
- **box**: Set to ``true`` to add an empty rectangle to the SVG that
matches the viewBox (e.g., ``box=true``).
Example:
``/web_iconify_proxy/mdi/home.svg?color=blue&width=64&flip=horizontal``
Changelog
=========
18.0.1.0.0 (2025-02-23)
-----------------------
- Initial version.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/web/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/web/issues/new?body=module:%20web_iconify_proxy%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
-------
* jaco.tech
Contributors
------------
- Jaco Waes <jaco@jaco.tech>
Maintainers
-----------
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/web <https://github.com/OCA/web/tree/18.0/web_iconify_proxy>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

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

View File

@ -0,0 +1,24 @@
# Copyright 2025 jaco.tech
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "Web Iconify Proxy",
"summary": "Proxies requests to the Iconify API, providing SVG icons, CSS,"
" and JSON data. It also implements caching using Odoo's "
"ir.attachment model.",
"version": "18.0.1.0.0",
"category": "Website",
"website": "https://github.com/OCA/web",
"author": "jaco.tech, Odoo Community Association (OCA)",
"license": "LGPL-3",
"depends": ["web"],
"data": [],
"assets": {
"web.assets_backend": [
"web_iconify_proxy/static/src/js/iconify_api_provider.js",
],
},
"installable": True,
"auto_install": False,
"tests": ["tests/test_main.py"],
}

View File

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

View File

@ -0,0 +1,261 @@
import ast
import base64
import datetime
import logging
import re
import requests
from odoo import http
from odoo.http import request
_logger = logging.getLogger(__name__)
class IconifyProxyController(http.Controller):
"""Controller for proxying Iconify requests."""
def _fetch_iconify_data(
self,
upstream_url,
content_type,
prefix,
icons=None,
icon=None,
normalized_params_string="",
):
"""Fetches data from the Iconify API or the local cache.
Args:
upstream_url (str): The URL of the Iconify API endpoint.
content_type (str): The expected content type of the response.
prefix (str): The icon prefix.
icons (str, optional): Comma-separated list of icons (for CSS and JSON).
icon (str, optional): The icon name (for SVG).
normalized_params_string (str, optional): Normalized parameters string.
Returns:
Response: The HTTP response.
"""
# Validate prefix
prefix = prefix.lower()
# Validate prefix
if not re.match(r"^[a-z0-9-]+$", prefix):
raise request.not_found()
# Validate icon (if provided)
if icon:
icon = icon.lower()
if not re.match(r"^[a-z0-9:-]+$", icon):
raise request.not_found()
# Validate icons (if provided)
if icons:
icon_list = [i.lower() for i in icons.split(",")]
for single_icon in icon_list:
if not re.match(r"^[a-z0-9:-]+$", single_icon):
raise request.not_found()
icons = ",".join(icon_list) # Reconstruct to prevent injection
Attachment = request.env["ir.attachment"].sudo()
if content_type == "image/svg+xml":
name = f"{prefix}-{icon}-{normalized_params_string.lower()}"
res_model = "iconify.svg"
elif content_type == "text/css":
name = f"{prefix}-{icons}-{normalized_params_string.lower()}"
res_model = "iconify.css"
elif content_type == "application/json":
name = f"{prefix}-{icons}-{normalized_params_string.lower()}"
res_model = "iconify.json"
else:
raise request.not_found()
attachment = Attachment.search(
[("res_model", "=", res_model), ("name", "=", name)], limit=1
)
if attachment:
_logger.info(f"Serving from cache: {name}")
data = base64.b64decode(attachment.datas)
headers = [
("Content-Type", content_type),
("Cache-Control", "public, max-age=31536000"),
("X-Cached-At", str(attachment.create_date)),
]
return request.make_response(data, headers)
_logger.info(f"Fetching from API: {upstream_url}")
try:
response = requests.get(upstream_url, timeout=5)
response.raise_for_status() # Raise HTTPError for bad responses
except requests.exceptions.RequestException as e:
_logger.error(f"Request to Iconify API failed: {e}")
raise request.not_found() from e
data = response.content
attachment = Attachment.create(
{
"name": name,
"datas": base64.b64encode(data).decode("utf-8"),
"res_model": res_model,
"res_id": 0,
"type": "binary",
}
)
headers = [
("Content-Type", content_type),
("Cache-Control", "public, max-age=31536000"), # Cache for one year
]
return request.make_response(data, headers)
def _normalize_params_svg(self, params):
"""Normalizes parameters specifically for SVG requests."""
allowed_params = ["color", "width", "height", "flip", "rotate", "box"]
normalized = []
for key in sorted(params.keys()):
if key in allowed_params:
value = params[key]
key = key.lower()
# Basic type validation
if key in ("width", "height", "rotate") and not (
isinstance(value, str) or isinstance(value, int)
):
continue # Skip invalid values
if key == "box":
try:
value = ast.literal_eval(str(value).lower())
if not isinstance(value, bool):
continue
except (ValueError, SyntaxError):
continue
normalized.append(f"{key}={value}")
return ";".join(normalized)
@http.route(
"/web_iconify_proxy/<string:prefix>/<string:icon>.svg",
type="http",
auth="public",
methods=["GET"],
csrf=False,
)
def get_svg(self, prefix, icon, **params):
"""Gets an SVG icon from the Iconify API.
Args:
prefix (str): The icon prefix.
icon (str): The icon name.
Returns:
Response: The HTTP response containing the SVG data.
"""
normalized_params = self._normalize_params_svg(params)
if normalized_params:
query_string = normalized_params.replace(";", "&")
upstream_url = (
f"https://api.iconify.design/{prefix}/{icon}.svg?{query_string}"
)
else:
upstream_url = f"https://api.iconify.design/{prefix}/{icon}.svg"
return self._fetch_iconify_data(
upstream_url,
"image/svg+xml",
prefix,
icon=icon,
normalized_params_string=normalized_params,
)
@http.route(
"/web_iconify_proxy/<string:prefix>.css",
type="http",
auth="public",
methods=["GET"],
csrf=False,
)
def get_css(self, prefix, **params):
"""Gets CSS for a set of icons from the Iconify API.
Args:
prefix (str): The icon prefix.
params (dict): Query parameters, including 'icons'.
Returns:
Response: The HTTP response containing the CSS data.
"""
icons = params.get("icons")
if not icons:
raise request.not_found()
upstream_url = f"https://api.iconify.design/{prefix}.css?icons={icons}"
return self._fetch_iconify_data(upstream_url, "text/css", prefix, icons=icons)
@http.route(
"/web_iconify_proxy/<string:prefix>.json",
type="http",
auth="public",
methods=["GET"],
csrf=False,
)
def get_json(self, prefix, **params):
"""Gets JSON data for a set of icons from the Iconify API.
Args:
prefix (str): The icon prefix.
params (dict): Query parameters, including 'icons'.
Returns:
Response: The HTTP response containing the JSON data.
"""
icons = params.get("icons")
if not icons:
raise request.not_found()
upstream_url = f"https://api.iconify.design/{prefix}.json?icons={icons}"
return self._fetch_iconify_data(
upstream_url, "application/json", prefix, icons=icons
)
@http.route(
"/web_iconify_proxy/last-modified",
type="http",
auth="public",
methods=["GET"],
csrf=False,
)
def get_last_modified(self, **params):
"""Gets the last modification timestamp for the cached data.
Args:
params (dict): Query parameters, including 'prefixes'.
Returns:
Response: The HTTP response containing the timestamp.
"""
prefixes = params.get("prefixes")
if not prefixes:
raise request.not_found()
prefixes_list = prefixes.split(",")
Attachment = request.env["ir.attachment"].sudo()
# Search for attachments related to iconify
attachments = Attachment.search(
[
("res_model", "in", ["iconify.svg", "iconify.css", "iconify.json"]),
# Check if name contains any of the prefixes
("name", "like", "|".join(prefixes_list)),
]
)
if not attachments:
raise request.not_found()
# Find the latest create_date
latest_timestamp = max(
attachments.mapped("create_date"), default=datetime.datetime.min
)
headers = [("Content-Type", "application/json"), ("Cache-Control", "no-cache")]
return request.make_response(str(latest_timestamp.timestamp()), headers)

View File

@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

View File

@ -0,0 +1 @@
- Jaco Waes \<jaco@jaco.tech\>

View File

@ -0,0 +1,4 @@
This module acts as a proxy for the Iconify API, allowing Odoo to fetch
icons through the Odoo server rather than directly from the Iconify API.
This improves performance by caching the icons locally using Odoo's
ir.attachment model.

View File

@ -0,0 +1,3 @@
## 18.0.1.0.0 (2025-02-23)
- Initial version.

View File

@ -0,0 +1,21 @@
This module works in conjunction with web_iconify. Once installed, icons
will be served through the proxy and cached locally.
## SVG Icon Parameters
You can customize SVG icons by adding query parameters to the URL. The format is:
`/web_iconify_proxy/<prefix>/<icon>.svg?param1=value1&param2=value2...`
Available parameters:
* **color**: Icon color (e.g., `color=red`, `color=%23ff0000`).
* **width**: Icon width (e.g., `width=50`, `width=50px`).
* **height**: Icon height (e.g., `height=50`, `height=50px`). If only one dimension is specified, the other will be calculated automatically to maintain aspect ratio.
* **flip**: Flip the icon. Possible values: `horizontal`, `vertical`, or both (e.g., `flip=horizontal`, `flip=vertical`, `flip=horizontal,vertical`).
* **rotate**: Rotate the icon by 90, 180, or 270 degrees (e.g., `rotate=90`, `rotate=180`).
* **box**: Set to `true` to add an empty rectangle to the SVG that matches the viewBox (e.g., `box=true`).
Example:
`/web_iconify_proxy/mdi/home.svg?color=blue&width=64&flip=horizontal`

View File

@ -0,0 +1,471 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Web Iconify Proxy</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="web-iconify-proxy">
<h1 class="title">Web Iconify Proxy</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:540c039653c052dd03103f8763a28c639f23b6e3a74191b78a2dd984352b5127
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/licence-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/web/tree/18.0/web_iconify_proxy"><img alt="OCA/web" src="https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/web-18-0/web-18-0-web_iconify_proxy"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/web&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module acts as a proxy for the Iconify API, allowing Odoo to fetch
icons through the Odoo server rather than directly from the Iconify API.
This improves performance by caching the icons locally using Odoos
ir.attachment model.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a><ul>
<li><a class="reference internal" href="#svg-icon-parameters" id="toc-entry-2">SVG Icon Parameters</a></li>
</ul>
</li>
<li><a class="reference internal" href="#changelog" id="toc-entry-3">Changelog</a><ul>
<li><a class="reference internal" href="#section-1" id="toc-entry-4">18.0.1.0.0 (2025-02-23)</a></li>
</ul>
</li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-5">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-6">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-7">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-8">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-9">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-1">Usage</a></h1>
<p>This module works in conjunction with web_iconify. Once installed, icons
will be served through the proxy and cached locally.</p>
<div class="section" id="svg-icon-parameters">
<h2><a class="toc-backref" href="#toc-entry-2">SVG Icon Parameters</a></h2>
<p>You can customize SVG icons by adding query parameters to the URL. The
format is:</p>
<p><tt class="docutils literal"><span class="pre">/web_iconify_proxy/&lt;prefix&gt;/&lt;icon&gt;.svg?param1=value1&amp;param2=value2...</span></tt></p>
<p>Available parameters:</p>
<ul class="simple">
<li><strong>color</strong>: Icon color (e.g., <tt class="docutils literal">color=red</tt>, <tt class="docutils literal"><span class="pre">color=%23ff0000</span></tt>).</li>
<li><strong>width</strong>: Icon width (e.g., <tt class="docutils literal">width=50</tt>, <tt class="docutils literal">width=50px</tt>).</li>
<li><strong>height</strong>: Icon height (e.g., <tt class="docutils literal">height=50</tt>, <tt class="docutils literal">height=50px</tt>). If
only one dimension is specified, the other will be calculated
automatically to maintain aspect ratio.</li>
<li><strong>flip</strong>: Flip the icon. Possible values: <tt class="docutils literal">horizontal</tt>,
<tt class="docutils literal">vertical</tt>, or both (e.g., <tt class="docutils literal">flip=horizontal</tt>, <tt class="docutils literal">flip=vertical</tt>,
<tt class="docutils literal">flip=horizontal,vertical</tt>).</li>
<li><strong>rotate</strong>: Rotate the icon by 90, 180, or 270 degrees (e.g.,
<tt class="docutils literal">rotate=90</tt>, <tt class="docutils literal">rotate=180</tt>).</li>
<li><strong>box</strong>: Set to <tt class="docutils literal">true</tt> to add an empty rectangle to the SVG that
matches the viewBox (e.g., <tt class="docutils literal">box=true</tt>).</li>
</ul>
<p>Example:</p>
<p><tt class="docutils literal"><span class="pre">/web_iconify_proxy/mdi/home.svg?color=blue&amp;width=64&amp;flip=horizontal</span></tt></p>
</div>
</div>
<div class="section" id="changelog">
<h1><a class="toc-backref" href="#toc-entry-3">Changelog</a></h1>
<div class="section" id="section-1">
<h2><a class="toc-backref" href="#toc-entry-4">18.0.1.0.0 (2025-02-23)</a></h2>
<ul class="simple">
<li>Initial version.</li>
</ul>
</div>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-5">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 to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/web/issues/new?body=module:%20web_iconify_proxy%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<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="#toc-entry-6">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-7">Authors</a></h2>
<ul class="simple">
<li>jaco.tech</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-8">Contributors</a></h2>
<ul class="simple">
<li>Jaco Waes &lt;<a class="reference external" href="mailto:jaco&#64;jaco.tech">jaco&#64;jaco.tech</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-9">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
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/web/tree/18.0/web_iconify_proxy">OCA/web</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,8 @@
/** @odoo-module ignore **/
// eslint-disable-next-line no-undef
const IconifyIcon = window.customElements.get("iconify-icon");
IconifyIcon.addAPIProvider("", {
resources: ["/web_iconify_proxy"],
});

View File

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

View File

@ -0,0 +1,272 @@
import base64
from unittest.mock import patch
import requests
from odoo.tests.common import HttpCase, tagged
@tagged("post_install", "-at_install")
class TestIconifyProxyController(HttpCase):
@patch("odoo.addons.web_iconify_proxy.controllers.main.requests.get")
def test_get_svg_success(self, mock_get):
mock_response = requests.Response()
mock_response.status_code = 200
mock_response._content = b"<svg>dummy content</svg>"
mock_response.headers["Content-Type"] = "image/svg+xml"
mock_get.return_value = mock_response
response = self.url_open("/web_iconify_proxy/mdi/home.svg")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], "image/svg+xml")
# Add basic check for SVG content (can be improved)
self.assertTrue(b"<svg" in response.content)
@patch("odoo.addons.web_iconify_proxy.controllers.main.requests.get")
def test_get_css_success(self, mock_get):
mock_response = requests.Response()
mock_response.status_code = 200
mock_response._content = b".iconify { color: red; }"
mock_response.headers["Content-Type"] = "text/css"
mock_get.return_value = mock_response
response = self.url_open("/web_iconify_proxy/mdi.css?icons=home,account")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], "text/css")
# Add basic check for CSS content
self.assertTrue(b".iconify" in response.content)
@patch("odoo.addons.web_iconify_proxy.controllers.main.requests.get")
def test_get_json_success(self, mock_get):
mock_response = requests.Response()
mock_response.status_code = 200
mock_response._content = b'{"prefix": "mdi", "icons": {"home": {}}}'
mock_response.headers["Content-Type"] = "application/json"
mock_get.return_value = mock_response
response = self.url_open("/web_iconify_proxy/mdi.json?icons=home,account")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], "application/json")
# Add basic check for JSON content (can be improved)
self.assertTrue(b'{"prefix":' in response.content)
def test_get_svg_invalid_prefix(self):
response = self.url_open("/web_iconify_proxy/invalid!prefix/home.svg")
self.assertEqual(response.status_code, 404)
def test_get_svg_invalid_icon(self):
response = self.url_open("/web_iconify_proxy/mdi/invalid!icon.svg")
self.assertEqual(response.status_code, 404)
def test_get_css_invalid_icons(self):
response = self.url_open("/web_iconify_proxy/mdi.css?icons=home,invalid!icon")
self.assertEqual(response.status_code, 404)
def test_get_json_invalid_icons(self):
response = self.url_open("/web_iconify_proxy/mdi.json?icons=home,invalid!icon")
self.assertEqual(response.status_code, 404)
def test_get_last_modified(self):
# Create a dummy attachment
attachment = (
self.env["ir.attachment"]
.sudo()
.create(
{
"name": "mdi-test",
"datas": base64.b64encode(b"dummy data").decode("utf-8"),
"res_model": "iconify.svg",
"res_id": 0,
"type": "binary",
}
)
)
response = self.url_open("/web_iconify_proxy/last-modified?prefixes=mdi")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], "application/json")
# Check if the response is a valid timestamp
try:
float(response.content)
except ValueError:
self.fail("last-modified did not return a valid timestamp")
# Clean up the attachment
attachment.unlink()
def test_get_last_modified_no_prefix(self):
response = self.url_open("/web_iconify_proxy/last-modified")
self.assertEqual(response.status_code, 404)
def test_get_css_no_icons(self):
response = self.url_open("/web_iconify_proxy/mdi.css")
self.assertEqual(response.status_code, 404)
def test_get_json_no_icons(self):
response = self.url_open("/web_iconify_proxy/mdi.json")
self.assertEqual(response.status_code, 404)
@patch("odoo.addons.web_iconify_proxy.controllers.main.requests.get")
def test_caching(self, mock_get):
# Mock the requests.get method to return a dummy response
mock_response = requests.Response()
mock_response.status_code = 200
mock_response._content = b"<svg>dummy content</svg>"
mock_response.headers["Content-Type"] = "image/svg+xml"
mock_get.return_value = mock_response
# First request, should fetch from API
response1 = self.url_open("/web_iconify_proxy/mdi/home.svg")
self.assertEqual(response1.status_code, 200)
self.assertFalse("X-Cached-At" in response1.headers) # Check that is NOT cached
# Second request, should be served from cache
response2 = self.url_open("/web_iconify_proxy/mdi/home.svg")
self.assertEqual(response2.status_code, 200)
self.assertTrue("X-Cached-At" in response2.headers) # Check that IS cached
# Check that content is the same
self.assertEqual(response1.content, response2.content)
@patch("odoo.addons.web_iconify_proxy.controllers.main.requests.get")
def test_caching_with_params(self, mock_get):
"""Test caching with different parameters."""
mock_response = requests.Response()
mock_response.status_code = 200
mock_response._content = b"<svg>dummy content</svg>"
mock_response.headers["Content-Type"] = "image/svg+xml"
mock_get.return_value = mock_response
# First request with specific parameters
response1 = self.url_open(
"/web_iconify_proxy/mdi/home.svg?color=red&width=50&flip=horizontal"
)
self.assertEqual(response1.status_code, 200)
self.assertFalse("X-Cached-At" in response1.headers)
# Second request with the same parameters, should be cached
response2 = self.url_open(
"/web_iconify_proxy/mdi/home.svg?color=red&width=50&flip=horizontal"
)
self.assertEqual(response2.status_code, 200)
self.assertTrue("X-Cached-At" in response2.headers)
# Third request with different parameters, should not be cached
response3 = self.url_open(
"/web_iconify_proxy/mdi/home.svg?color=blue&width=100"
)
self.assertEqual(response3.status_code, 200)
self.assertFalse("X-Cached-At" in response3.headers)
@patch("odoo.addons.web_iconify_proxy.controllers.main.requests.get")
def test_caching_parameter_order(self, mock_get):
"""Test that parameter order doesn't affect caching."""
mock_response = requests.Response()
mock_response.status_code = 200
mock_response._content = b"<svg>dummy content</svg>"
mock_response.headers["Content-Type"] = "image/svg+xml"
mock_get.return_value = mock_response
# First request with specific parameter order
response1 = self.url_open(
"/web_iconify_proxy/mdi/home.svg?color=red&width=50&flip=horizontal"
)
self.assertEqual(response1.status_code, 200)
self.assertFalse("X-Cached-At" in response1.headers)
# Second request with different parameter order, should be cached
response2 = self.url_open(
"/web_iconify_proxy/mdi/home.svg?flip=horizontal&width=50&color=red"
)
self.assertEqual(response2.status_code, 200)
self.assertTrue("X-Cached-At" in response2.headers)
# Check that content is the same
self.assertEqual(response1.content, response2.content)
@patch("odoo.addons.web_iconify_proxy.controllers.main.requests.get")
def test_caching_boolean_values(self, mock_get):
"""Test that boolean values are case-insensitive for caching."""
mock_response = requests.Response()
mock_response.status_code = 200
mock_response._content = b"<svg>dummy content</svg>"
mock_response.headers["Content-Type"] = "image/svg+xml"
mock_get.return_value = mock_response
# First request with "true"
response1 = self.url_open("/web_iconify_proxy/mdi/home.svg?box=true")
self.assertEqual(response1.status_code, 200)
self.assertFalse("X-Cached-At" in response1.headers)
# Second request with "True", should be cached
response2 = self.url_open("/web_iconify_proxy/mdi/home.svg?box=True")
self.assertEqual(response2.status_code, 200)
self.assertTrue("X-Cached-At" in response2.headers)
# Third request with "TRUE", should be cached
response3 = self.url_open("/web_iconify_proxy/mdi/home.svg?box=TRUE")
self.assertEqual(response3.status_code, 200)
self.assertTrue("X-Cached-At" in response3.headers)
# Check that content is the same for all
self.assertEqual(response1.content, response2.content)
self.assertEqual(response1.content, response3.content)
@patch("odoo.addons.web_iconify_proxy.controllers.main.requests.get")
def test_get_svg_with_parameters(self, mock_get):
"""Test the get_svg route with various valid parameters."""
mock_response = requests.Response()
mock_response.status_code = 200
mock_response._content = b"<svg>dummy content</svg>"
mock_response.headers["Content-Type"] = "image/svg+xml"
mock_get.return_value = mock_response
test_cases = [
"/web_iconify_proxy/mdi/home.svg?color=red",
"/web_iconify_proxy/mdi/home.svg?width=50",
"/web_iconify_proxy/mdi/home.svg?height=50",
"/web_iconify_proxy/mdi/home.svg?flip=horizontal",
"/web_iconify_proxy/mdi/home.svg?flip=vertical",
"/web_iconify_proxy/mdi/home.svg?flip=horizontal,vertical",
"/web_iconify_proxy/mdi/home.svg?rotate=90",
"/web_iconify_proxy/mdi/home.svg?rotate=180",
"/web_iconify_proxy/mdi/home.svg?rotate=270",
"/web_iconify_proxy/mdi/home.svg?box=true",
"/web_iconify_proxy/mdi/home.svg?color=blue&width=64&flip=horizontal&rotate=180",
]
for url in test_cases:
with self.subTest(url=url):
response = self.url_open(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], "image/svg+xml")
@patch("odoo.addons.web_iconify_proxy.controllers.main.requests.get")
def test_get_svg_with_invalid_parameters(self, mock_get):
"""Test the get_svg route with invalid parameters."""
mock_response = requests.Response()
mock_response.status_code = 200
mock_response._content = b"<svg>dummy content</svg>"
mock_response.headers["Content-Type"] = "image/svg+xml"
mock_get.return_value = mock_response
test_cases = [
"/web_iconify_proxy/mdi/home.svg?width=invalid", # Invalid width
"/web_iconify_proxy/mdi/home.svg?height=invalid", # Invalid height
"/web_iconify_proxy/mdi/home.svg?rotate=invalid", # Invalid rotate
"/web_iconify_proxy/mdi/home.svg?box=invalid", # Invalid box
"/web_iconify_proxy/mdi/home.svg?unknown=param", # Unknown parameter
]
for url in test_cases:
with self.subTest(url=url):
response = self.url_open(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], "image/svg+xml")
@patch("odoo.addons.web_iconify_proxy.controllers.main.requests.get")
def test_api_error(self, mock_get):
# Mock requests.get to simulate an API error
mock_get.side_effect = requests.exceptions.RequestException("Simulated Error")
with self.assertLogs(level="ERROR") as log:
response = self.url_open("/web_iconify_proxy/mdi/home.svg")
self.assertEqual(response.status_code, 404) # Expect 404 Not Found
self.assertIn("Simulated Error", log.output[0])

View File

@ -4,7 +4,7 @@
{
"name": "Web No Bubble",
"version": "18.0.1.0.0",
"author": "Savoir-faire Linux, " "Odoo Community Association (OCA)",
"author": "Savoir-faire Linux, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/web",
"license": "AGPL-3",
"category": "Web",

View File

@ -34,11 +34,11 @@ This technical module allows you to send instant notification messages
from the server to the user in live. Two kinds of notification are
supported.
- Success: Displayed in a success theme color flying popup div
- Danger: Displayed in a danger theme color flying popup div
- Warning: Displayed in a warning theme color flying popup div
- Information: Displayed in a info theme color flying popup div
- Default: Displayed in a default theme color flying popup div
- Success: Displayed in a success theme color flying popup div
- Danger: Displayed in a danger theme color flying popup div
- Warning: Displayed in a warning theme color flying popup div
- Information: Displayed in a info theme color flying popup div
- Default: Displayed in a default theme color flying popup div
**Table of contents**
@ -87,8 +87,8 @@ or
The notifications can bring interactivity with some buttons.
- One allowing to refresh the active view
- Another allowing to send a window / client action
- One allowing to refresh the active view
- Another allowing to send a window / client action
The reload button is activated when sending the notification with:
@ -142,17 +142,17 @@ Authors
Contributors
------------
- Laurent Mignon <laurent.mignon@acsone.eu>
- Serpent Consulting Services Pvt. Ltd.<jay.vora@serpentcs.com>
- Aitor Bouzas <aitor.bouzas@adaptivecity.com>
- Shepilov Vladislav <shepilov.v@protonmail.com>
- Kevin Khao <kevin.khao@akretion.com>
- `Tecnativa <https://www.tecnativa.com>`__:
- Laurent Mignon <laurent.mignon@acsone.eu>
- Serpent Consulting Services Pvt. Ltd.<jay.vora@serpentcs.com>
- Aitor Bouzas <aitor.bouzas@adaptivecity.com>
- Shepilov Vladislav <shepilov.v@protonmail.com>
- Kevin Khao <kevin.khao@akretion.com>
- `Tecnativa <https://www.tecnativa.com>`__:
- David Vidal
- David Vidal
- Nikul Chaudhary <nchaudhary@opensourceintegrators.com>
- Tris Doan <tridm@trobz.com>
- Nikul Chaudhary <nchaudhary@opensourceintegrators.com>
- Tris Doan <tridm@trobz.com>
Other credits
-------------

View File

@ -8,7 +8,7 @@
Send notification messages to user""",
"version": "18.0.1.0.1",
"license": "AGPL-3",
"author": "ACSONE SA/NV," "AdaptiveCity," "Odoo Community Association (OCA)",
"author": "ACSONE SA/NV,AdaptiveCity,Odoo Community Association (OCA)",
"development_status": "Production/Stable",
"website": "https://github.com/OCA/web",
"depends": ["web", "bus", "base", "mail"],