initial release for web_iconify and web_iconify_proxy

pull/3103/head
jwaes 2025-02-23 09:01:57 +00:00
parent 6c72295e51
commit bf0b5fa835
27 changed files with 3816 additions and 5 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
@ -96,7 +96,7 @@ repos:
- "eslint@9.12.0"
- "eslint-plugin-jsdoc@50.3.1"
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- id: trailing-whitespace
# exclude autogenerated files
@ -118,13 +118,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

@ -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,94 @@
=================
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-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_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. No specific usage
instructions are required.
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,23 @@
# 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": "AGPL-3",
"depends": ["web"],
"data": [],
"assets": {
"web.assets_backend": [
"web_iconify_proxy/static/src/js/iconify_api_provider.js",
],
},
"installable": True,
"auto_install": False,
}

View File

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

View File

@ -0,0 +1,213 @@
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
):
"""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).
Returns:
Response: The HTTP response.
"""
# Validate prefix
if not re.match(r"^[a-z0-9-]+$", prefix):
return request.not_found()
# Validate icon (if provided)
if icon and not re.match(r"^[a-z0-9:-]+$", icon):
return request.not_found()
# Validate icons (if provided)
if icons:
icon_list = icons.split(",")
for single_icon in icon_list:
if not re.match(r"^[a-z0-9:-]+$", single_icon):
return 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}"
res_model = "iconify.svg"
elif content_type == "text/css":
name = f"{prefix}-{icons}"
res_model = "iconify.css"
elif content_type == "application/json":
name = f"{prefix}-{icons}"
res_model = "iconify.json"
else:
return 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}")
return request.not_found()
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)
@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.
"""
upstream_url = f"https://api.iconify.design/{prefix}/{icon}.svg"
return self._fetch_iconify_data(
upstream_url, "image/svg+xml", prefix, icon=icon
)
@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:
return 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:
return 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:
return 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:
return 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,3 @@
This module works in conjunction with web_iconify. Once installed, icons
will be served through the proxy and cached locally. No specific usage
instructions are required.

View File

@ -0,0 +1,446 @@
<!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/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_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></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>This module works in conjunction with web_iconify. Once installed, icons
will be served through the proxy and cached locally. No specific usage
instructions are required.</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_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-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_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,97 @@
from unittest.mock import patch
import requests
from odoo.tests import tagged
from odoo.tests.common import HttpCase
@tagged("post_install", "-at_install")
class TestIconifyProxyController(HttpCase):
def test_get_svg_success(self):
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)
def test_get_css_success(self):
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)
def test_get_json_success(self):
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):
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")
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_api_error(self, mock_get):
# Mock requests.get to simulate an API error
mock_get.side_effect = requests.exceptions.RequestException("Simulated Error")
response = self.url_open("/web_iconify_proxy/mdi/home.svg")
self.assertEqual(response.status_code, 404)