mirror of
https://github.com/bringout/oca-ocb-web.git
synced 2026-04-18 00:12:07 +02:00
Initial commit: Web packages
This commit is contained in:
commit
cd458d4b85
791 changed files with 410049 additions and 0 deletions
10
README.md
Normal file
10
README.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Web
|
||||
|
||||
This repository contains OCA OCB packages for web.
|
||||
|
||||
## Packages Included
|
||||
|
||||
- odoo-bringout-oca-ocb-web_editor
|
||||
- odoo-bringout-oca-ocb-web_kanban_gauge
|
||||
- odoo-bringout-oca-ocb-web_tour
|
||||
- odoo-bringout-oca-ocb-web_unsplash
|
||||
49
odoo-bringout-oca-ocb-web_editor/README.md
Normal file
49
odoo-bringout-oca-ocb-web_editor/README.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Web Editor
|
||||
|
||||
|
||||
Odoo Web Editor widget.
|
||||
==========================
|
||||
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-ocb-web_editor
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
This addon depends on:
|
||||
- bus
|
||||
- web
|
||||
|
||||
## Manifest Information
|
||||
|
||||
- **Name**: Web Editor
|
||||
- **Version**: N/A
|
||||
- **Category**: Hidden
|
||||
- **License**: LGPL-3
|
||||
- **Installable**: False
|
||||
|
||||
## Source
|
||||
|
||||
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `web_editor`.
|
||||
|
||||
## License
|
||||
|
||||
This package maintains the original LGPL-3 license from the upstream Odoo project.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Overview: doc/OVERVIEW.md
|
||||
- Architecture: doc/ARCHITECTURE.md
|
||||
- Models: doc/MODELS.md
|
||||
- Controllers: doc/CONTROLLERS.md
|
||||
- Wizards: doc/WIZARDS.md
|
||||
- Install: doc/INSTALL.md
|
||||
- Usage: doc/USAGE.md
|
||||
- Configuration: doc/CONFIGURATION.md
|
||||
- Dependencies: doc/DEPENDENCIES.md
|
||||
- Troubleshooting: doc/TROUBLESHOOTING.md
|
||||
- FAQ: doc/FAQ.md
|
||||
32
odoo-bringout-oca-ocb-web_editor/doc/ARCHITECTURE.md
Normal file
32
odoo-bringout-oca-ocb-web_editor/doc/ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
U[Users] -->|HTTP| V[Views and QWeb Templates]
|
||||
V --> C[Controllers]
|
||||
V --> W[Wizards – Transient Models]
|
||||
C --> M[Models and ORM]
|
||||
W --> M
|
||||
M --> R[Reports]
|
||||
DX[Data XML] --> M
|
||||
S[Security – ACLs and Groups] -. enforces .-> M
|
||||
|
||||
subgraph Web_editor Module - web_editor
|
||||
direction LR
|
||||
M:::layer
|
||||
W:::layer
|
||||
C:::layer
|
||||
V:::layer
|
||||
R:::layer
|
||||
S:::layer
|
||||
DX:::layer
|
||||
end
|
||||
|
||||
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
|
||||
```
|
||||
|
||||
Notes
|
||||
- Views include tree/form/kanban templates and report templates.
|
||||
- Controllers provide website/portal routes when present.
|
||||
- Wizards are UI flows implemented with `models.TransientModel`.
|
||||
- Data XML loads data/demo records; Security defines groups and access.
|
||||
3
odoo-bringout-oca-ocb-web_editor/doc/CONFIGURATION.md
Normal file
3
odoo-bringout-oca-ocb-web_editor/doc/CONFIGURATION.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Configuration
|
||||
|
||||
Refer to Odoo settings for web_editor. Configure related models, access rights, and options as needed.
|
||||
17
odoo-bringout-oca-ocb-web_editor/doc/CONTROLLERS.md
Normal file
17
odoo-bringout-oca-ocb-web_editor/doc/CONTROLLERS.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Controllers
|
||||
|
||||
HTTP routes provided by this module.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as User/Client
|
||||
participant C as Module Controllers
|
||||
participant O as ORM/Views
|
||||
|
||||
U->>C: HTTP GET/POST (routes)
|
||||
C->>O: ORM operations, render templates
|
||||
O-->>U: HTML/JSON/PDF
|
||||
```
|
||||
|
||||
Notes
|
||||
- See files in controllers/ for route definitions.
|
||||
6
odoo-bringout-oca-ocb-web_editor/doc/DEPENDENCIES.md
Normal file
6
odoo-bringout-oca-ocb-web_editor/doc/DEPENDENCIES.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Dependencies
|
||||
|
||||
This addon depends on:
|
||||
|
||||
- [bus](../../odoo-bringout-oca-ocb-bus)
|
||||
- [web](../../odoo-bringout-oca-ocb-web)
|
||||
4
odoo-bringout-oca-ocb-web_editor/doc/FAQ.md
Normal file
4
odoo-bringout-oca-ocb-web_editor/doc/FAQ.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# FAQ
|
||||
|
||||
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
|
||||
- Q: How to enable? A: Start server with --addon web_editor or install in UI.
|
||||
7
odoo-bringout-oca-ocb-web_editor/doc/INSTALL.md
Normal file
7
odoo-bringout-oca-ocb-web_editor/doc/INSTALL.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Install
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-ocb-web_editor"
|
||||
# or
|
||||
uv pip install odoo-bringout-oca-ocb-web_editor"
|
||||
```
|
||||
50
odoo-bringout-oca-ocb-web_editor/doc/MODELS.md
Normal file
50
odoo-bringout-oca-ocb-web_editor/doc/MODELS.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Models
|
||||
|
||||
Detected core models and extensions in web_editor.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class ir_qweb_field
|
||||
class ir_qweb_field_contact
|
||||
class ir_qweb_field_date
|
||||
class ir_qweb_field_datetime
|
||||
class ir_qweb_field_duration
|
||||
class ir_qweb_field_float
|
||||
class ir_qweb_field_html
|
||||
class ir_qweb_field_image
|
||||
class ir_qweb_field_integer
|
||||
class ir_qweb_field_many2one
|
||||
class ir_qweb_field_monetary
|
||||
class ir_qweb_field_qweb
|
||||
class ir_qweb_field_relative
|
||||
class ir_qweb_field_selection
|
||||
class ir_qweb_field_text
|
||||
class web_editor_assets
|
||||
class web_editor_converter_test
|
||||
class web_editor_converter_test_sub
|
||||
class base
|
||||
class ir_attachment
|
||||
class ir_http
|
||||
class ir_qweb
|
||||
class ir_qweb_field
|
||||
class ir_qweb_field_contact
|
||||
class ir_qweb_field_date
|
||||
class ir_qweb_field_datetime
|
||||
class ir_qweb_field_duration
|
||||
class ir_qweb_field_float
|
||||
class ir_qweb_field_html
|
||||
class ir_qweb_field_image
|
||||
class ir_qweb_field_integer
|
||||
class ir_qweb_field_many2one
|
||||
class ir_qweb_field_monetary
|
||||
class ir_qweb_field_qweb
|
||||
class ir_qweb_field_relative
|
||||
class ir_qweb_field_selection
|
||||
class ir_qweb_field_text
|
||||
class ir_ui_view
|
||||
class ir_websocket
|
||||
```
|
||||
|
||||
Notes
|
||||
- Classes show model technical names; fields omitted for brevity.
|
||||
- Items listed under _inherit are extensions of existing models.
|
||||
6
odoo-bringout-oca-ocb-web_editor/doc/OVERVIEW.md
Normal file
6
odoo-bringout-oca-ocb-web_editor/doc/OVERVIEW.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Overview
|
||||
|
||||
Packaged Odoo addon: web_editor. Provides features documented in upstream Odoo 16 under this addon.
|
||||
|
||||
- Source: OCA/OCB 16.0, addon web_editor
|
||||
- License: LGPL-3
|
||||
3
odoo-bringout-oca-ocb-web_editor/doc/REPORTS.md
Normal file
3
odoo-bringout-oca-ocb-web_editor/doc/REPORTS.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Reports
|
||||
|
||||
This module does not define custom reports.
|
||||
34
odoo-bringout-oca-ocb-web_editor/doc/SECURITY.md
Normal file
34
odoo-bringout-oca-ocb-web_editor/doc/SECURITY.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Security
|
||||
|
||||
Access control and security definitions in web_editor.
|
||||
|
||||
## Access Control Lists (ACLs)
|
||||
|
||||
Model access permissions defined in:
|
||||
- **[ir.model.access.csv](../web_editor/security/ir.model.access.csv)**
|
||||
- 2 model access rules
|
||||
|
||||
## Record Rules
|
||||
|
||||
Row-level security rules defined in:
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Security Layers"
|
||||
A[Users] --> B[Groups]
|
||||
B --> C[Access Control Lists]
|
||||
C --> D[Models]
|
||||
B --> E[Record Rules]
|
||||
E --> F[Individual Records]
|
||||
end
|
||||
```
|
||||
|
||||
Security files overview:
|
||||
- **[ir.model.access.csv](../web_editor/security/ir.model.access.csv)**
|
||||
- Model access permissions (CRUD rights)
|
||||
|
||||
Notes
|
||||
- Access Control Lists define which groups can access which models
|
||||
- Record Rules provide row-level security (filter records by user/group)
|
||||
- Security groups organize users and define permission sets
|
||||
- All security is enforced at the ORM level by Odoo
|
||||
5
odoo-bringout-oca-ocb-web_editor/doc/TROUBLESHOOTING.md
Normal file
5
odoo-bringout-oca-ocb-web_editor/doc/TROUBLESHOOTING.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Troubleshooting
|
||||
|
||||
- Ensure Python and Odoo environment matches repo guidance.
|
||||
- Check database connectivity and logs if startup fails.
|
||||
- Validate that dependent addons listed in DEPENDENCIES.md are installed.
|
||||
7
odoo-bringout-oca-ocb-web_editor/doc/USAGE.md
Normal file
7
odoo-bringout-oca-ocb-web_editor/doc/USAGE.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Usage
|
||||
|
||||
Start Odoo including this addon (from repo root):
|
||||
|
||||
```bash
|
||||
python3 scripts/odoo_web_server.py --db-name mydb --addon web_editor
|
||||
```
|
||||
3
odoo-bringout-oca-ocb-web_editor/doc/WIZARDS.md
Normal file
3
odoo-bringout-oca-ocb-web_editor/doc/WIZARDS.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Wizards
|
||||
|
||||
This module does not include UI wizards.
|
||||
43
odoo-bringout-oca-ocb-web_editor/pyproject.toml
Normal file
43
odoo-bringout-oca-ocb-web_editor/pyproject.toml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[project]
|
||||
name = "odoo-bringout-oca-ocb-web_editor"
|
||||
version = "16.0.0"
|
||||
description = "Web Editor - Odoo addon"
|
||||
authors = [
|
||||
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
|
||||
]
|
||||
dependencies = [
|
||||
"odoo-bringout-oca-ocb-bus>=16.0.0",
|
||||
"odoo-bringout-oca-ocb-web>=16.0.0",
|
||||
"requests>=2.25.1"
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">= 3.11"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Office/Business",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://github.com/bringout/odoo-bringout-oca-ocb-web_editor"
|
||||
repository = "https://github.com/bringout/odoo-bringout-oca-ocb-web_editor"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.metadata]
|
||||
allow-direct-references = true
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["web_editor"]
|
||||
|
||||
[tool.rye]
|
||||
managed = true
|
||||
dev-dependencies = [
|
||||
"pytest>=8.4.1",
|
||||
]
|
||||
5
odoo-bringout-oca-ocb-web_editor/web_editor/__init__.py
Normal file
5
odoo-bringout-oca-ocb-web_editor/web_editor/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
180
odoo-bringout-oca-ocb-web_editor/web_editor/__manifest__.py
Normal file
180
odoo-bringout-oca-ocb-web_editor/web_editor/__manifest__.py
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Web Editor',
|
||||
'category': 'Hidden',
|
||||
'description': """
|
||||
Odoo Web Editor widget.
|
||||
==========================
|
||||
|
||||
""",
|
||||
'depends': ['bus', 'web'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/editor_assets.xml',
|
||||
'views/editor.xml',
|
||||
'views/snippets.xml',
|
||||
],
|
||||
'assets': {
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# MAIN BUNDLES
|
||||
#----------------------------------------------------------------------
|
||||
|
||||
'web_editor.assets_wysiwyg': [
|
||||
# lib
|
||||
'web_editor/static/lib/cropperjs/cropper.css',
|
||||
'web_editor/static/lib/cropperjs/cropper.js',
|
||||
'web_editor/static/lib/jquery-cropper/jquery-cropper.js',
|
||||
'web_editor/static/lib/jQuery.transfo.js',
|
||||
'web/static/lib/nearest/jquery.nearest.js',
|
||||
'web_editor/static/lib/webgl-image-filter/webgl-image-filter.js',
|
||||
'web_editor/static/lib/DOMPurify.js',
|
||||
|
||||
# odoo-editor
|
||||
'web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js',
|
||||
'web_editor/static/src/js/editor/odoo-editor/src/utils/constants.js',
|
||||
'web_editor/static/src/js/editor/odoo-editor/src/utils/sanitize.js',
|
||||
'web_editor/static/src/js/editor/odoo-editor/src/utils/serialize.js',
|
||||
'web_editor/static/src/js/editor/odoo-editor/src/tablepicker/TablePicker.js',
|
||||
'web_editor/static/src/js/editor/odoo-editor/src/powerbox/patienceDiff.js',
|
||||
'web_editor/static/src/js/editor/odoo-editor/src/powerbox/Powerbox.js',
|
||||
'web_editor/static/src/js/editor/odoo-editor/src/commands/align.js',
|
||||
'web_editor/static/src/js/editor/odoo-editor/src/commands/commands.js',
|
||||
'web_editor/static/src/js/editor/odoo-editor/src/commands/deleteBackward.js',
|
||||
'web_editor/static/src/js/editor/odoo-editor/src/commands/deleteForward.js',
|
||||
'web_editor/static/src/js/editor/odoo-editor/src/commands/enter.js',
|
||||
'web_editor/static/src/js/editor/odoo-editor/src/commands/shiftEnter.js',
|
||||
'web_editor/static/src/js/editor/odoo-editor/src/commands/shiftTab.js',
|
||||
'web_editor/static/src/js/editor/odoo-editor/src/commands/tab.js',
|
||||
'web_editor/static/src/js/editor/odoo-editor/src/commands/toggleList.js',
|
||||
|
||||
# utils
|
||||
'web_editor/static/src/js/wysiwyg/linkDialogCommand.js',
|
||||
'web_editor/static/src/js/wysiwyg/PeerToPeer.js',
|
||||
|
||||
# odoo utils
|
||||
('include', 'web._assets_helpers'),
|
||||
|
||||
'web_editor/static/src/scss/bootstrap_overridden.scss',
|
||||
'web/static/src/scss/pre_variables.scss',
|
||||
'web/static/lib/bootstrap/scss/_variables.scss',
|
||||
'web_editor/static/src/js/editor/odoo-editor/src/style.scss',
|
||||
|
||||
# integration
|
||||
'web_editor/static/src/scss/wysiwyg.scss',
|
||||
'web_editor/static/src/scss/wysiwyg_iframe.scss',
|
||||
'web_editor/static/src/scss/wysiwyg_snippets.scss',
|
||||
|
||||
'web_editor/static/src/js/editor/perspective_utils.js',
|
||||
'web_editor/static/src/js/editor/image_processing.js',
|
||||
'web_editor/static/src/js/editor/custom_colors.js',
|
||||
|
||||
# widgets & plugins
|
||||
'web_editor/static/src/js/wysiwyg/widgets/**/*',
|
||||
'web_editor/static/src/js/editor/snippets.editor.js',
|
||||
'web_editor/static/src/js/editor/toolbar.js',
|
||||
'web_editor/static/src/js/editor/snippets.options.js',
|
||||
|
||||
# Launcher
|
||||
'web_editor/static/src/js/wysiwyg/wysiwyg.js',
|
||||
'web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js',
|
||||
|
||||
'web_editor/static/src/xml/editor.xml',
|
||||
'web_editor/static/src/xml/grid_layout.xml',
|
||||
'web_editor/static/src/xml/snippets.xml',
|
||||
'web_editor/static/src/xml/wysiwyg.xml',
|
||||
'web_editor/static/src/xml/wysiwyg_colorpicker.xml',
|
||||
],
|
||||
'web_editor.assets_media_dialog': [
|
||||
'web_editor/static/src/components/**/*',
|
||||
],
|
||||
'web_editor.assets_tests_styles': [
|
||||
('include', 'web._assets_helpers'),
|
||||
'web_editor/static/src/js/editor/odoo-editor/src/base_style.scss',
|
||||
'web_editor/static/src/js/editor/odoo-editor/src/checklist.scss',
|
||||
],
|
||||
'web.assets_common': [
|
||||
'web_editor/static/src/js/editor/odoo-editor/src/base_style.scss',
|
||||
'web_editor/static/lib/vkbeautify/**/*',
|
||||
'web_editor/static/src/js/common/**/*',
|
||||
'web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js',
|
||||
'web_editor/static/src/js/wysiwyg/fonts.js',
|
||||
'web_editor/static/src/xml/ace.xml',
|
||||
],
|
||||
'web.assets_backend': [
|
||||
('include', 'web_editor.assets_media_dialog'),
|
||||
|
||||
'web_editor/static/src/scss/web_editor.common.scss',
|
||||
'web_editor/static/src/scss/web_editor.backend.scss',
|
||||
|
||||
'web_editor/static/src/js/wysiwyg/dialog.js',
|
||||
'web_editor/static/src/js/frontend/loader.js',
|
||||
'web_editor/static/src/js/backend/**/*',
|
||||
'web_editor/static/src/xml/backend.xml',
|
||||
],
|
||||
"web.dark_mode_assets_backend": [
|
||||
'web_editor/static/src/scss/odoo-editor/powerbox.dark.scss',
|
||||
'web_editor/static/src/scss/odoo-editor/tablepicker.dark.scss',
|
||||
'web_editor/static/src/scss/odoo-editor/tableui.dark.scss',
|
||||
'web_editor/static/src/scss/wysiwyg.dark.scss',
|
||||
'web_editor/static/src/scss/web_editor.common.dark.scss',
|
||||
],
|
||||
'web.assets_frontend_minimal': [
|
||||
'web_editor/static/src/js/frontend/loader_loading.js',
|
||||
],
|
||||
'web.assets_frontend': [
|
||||
('include', 'web_editor.assets_media_dialog'),
|
||||
|
||||
'web_editor/static/src/js/editor/odoo-editor/src/base_style.scss',
|
||||
'web_editor/static/lib/vkbeautify/**/*',
|
||||
'web_editor/static/src/js/common/**/*',
|
||||
'web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js',
|
||||
'web_editor/static/src/js/wysiwyg/fonts.js',
|
||||
'web_editor/static/src/xml/ace.xml',
|
||||
|
||||
'web_editor/static/src/scss/web_editor.common.scss',
|
||||
'web_editor/static/src/scss/web_editor.frontend.scss',
|
||||
|
||||
'web_editor/static/src/js/wysiwyg/dialog.js',
|
||||
'web_editor/static/src/js/frontend/loader.js',
|
||||
],
|
||||
'web.report_assets_common': [
|
||||
'web_editor/static/src/scss/bootstrap_overridden.scss',
|
||||
'web_editor/static/src/scss/web_editor.common.scss',
|
||||
],
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# SUB BUNDLES
|
||||
#----------------------------------------------------------------------
|
||||
|
||||
'web._assets_primary_variables': [
|
||||
'web_editor/static/src/scss/web_editor.variables.scss',
|
||||
'web_editor/static/src/scss/wysiwyg.variables.scss',
|
||||
],
|
||||
'web._assets_secondary_variables': [
|
||||
'web_editor/static/src/scss/secondary_variables.scss',
|
||||
],
|
||||
'web._assets_backend_helpers': [
|
||||
'web_editor/static/src/scss/bootstrap_overridden_backend.scss',
|
||||
'web_editor/static/src/scss/bootstrap_overridden.scss',
|
||||
],
|
||||
'web._assets_frontend_helpers': [
|
||||
('prepend', 'web_editor/static/src/scss/bootstrap_overridden.scss'),
|
||||
],
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# TESTS BUNDLES
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
'web.qunit_suite_tests': [
|
||||
('include', 'web_editor.assets_wysiwyg'),
|
||||
|
||||
'web_editor/static/tests/**/*',
|
||||
'web_editor/static/src/js/editor/odoo-editor/test/utils.js'
|
||||
],
|
||||
},
|
||||
'auto_install': True,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import main
|
||||
Binary file not shown.
Binary file not shown.
868
odoo-bringout-oca-ocb-web_editor/web_editor/controllers/main.py
Normal file
868
odoo-bringout-oca-ocb-web_editor/web_editor/controllers/main.py
Normal file
|
|
@ -0,0 +1,868 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
import requests
|
||||
import werkzeug.exceptions
|
||||
import werkzeug.urls
|
||||
from PIL import Image, ImageFont, ImageDraw
|
||||
from lxml import etree
|
||||
from base64 import b64decode, b64encode
|
||||
from math import floor
|
||||
|
||||
from odoo.http import request, Response
|
||||
from odoo import http, tools, _, SUPERUSER_ID
|
||||
from odoo.addons.http_routing.models.ir_http import slug, unslug
|
||||
from odoo.addons.web_editor.tools import get_video_url_data
|
||||
from odoo.exceptions import UserError, MissingError, ValidationError
|
||||
from odoo.modules.module import get_resource_path
|
||||
from odoo.tools import file_open
|
||||
from odoo.tools.mimetypes import guess_mimetype
|
||||
from odoo.tools.image import image_data_uri, binary_to_image
|
||||
from odoo.addons.base.models.assetsbundle import AssetsBundle
|
||||
|
||||
from ..models.ir_attachment import SUPPORTED_IMAGE_EXTENSIONS, SUPPORTED_IMAGE_MIMETYPES
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
DEFAULT_LIBRARY_ENDPOINT = 'https://media-api.odoo.com'
|
||||
|
||||
diverging_history_regex = 'data-last-history-steps="([0-9,]+)"'
|
||||
|
||||
def ensure_no_history_divergence(record, html_field_name, incoming_history_ids):
|
||||
server_history_matches = re.search(diverging_history_regex, record[html_field_name] or '')
|
||||
# Do not check old documents without data-last-history-steps.
|
||||
if server_history_matches:
|
||||
server_last_history_id = server_history_matches[1].split(',')[-1]
|
||||
if server_last_history_id not in incoming_history_ids:
|
||||
logger.warning('The document was already saved from someone with a different history for model %r, field %r with id %r.', record._name, html_field_name, record.id)
|
||||
raise ValidationError(_('The document was already saved from someone with a different history for model %r, field %r with id %r.', record._name, html_field_name, record.id))
|
||||
|
||||
# This method must be called in a context that has write access to the record as
|
||||
# it will write to the bus.
|
||||
def handle_history_divergence(record, html_field_name, vals):
|
||||
# Do not handle history divergence if the field is not in the values.
|
||||
if html_field_name not in vals:
|
||||
return
|
||||
# Do not handle history divergence if in module installation mode.
|
||||
if record.env.context.get('install_module'):
|
||||
return
|
||||
incoming_html = vals[html_field_name]
|
||||
incoming_history_matches = re.search(diverging_history_regex, incoming_html or '')
|
||||
# When there is no incoming history id, it means that the value does not
|
||||
# comes from the odoo editor or the collaboration was not activated. In
|
||||
# project, it could come from the collaboration pad. In that case, we do not
|
||||
# handle history divergences.
|
||||
if request:
|
||||
channel = (request.db, 'editor_collaboration', record._name, html_field_name, record.id)
|
||||
if incoming_history_matches is None:
|
||||
if request:
|
||||
bus_data = {
|
||||
'model_name': record._name,
|
||||
'field_name': html_field_name,
|
||||
'res_id': record.id,
|
||||
'notificationName': 'html_field_write',
|
||||
'notificationPayload': {'last_step_id': None},
|
||||
}
|
||||
request.env['bus.bus']._sendone(channel, 'editor_collaboration', bus_data)
|
||||
return
|
||||
incoming_history_ids = incoming_history_matches[1].split(',')
|
||||
last_step_id = incoming_history_ids[-1]
|
||||
|
||||
bus_data = {
|
||||
'model_name': record._name,
|
||||
'field_name': html_field_name,
|
||||
'res_id': record.id,
|
||||
'notificationName': 'html_field_write',
|
||||
'notificationPayload': {'last_step_id': last_step_id},
|
||||
}
|
||||
if request:
|
||||
request.env['bus.bus']._sendone(channel, 'editor_collaboration', bus_data)
|
||||
|
||||
if record[html_field_name]:
|
||||
ensure_no_history_divergence(record, html_field_name, incoming_history_ids)
|
||||
|
||||
# Save only the latest id.
|
||||
vals[html_field_name] = incoming_html[0:incoming_history_matches.start(1)] + last_step_id + incoming_html[incoming_history_matches.end(1):]
|
||||
|
||||
def get_existing_attachment(IrAttachment, vals):
|
||||
"""
|
||||
Check if an attachment already exists for the same vals. Return it if
|
||||
so, None otherwise.
|
||||
"""
|
||||
fields = dict(vals)
|
||||
# Falsy res_id defaults to 0 on attachment creation.
|
||||
fields['res_id'] = fields.get('res_id') or 0
|
||||
raw, datas = fields.pop('raw', None), fields.pop('datas', None)
|
||||
domain = [(field, '=', value) for field, value in fields.items()]
|
||||
if fields.get('type') == 'url':
|
||||
if 'url' not in fields:
|
||||
return None
|
||||
domain.append(('checksum', '=', False))
|
||||
else:
|
||||
if not (raw or datas):
|
||||
return None
|
||||
domain.append(('checksum', '=', IrAttachment._compute_checksum(raw or b64decode(datas))))
|
||||
return IrAttachment.search(domain, limit=1) or None
|
||||
|
||||
class Web_Editor(http.Controller):
|
||||
#------------------------------------------------------
|
||||
# convert font into picture
|
||||
#------------------------------------------------------
|
||||
@http.route([
|
||||
'/web_editor/font_to_img/<icon>',
|
||||
'/web_editor/font_to_img/<icon>/<color>',
|
||||
'/web_editor/font_to_img/<icon>/<color>/<int:size>',
|
||||
'/web_editor/font_to_img/<icon>/<color>/<int:width>x<int:height>',
|
||||
'/web_editor/font_to_img/<icon>/<color>/<int:size>/<int:alpha>',
|
||||
'/web_editor/font_to_img/<icon>/<color>/<int:width>x<int:height>/<int:alpha>',
|
||||
'/web_editor/font_to_img/<icon>/<color>/<bg>',
|
||||
'/web_editor/font_to_img/<icon>/<color>/<bg>/<int:size>',
|
||||
'/web_editor/font_to_img/<icon>/<color>/<bg>/<int:width>x<int:height>',
|
||||
'/web_editor/font_to_img/<icon>/<color>/<bg>/<int:width>x<int:height>/<int:alpha>',
|
||||
], type='http', auth="none")
|
||||
def export_icon_to_png(self, icon, color='#000', bg=None, size=100, alpha=255, font='/web/static/src/libs/fontawesome/fonts/fontawesome-webfont.ttf', width=None, height=None):
|
||||
""" This method converts an unicode character to an image (using Font
|
||||
Awesome font by default) and is used only for mass mailing because
|
||||
custom fonts are not supported in mail.
|
||||
:param icon : decimal encoding of unicode character
|
||||
:param color : RGB code of the color
|
||||
:param bg : RGB code of the background color
|
||||
:param size : Pixels in integer
|
||||
:param alpha : transparency of the image from 0 to 255
|
||||
:param font : font path
|
||||
:param width : Pixels in integer
|
||||
:param height : Pixels in integer
|
||||
|
||||
:returns PNG image converted from given font
|
||||
"""
|
||||
# For custom icons, use the corresponding custom font
|
||||
if icon.isdigit():
|
||||
if int(icon) == 57467:
|
||||
font = "/web/static/fonts/tiktok_only.woff"
|
||||
elif int(icon) == 61593: # F099
|
||||
icon = "59392" # E800
|
||||
font = "/web/static/fonts/twitter_x_only.woff"
|
||||
elif int(icon) == 61569: # F081
|
||||
icon = "59395" # E803
|
||||
font = "/web/static/fonts/twitter_x_only.woff"
|
||||
|
||||
size = max(width, height, 1) if width else size
|
||||
width = width or size
|
||||
height = height or size
|
||||
# Make sure we have at least size=1
|
||||
width = max(1, min(width, 512))
|
||||
height = max(1, min(height, 512))
|
||||
# Initialize font
|
||||
if font.startswith('/'):
|
||||
font = font[1:]
|
||||
font_obj = ImageFont.truetype(file_open(font, 'rb'), height)
|
||||
|
||||
# if received character is not a number, keep old behaviour (icon is character)
|
||||
icon = chr(int(icon)) if icon.isdigit() else icon
|
||||
|
||||
# Background standardization
|
||||
if bg is not None and bg.startswith('rgba'):
|
||||
bg = bg.replace('rgba', 'rgb')
|
||||
bg = ','.join(bg.split(',')[:-1])+')'
|
||||
|
||||
# Convert the opacity value compatible with PIL Image color (0 to 255)
|
||||
# when color specifier is 'rgba'
|
||||
if color is not None and color.startswith('rgba'):
|
||||
*rgb, a = color.strip(')').split(',')
|
||||
opacity = str(floor(float(a) * 255))
|
||||
color = ','.join([*rgb, opacity]) + ')'
|
||||
|
||||
# Determine the dimensions of the icon
|
||||
image = Image.new("RGBA", (width, height), color)
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
if hasattr(draw, 'textbbox'):
|
||||
box = draw.textbbox((0, 0), icon, font=font_obj)
|
||||
left = box[0]
|
||||
top = box[1]
|
||||
boxw = box[2] - box[0]
|
||||
boxh = box[3] - box[1]
|
||||
else: # pillow < 8.00 (Focal)
|
||||
left, top, _right, _bottom = image.getbbox()
|
||||
boxw, boxh = draw.textsize(icon, font=font_obj)
|
||||
|
||||
draw.text((0, 0), icon, font=font_obj)
|
||||
|
||||
# Create an alpha mask
|
||||
imagemask = Image.new("L", (boxw, boxh), 0)
|
||||
drawmask = ImageDraw.Draw(imagemask)
|
||||
drawmask.text((-left, -top), icon, font=font_obj, fill=255)
|
||||
|
||||
# Create a solid color image and apply the mask
|
||||
if color.startswith('rgba'):
|
||||
color = color.replace('rgba', 'rgb')
|
||||
color = ','.join(color.split(',')[:-1])+')'
|
||||
iconimage = Image.new("RGBA", (boxw, boxh), color)
|
||||
iconimage.putalpha(imagemask)
|
||||
|
||||
# Create output image
|
||||
outimage = Image.new("RGBA", (boxw, height), bg or (0, 0, 0, 0))
|
||||
outimage.paste(iconimage, (left, top), iconimage)
|
||||
|
||||
# output image
|
||||
output = io.BytesIO()
|
||||
outimage.save(output, format="PNG")
|
||||
response = Response()
|
||||
response.mimetype = 'image/png'
|
||||
response.data = output.getvalue()
|
||||
response.headers['Cache-Control'] = 'public, max-age=604800'
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
response.headers['Access-Control-Allow-Methods'] = 'GET, POST'
|
||||
response.headers['Connection'] = 'close'
|
||||
response.headers['Date'] = time.strftime("%a, %d-%b-%Y %T GMT", time.gmtime())
|
||||
response.headers['Expires'] = time.strftime("%a, %d-%b-%Y %T GMT", time.gmtime(time.time()+604800*60))
|
||||
|
||||
return response
|
||||
|
||||
#------------------------------------------------------
|
||||
# Update a checklist in the editor on check/uncheck
|
||||
#------------------------------------------------------
|
||||
@http.route('/web_editor/checklist', type='json', auth='user')
|
||||
def update_checklist(self, res_model, res_id, filename, checklistId, checked, **kwargs):
|
||||
record = request.env[res_model].browse(res_id)
|
||||
value = filename in record._fields and record.read([filename])[0][filename]
|
||||
htmlelem = etree.fromstring("<div>%s</div>" % value, etree.HTMLParser())
|
||||
checked = bool(checked)
|
||||
|
||||
li = htmlelem.find(".//li[@id='checkId-%s']" % checklistId)
|
||||
|
||||
if li is None:
|
||||
return value
|
||||
|
||||
classname = li.get('class', '')
|
||||
if ('o_checked' in classname) != checked:
|
||||
if checked:
|
||||
classname = '%s o_checked' % classname
|
||||
else:
|
||||
classname = re.sub(r"\s?o_checked\s?", '', classname)
|
||||
li.set('class', classname)
|
||||
else:
|
||||
return value
|
||||
|
||||
value = etree.tostring(htmlelem[0][0], encoding='utf-8', method='html')[5:-6].decode("utf-8")
|
||||
record.write({filename: value})
|
||||
|
||||
return value
|
||||
|
||||
#------------------------------------------------------
|
||||
# Update a stars rating in the editor on check/uncheck
|
||||
#------------------------------------------------------
|
||||
@http.route('/web_editor/stars', type='json', auth='user')
|
||||
def update_stars(self, res_model, res_id, filename, starsId, rating):
|
||||
record = request.env[res_model].browse(res_id)
|
||||
value = filename in record._fields and record.read([filename])[0][filename]
|
||||
htmlelem = etree.fromstring("<div>%s</div>" % value, etree.HTMLParser())
|
||||
|
||||
stars_widget = htmlelem.find(".//span[@id='checkId-%s']" % starsId)
|
||||
|
||||
if stars_widget is None:
|
||||
return value
|
||||
|
||||
# Check the `rating` first stars and uncheck the others if any.
|
||||
stars = []
|
||||
for star in stars_widget.getchildren():
|
||||
if 'fa-star' in star.get('class', ''):
|
||||
stars.append(star)
|
||||
star_index = 0
|
||||
for star in stars:
|
||||
classname = star.get('class', '')
|
||||
if star_index < rating and (not 'fa-star' in classname or 'fa-star-o' in classname):
|
||||
classname = re.sub(r"\s?fa-star-o\s?", '', classname)
|
||||
classname = '%s fa-star' % classname
|
||||
star.set('class', classname)
|
||||
elif star_index >= rating and not 'fa-star-o' in classname:
|
||||
classname = re.sub(r"\s?fa-star\s?", '', classname)
|
||||
classname = '%s fa-star-o' % classname
|
||||
star.set('class', classname)
|
||||
star_index += 1
|
||||
|
||||
value = etree.tostring(htmlelem[0][0], encoding='utf-8', method='html')[5:-6]
|
||||
record.write({filename: value})
|
||||
|
||||
return value
|
||||
|
||||
@http.route('/web_editor/video_url/data', type='json', auth='user', website=True)
|
||||
def video_url_data(self, video_url, autoplay=False, loop=False,
|
||||
hide_controls=False, hide_fullscreen=False, hide_yt_logo=False,
|
||||
hide_dm_logo=False, hide_dm_share=False):
|
||||
# TODO: In Master, remove the parameter "hide_yt_logo" (the parameter is
|
||||
# no longer supported in the YouTube API.)
|
||||
if not request.env.user._is_internal():
|
||||
raise werkzeug.exceptions.Forbidden()
|
||||
return get_video_url_data(
|
||||
video_url, autoplay=autoplay, loop=loop,
|
||||
hide_controls=hide_controls, hide_fullscreen=hide_fullscreen,
|
||||
hide_dm_logo=hide_dm_logo, hide_dm_share=hide_dm_share
|
||||
)
|
||||
|
||||
@http.route('/web_editor/attachment/add_data', type='json', auth='user', methods=['POST'], website=True)
|
||||
def add_data(self, name, data, is_image, quality=0, width=0, height=0, res_id=False, res_model='ir.ui.view', **kwargs):
|
||||
data = b64decode(data)
|
||||
if is_image:
|
||||
format_error_msg = _("Uploaded image's format is not supported. Try with: %s", ', '.join(SUPPORTED_IMAGE_EXTENSIONS))
|
||||
try:
|
||||
mimetype = guess_mimetype(data)
|
||||
if mimetype not in SUPPORTED_IMAGE_MIMETYPES:
|
||||
return {'error': format_error_msg}
|
||||
data = tools.image_process(data, size=(width, height), quality=quality, verify_resolution=True)
|
||||
except (ValueError, UserError) as e:
|
||||
# When UserError thrown, browser considers file input an
|
||||
# image but not recognized as such by PIL, eg .webp
|
||||
return {'error': e.args[0]}
|
||||
|
||||
self._clean_context()
|
||||
attachment = self._attachment_create(name=name, data=data, res_id=res_id, res_model=res_model)
|
||||
return attachment._get_media_info()
|
||||
|
||||
@http.route('/web_editor/attachment/add_url', type='json', auth='user', methods=['POST'], website=True)
|
||||
def add_url(self, url, res_id=False, res_model='ir.ui.view', **kwargs):
|
||||
self._clean_context()
|
||||
attachment = self._attachment_create(url=url, res_id=res_id, res_model=res_model)
|
||||
return attachment._get_media_info()
|
||||
|
||||
@http.route('/web_editor/attachment/remove', type='json', auth='user', website=True)
|
||||
def remove(self, ids, **kwargs):
|
||||
""" Removes a web-based image attachment if it is used by no view (template)
|
||||
|
||||
Returns a dict mapping attachments which would not be removed (if any)
|
||||
mapped to the views preventing their removal
|
||||
"""
|
||||
self._clean_context()
|
||||
Attachment = attachments_to_remove = request.env['ir.attachment']
|
||||
Views = request.env['ir.ui.view']
|
||||
|
||||
# views blocking removal of the attachment
|
||||
removal_blocked_by = {}
|
||||
|
||||
for attachment in Attachment.browse(ids):
|
||||
# in-document URLs are html-escaped, a straight search will not
|
||||
# find them
|
||||
url = tools.html_escape(attachment.local_url)
|
||||
views = Views.search([
|
||||
"|",
|
||||
('arch_db', 'like', '"%s"' % url),
|
||||
('arch_db', 'like', "'%s'" % url)
|
||||
])
|
||||
|
||||
if views:
|
||||
removal_blocked_by[attachment.id] = views.read(['name'])
|
||||
else:
|
||||
attachments_to_remove += attachment
|
||||
if attachments_to_remove:
|
||||
attachments_to_remove.unlink()
|
||||
return removal_blocked_by
|
||||
|
||||
@http.route('/web_editor/get_image_info', type='json', auth='user', website=True)
|
||||
def get_image_info(self, src=''):
|
||||
"""This route is used to determine the original of an attachment so that
|
||||
it can be used as a base to modify it again (crop/optimization/filters).
|
||||
"""
|
||||
attachment = None
|
||||
if src.startswith('/web/image'):
|
||||
with contextlib.suppress(werkzeug.exceptions.NotFound, MissingError):
|
||||
_, args = request.env['ir.http']._match(src)
|
||||
record = request.env['ir.binary']._find_record(
|
||||
xmlid=args.get('xmlid'),
|
||||
res_model=args.get('model', 'ir.attachment'),
|
||||
res_id=args.get('id'),
|
||||
)
|
||||
if record._name == 'ir.attachment':
|
||||
attachment = record
|
||||
if not attachment:
|
||||
# Find attachment by url. There can be multiple matches because of default
|
||||
# snippet images referencing the same image in /static/, so we limit to 1
|
||||
attachment = request.env['ir.attachment'].search([
|
||||
'|', ('url', '=like', src), ('url', '=like', '%s?%%' % src),
|
||||
('mimetype', 'in', SUPPORTED_IMAGE_MIMETYPES),
|
||||
], limit=1)
|
||||
if not attachment:
|
||||
return {
|
||||
'attachment': False,
|
||||
'original': False,
|
||||
}
|
||||
return {
|
||||
'attachment': attachment.read(['id'])[0],
|
||||
'original': (attachment.original_id or attachment).read(['id', 'image_src', 'mimetype'])[0],
|
||||
}
|
||||
|
||||
def _attachment_create(self, name='', data=False, url=False, res_id=False, res_model='ir.ui.view'):
|
||||
"""Create and return a new attachment."""
|
||||
IrAttachment = request.env['ir.attachment']
|
||||
|
||||
if name.lower().endswith('.bmp'):
|
||||
# Avoid mismatch between content type and mimetype, see commit msg
|
||||
name = name[:-4]
|
||||
|
||||
if not name and url:
|
||||
name = url.split("/").pop()
|
||||
|
||||
if res_model != 'ir.ui.view' and res_id:
|
||||
res_id = int(res_id)
|
||||
else:
|
||||
res_id = False
|
||||
|
||||
attachment_data = {
|
||||
'name': name,
|
||||
'public': res_model == 'ir.ui.view',
|
||||
'res_id': res_id,
|
||||
'res_model': res_model,
|
||||
}
|
||||
|
||||
if data:
|
||||
attachment_data['raw'] = data
|
||||
if url:
|
||||
attachment_data['url'] = url
|
||||
elif url:
|
||||
attachment_data.update({
|
||||
'type': 'url',
|
||||
'url': url,
|
||||
})
|
||||
else:
|
||||
raise UserError(_("You need to specify either data or url to create an attachment."))
|
||||
|
||||
# Despite the user having no right to create an attachment, he can still
|
||||
# create an image attachment through some flows
|
||||
if (
|
||||
not request.env.is_admin()
|
||||
and IrAttachment._can_bypass_rights_on_media_dialog(**attachment_data)
|
||||
):
|
||||
attachment = IrAttachment.sudo().create(attachment_data)
|
||||
# When portal users upload an attachment with the wysiwyg widget,
|
||||
# the access token is needed to use the image in the editor. If
|
||||
# the attachment is not public, the user won't be able to generate
|
||||
# the token, so we need to generate it using sudo
|
||||
if not attachment_data['public']:
|
||||
attachment.sudo().generate_access_token()
|
||||
else:
|
||||
attachment = get_existing_attachment(IrAttachment, attachment_data) \
|
||||
or IrAttachment.create(attachment_data)
|
||||
|
||||
return attachment
|
||||
|
||||
def _clean_context(self):
|
||||
# avoid allowed_company_ids which may erroneously restrict based on website
|
||||
context = dict(request.context)
|
||||
context.pop('allowed_company_ids', None)
|
||||
request.update_env(context=context)
|
||||
|
||||
@http.route("/web_editor/get_assets_editor_resources", type="json", auth="user", website=True)
|
||||
def get_assets_editor_resources(self, key, get_views=True, get_scss=True, get_js=True, bundles=False, bundles_restriction=[], only_user_custom_files=True):
|
||||
"""
|
||||
Transmit the resources the assets editor needs to work.
|
||||
|
||||
Params:
|
||||
key (str): the key of the view the resources are related to
|
||||
|
||||
get_views (bool, default=True):
|
||||
True if the views must be fetched
|
||||
|
||||
get_scss (bool, default=True):
|
||||
True if the style must be fetched
|
||||
|
||||
get_js (bool, default=True):
|
||||
True if the javascript must be fetched
|
||||
|
||||
bundles (bool, default=False):
|
||||
True if the bundles views must be fetched
|
||||
|
||||
bundles_restriction (list, default=[]):
|
||||
Names of the bundles in which to look for scss files
|
||||
(if empty, search in all of them)
|
||||
|
||||
only_user_custom_files (bool, default=True):
|
||||
True if only user custom files must be fetched
|
||||
|
||||
Returns:
|
||||
dict: views, scss, js
|
||||
"""
|
||||
# Related views must be fetched if the user wants the views and/or the style
|
||||
views = request.env["ir.ui.view"].with_context(no_primary_children=True, __views_get_original_hierarchy=[]).get_related_views(key, bundles=bundles)
|
||||
views = views.read(['name', 'id', 'key', 'xml_id', 'arch', 'active', 'inherit_id'])
|
||||
|
||||
scss_files_data_by_bundle = []
|
||||
js_files_data_by_bundle = []
|
||||
|
||||
if get_scss:
|
||||
scss_files_data_by_bundle = self._load_resources('scss', views, bundles_restriction, only_user_custom_files)
|
||||
if get_js:
|
||||
js_files_data_by_bundle = self._load_resources('js', views, bundles_restriction, only_user_custom_files)
|
||||
|
||||
return {
|
||||
'views': get_views and views or [],
|
||||
'scss': get_scss and scss_files_data_by_bundle or [],
|
||||
'js': get_js and js_files_data_by_bundle or [],
|
||||
}
|
||||
|
||||
def _load_resources(self, file_type, views, bundles_restriction, only_user_custom_files):
|
||||
AssetsUtils = request.env['web_editor.assets']
|
||||
|
||||
files_data_by_bundle = []
|
||||
resources_type_info = {'t_call_assets_attribute': 't-js', 'mimetype': 'text/javascript'}
|
||||
if file_type == 'scss':
|
||||
resources_type_info = {'t_call_assets_attribute': 't-css', 'mimetype': 'text/scss'}
|
||||
|
||||
# Compile regex outside of the loop
|
||||
# This will used to exclude library scss files from the result
|
||||
excluded_url_matcher = re.compile(r"^(.+/lib/.+)|(.+import_bootstrap.+\.scss)$")
|
||||
|
||||
# First check the t-call-assets used in the related views
|
||||
url_infos = dict()
|
||||
for v in views:
|
||||
for asset_call_node in etree.fromstring(v["arch"]).xpath("//t[@t-call-assets]"):
|
||||
attr = asset_call_node.get(resources_type_info['t_call_assets_attribute'])
|
||||
if attr and not json.loads(attr.lower()):
|
||||
continue
|
||||
asset_name = asset_call_node.get("t-call-assets")
|
||||
|
||||
# Loop through bundle files to search for file info
|
||||
files_data = []
|
||||
for file_info in request.env["ir.qweb"]._get_asset_content(asset_name)[0]:
|
||||
if file_info["atype"] != resources_type_info['mimetype']:
|
||||
continue
|
||||
url = file_info["url"]
|
||||
|
||||
# Exclude library files (see regex above)
|
||||
if excluded_url_matcher.match(url):
|
||||
continue
|
||||
|
||||
# Check if the file is customized and get bundle/path info
|
||||
file_data = AssetsUtils._get_data_from_url(url)
|
||||
if not file_data:
|
||||
continue
|
||||
|
||||
# Save info according to the filter (arch will be fetched later)
|
||||
url_infos[url] = file_data
|
||||
|
||||
if '/user_custom_' in url \
|
||||
or file_data['customized'] \
|
||||
or file_type == 'scss' and not only_user_custom_files:
|
||||
files_data.append(url)
|
||||
|
||||
# scss data is returned sorted by bundle, with the bundles
|
||||
# names and xmlids
|
||||
if len(files_data):
|
||||
files_data_by_bundle.append([asset_name, files_data])
|
||||
|
||||
# Filter bundles/files:
|
||||
# - A file which appears in multiple bundles only appears in the
|
||||
# first one (the first in the DOM)
|
||||
# - Only keep bundles with files which appears in the asked bundles
|
||||
# and only keep those files
|
||||
for i in range(0, len(files_data_by_bundle)):
|
||||
bundle_1 = files_data_by_bundle[i]
|
||||
for j in range(0, len(files_data_by_bundle)):
|
||||
bundle_2 = files_data_by_bundle[j]
|
||||
# In unwanted bundles, keep only the files which are in wanted bundles too (web._helpers)
|
||||
if bundle_1[0] not in bundles_restriction and bundle_2[0] in bundles_restriction:
|
||||
bundle_1[1] = [item_1 for item_1 in bundle_1[1] if item_1 in bundle_2[1]]
|
||||
for i in range(0, len(files_data_by_bundle)):
|
||||
bundle_1 = files_data_by_bundle[i]
|
||||
for j in range(i + 1, len(files_data_by_bundle)):
|
||||
bundle_2 = files_data_by_bundle[j]
|
||||
# In every bundle, keep only the files which were not found
|
||||
# in previous bundles
|
||||
bundle_2[1] = [item_2 for item_2 in bundle_2[1] if item_2 not in bundle_1[1]]
|
||||
|
||||
# Only keep bundles which still have files and that were requested
|
||||
files_data_by_bundle = [
|
||||
data for data in files_data_by_bundle
|
||||
if (len(data[1]) > 0 and (not bundles_restriction or data[0] in bundles_restriction))
|
||||
]
|
||||
|
||||
# Fetch the arch of each kept file, in each bundle
|
||||
urls = []
|
||||
for bundle_data in files_data_by_bundle:
|
||||
urls += bundle_data[1]
|
||||
custom_attachments = AssetsUtils._get_custom_attachment(urls, op='in')
|
||||
|
||||
for bundle_data in files_data_by_bundle:
|
||||
for i in range(0, len(bundle_data[1])):
|
||||
url = bundle_data[1][i]
|
||||
url_info = url_infos[url]
|
||||
|
||||
content = AssetsUtils._get_content_from_url(url, url_info, custom_attachments)
|
||||
|
||||
bundle_data[1][i] = {
|
||||
'url': "/%s/%s" % (url_info["module"], url_info["resource_path"]),
|
||||
'arch': content,
|
||||
'customized': url_info["customized"],
|
||||
}
|
||||
|
||||
return files_data_by_bundle
|
||||
|
||||
@http.route('/web_editor/modify_image/<model("ir.attachment"):attachment>', type="json", auth="user", website=True)
|
||||
def modify_image(self, attachment, res_model=None, res_id=None, name=None, data=None, original_id=None, mimetype=None):
|
||||
"""
|
||||
Creates a modified copy of an attachment and returns its image_src to be
|
||||
inserted into the DOM.
|
||||
"""
|
||||
fields = {
|
||||
'original_id': attachment.id,
|
||||
'datas': data,
|
||||
'type': 'binary',
|
||||
'res_model': res_model or 'ir.ui.view',
|
||||
'mimetype': mimetype or attachment.mimetype,
|
||||
}
|
||||
if fields['res_model'] == 'ir.ui.view':
|
||||
fields['res_id'] = 0
|
||||
elif res_id:
|
||||
fields['res_id'] = res_id
|
||||
if name:
|
||||
fields['name'] = name
|
||||
existing_attachment = get_existing_attachment(request.env['ir.attachment'], fields)
|
||||
if existing_attachment and not existing_attachment.url:
|
||||
attachment = existing_attachment
|
||||
else:
|
||||
attachment = attachment.copy(fields)
|
||||
if attachment.url:
|
||||
# Don't keep url if modifying static attachment because static images
|
||||
# are only served from disk and don't fallback to attachments.
|
||||
if re.match(r'^/\w+/static/', attachment.url):
|
||||
attachment.url = None
|
||||
# Uniquify url by adding a path segment with the id before the name.
|
||||
# This allows us to keep the unsplash url format so it still reacts
|
||||
# to the unsplash beacon.
|
||||
else:
|
||||
url_fragments = attachment.url.split('/')
|
||||
url_fragments.insert(-1, str(attachment.id))
|
||||
attachment.url = '/'.join(url_fragments)
|
||||
if attachment.public:
|
||||
return attachment.image_src
|
||||
attachment.generate_access_token()
|
||||
return '%s?access_token=%s' % (attachment.image_src, attachment.access_token)
|
||||
|
||||
def _get_shape_svg(self, module, *segments):
|
||||
Module = request.env['ir.module.module'].sudo()
|
||||
# Avoid creating a bridge module just for this check.
|
||||
if 'imported' in Module._fields and Module.search([('name', '=', module)]).imported:
|
||||
attachment = request.env['ir.attachment'].sudo().search([
|
||||
('url', '=', f"/{module.replace('.', '_')}/static/{'/'.join(segments)}"),
|
||||
('public', '=', True),
|
||||
('type', '=', 'binary'),
|
||||
], limit=1)
|
||||
if attachment:
|
||||
return b64decode(attachment.datas)
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
shape_path = get_resource_path(module, 'static', *segments)
|
||||
if not shape_path:
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
with tools.file_open(shape_path, 'r', filter_ext=('.svg',)) as file:
|
||||
return file.read()
|
||||
|
||||
def _update_svg_colors(self, options, svg):
|
||||
user_colors = []
|
||||
svg_options = {}
|
||||
default_palette = {
|
||||
'1': '#3AADAA',
|
||||
'2': '#7C6576',
|
||||
'3': '#F6F6F6',
|
||||
'4': '#FFFFFF',
|
||||
'5': '#383E45',
|
||||
}
|
||||
bundle_css = None
|
||||
regex_hex = r'#[0-9A-F]{6,8}'
|
||||
regex_rgba = r'rgba?\(\d{1,3},\d{1,3},\d{1,3}(?:,[0-9.]{1,4})?\)'
|
||||
for key, value in options.items():
|
||||
colorMatch = re.match('^c([1-5])$', key)
|
||||
if colorMatch:
|
||||
css_color_value = value
|
||||
# Check that color is hex or rgb(a) to prevent arbitrary injection
|
||||
if not re.match(r'(?i)^%s$|^%s$' % (regex_hex, regex_rgba), css_color_value.replace(' ', '')):
|
||||
if re.match('^o-color-([1-5])$', css_color_value):
|
||||
if not bundle_css:
|
||||
bundle = 'web.assets_frontend'
|
||||
files, _ = request.env["ir.qweb"]._get_asset_content(bundle)
|
||||
asset = AssetsBundle(bundle, files)
|
||||
bundle_css = asset.css().index_content
|
||||
color_search = re.search(r'(?i)--%s:\s+(%s|%s)' % (css_color_value, regex_hex, regex_rgba), bundle_css)
|
||||
if not color_search:
|
||||
raise werkzeug.exceptions.BadRequest()
|
||||
css_color_value = color_search.group(1)
|
||||
else:
|
||||
raise werkzeug.exceptions.BadRequest()
|
||||
user_colors.append([tools.html_escape(css_color_value), colorMatch.group(1)])
|
||||
else:
|
||||
svg_options[key] = value
|
||||
|
||||
color_mapping = {default_palette[palette_number]: color for color, palette_number in user_colors}
|
||||
# create a case-insensitive regex to match all the colors to replace, eg: '(?i)(#3AADAA)|(#7C6576)'
|
||||
regex = '(?i)%s' % '|'.join('(%s)' % color for color in color_mapping.keys())
|
||||
|
||||
def subber(match):
|
||||
key = match.group().upper()
|
||||
return color_mapping[key] if key in color_mapping else key
|
||||
return re.sub(regex, subber, svg), svg_options
|
||||
|
||||
@http.route(['/web_editor/shape/<module>/<path:filename>'], type='http', auth="public", website=True)
|
||||
def shape(self, module, filename, **kwargs):
|
||||
"""
|
||||
Returns a color-customized svg (background shape or illustration).
|
||||
"""
|
||||
svg = None
|
||||
if module == 'illustration':
|
||||
attachment = request.env['ir.attachment'].sudo().browse(unslug(filename)[1])
|
||||
if (not attachment.exists()
|
||||
or attachment.type != 'binary'
|
||||
or not attachment.public
|
||||
or not attachment.url.startswith(request.httprequest.path)):
|
||||
# Fallback to URL lookup to allow using shapes that were
|
||||
# imported from data files.
|
||||
attachment = request.env['ir.attachment'].sudo().search([
|
||||
('type', '=', 'binary'),
|
||||
('public', '=', True),
|
||||
('url', '=', request.httprequest.path),
|
||||
], limit=1)
|
||||
if not attachment:
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
svg = attachment.raw.decode('utf-8')
|
||||
else:
|
||||
svg = self._get_shape_svg(module, 'shapes', filename)
|
||||
|
||||
svg, options = self._update_svg_colors(kwargs, svg)
|
||||
flip_value = options.get('flip', False)
|
||||
if flip_value == 'x':
|
||||
svg = svg.replace('<svg ', '<svg style="transform: scaleX(-1);" ', 1)
|
||||
elif flip_value == 'y':
|
||||
svg = svg.replace('<svg ', '<svg style="transform: scaleY(-1)" ', 1)
|
||||
elif flip_value == 'xy':
|
||||
svg = svg.replace('<svg ', '<svg style="transform: scale(-1)" ', 1)
|
||||
|
||||
return request.make_response(svg, [
|
||||
('Content-type', 'image/svg+xml'),
|
||||
('Cache-control', 'max-age=%s' % http.STATIC_CACHE_LONG),
|
||||
])
|
||||
|
||||
@http.route(['/web_editor/image_shape/<string:img_key>/<module>/<path:filename>'], type='http', auth="public", website=True)
|
||||
def image_shape(self, module, filename, img_key, **kwargs):
|
||||
svg = self._get_shape_svg(module, 'image_shapes', filename)
|
||||
|
||||
record = request.env['ir.binary']._find_record(img_key)
|
||||
stream = request.env['ir.binary']._get_image_stream_from(record)
|
||||
if stream.type == 'url':
|
||||
return stream.get_response()
|
||||
|
||||
image = stream.read()
|
||||
img = binary_to_image(image)
|
||||
width, height = tuple(str(size) for size in img.size)
|
||||
root = etree.fromstring(svg)
|
||||
|
||||
if root.attrib.get("data-forced-size"):
|
||||
# Adjusts the SVG height to ensure the image fits properly within
|
||||
# the SVG (e.g. for "devices" shapes).
|
||||
svgHeight = float(root.attrib.get("height"))
|
||||
svgWidth = float(root.attrib.get("width"))
|
||||
svgAspectRatio = svgWidth / svgHeight
|
||||
height = str(float(width) / svgAspectRatio)
|
||||
|
||||
root.attrib.update({'width': width, 'height': height})
|
||||
# Update default color palette on shape SVG.
|
||||
svg, _ = self._update_svg_colors(kwargs, etree.tostring(root, pretty_print=True).decode('utf-8'))
|
||||
# Add image in base64 inside the shape.
|
||||
uri = image_data_uri(b64encode(image))
|
||||
svg = svg.replace('<image xlink:href="', '<image xlink:href="%s' % uri)
|
||||
|
||||
return request.make_response(svg, [
|
||||
('Content-type', 'image/svg+xml'),
|
||||
('Cache-control', 'max-age=%s' % http.STATIC_CACHE_LONG),
|
||||
])
|
||||
|
||||
@http.route(['/web_editor/media_library_search'], type='json', auth="user", website=True)
|
||||
def media_library_search(self, **params):
|
||||
ICP = request.env['ir.config_parameter'].sudo()
|
||||
endpoint = ICP.get_param('web_editor.media_library_endpoint', DEFAULT_LIBRARY_ENDPOINT)
|
||||
params['dbuuid'] = ICP.get_param('database.uuid')
|
||||
response = requests.post('%s/media-library/1/search' % endpoint, data=params)
|
||||
if response.status_code == requests.codes.ok and response.headers['content-type'] == 'application/json':
|
||||
return response.json()
|
||||
else:
|
||||
return {'error': response.status_code}
|
||||
|
||||
@http.route('/web_editor/save_library_media', type='json', auth='user', methods=['POST'])
|
||||
def save_library_media(self, media):
|
||||
"""
|
||||
Saves images from the media library as new attachments, making them
|
||||
dynamic SVGs if needed.
|
||||
media = {
|
||||
<media_id>: {
|
||||
'query': 'space separated search terms',
|
||||
'is_dynamic_svg': True/False,
|
||||
'dynamic_colors': maps color names to their color,
|
||||
}, ...
|
||||
}
|
||||
"""
|
||||
attachments = []
|
||||
ICP = request.env['ir.config_parameter'].sudo()
|
||||
library_endpoint = ICP.get_param('web_editor.media_library_endpoint', DEFAULT_LIBRARY_ENDPOINT)
|
||||
|
||||
media_ids = ','.join(media.keys())
|
||||
params = {
|
||||
'dbuuid': ICP.get_param('database.uuid'),
|
||||
'media_ids': media_ids,
|
||||
}
|
||||
response = requests.post('%s/media-library/1/download_urls' % library_endpoint, data=params)
|
||||
if response.status_code != requests.codes.ok:
|
||||
raise Exception(_("ERROR: couldn't get download urls from media library."))
|
||||
|
||||
for id, url in response.json().items():
|
||||
req = requests.get(url)
|
||||
name = '_'.join([media[id]['query'], url.split('/')[-1]])
|
||||
IrAttachment = request.env['ir.attachment']
|
||||
attachment_data = {
|
||||
'name': name,
|
||||
'mimetype': req.headers['content-type'],
|
||||
'datas': b64encode(req.content),
|
||||
'public': True,
|
||||
'res_model': 'ir.ui.view',
|
||||
'res_id': 0,
|
||||
}
|
||||
attachment = get_existing_attachment(IrAttachment, attachment_data)
|
||||
# Need to bypass security check to write image with mimetype image/svg+xml
|
||||
# ok because svgs come from whitelisted origin
|
||||
if not attachment:
|
||||
context = {'binary_field_real_user': request.env['res.users'].sudo().browse([SUPERUSER_ID])}
|
||||
attachment = IrAttachment.sudo().with_context(context).create(attachment_data)
|
||||
if media[id]['is_dynamic_svg']:
|
||||
colorParams = werkzeug.urls.url_encode(media[id]['dynamic_colors'])
|
||||
attachment['url'] = '/web_editor/shape/illustration/%s?%s' % (slug(attachment), colorParams)
|
||||
attachments.append(attachment._get_media_info())
|
||||
|
||||
return attachments
|
||||
|
||||
@http.route("/web_editor/get_ice_servers", type='json', auth="user")
|
||||
def get_ice_servers(self):
|
||||
return request.env['mail.ice.server']._get_ice_servers()
|
||||
|
||||
@http.route("/web_editor/bus_broadcast", type="json", auth="user")
|
||||
def bus_broadcast(self, model_name, field_name, res_id, bus_data):
|
||||
document = request.env[model_name].browse([res_id])
|
||||
|
||||
document.check_access_rights('read')
|
||||
document.check_field_access_rights('read', [field_name])
|
||||
document.check_access_rule('read')
|
||||
document.check_access_rights('write')
|
||||
document.check_field_access_rights('write', [field_name])
|
||||
document.check_access_rule('write')
|
||||
|
||||
channel = (request.db, 'editor_collaboration', model_name, field_name, int(res_id))
|
||||
bus_data.update({'model_name': model_name, 'field_name': field_name, 'res_id': res_id})
|
||||
request.env['bus.bus']._sendone(channel, 'editor_collaboration', bus_data)
|
||||
|
||||
@http.route('/web_editor/tests', type='http', auth="user")
|
||||
def test_suite(self, mod=None, **kwargs):
|
||||
return request.render('web_editor.tests')
|
||||
|
||||
@http.route("/web_editor/ensure_common_history", type="json", auth="user")
|
||||
def ensure_common_history(self, model_name, field_name, res_id, history_ids):
|
||||
record = request.env[model_name].browse([res_id])
|
||||
try:
|
||||
ensure_no_history_divergence(record, field_name, history_ids)
|
||||
except ValidationError:
|
||||
return record[field_name]
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="web_editor.13_0_color_system_support_primary_variables_scss" model="ir.asset">
|
||||
<field name="name">13 0 color system support primary variables SCSS</field>
|
||||
<field name="bundle">web._assets_primary_variables</field>
|
||||
<field name="path">web_editor/static/src/scss/13_0_color_system_support_primary_variables.scss</field>
|
||||
<field name="active" eval="False"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
3392
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/af.po
Normal file
3392
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/af.po
Normal file
File diff suppressed because it is too large
Load diff
3388
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/am.po
Normal file
3388
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/am.po
Normal file
File diff suppressed because it is too large
Load diff
3450
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/ar.po
Normal file
3450
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/ar.po
Normal file
File diff suppressed because it is too large
Load diff
3437
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/az.po
Normal file
3437
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/az.po
Normal file
File diff suppressed because it is too large
Load diff
3420
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/be.po
Normal file
3420
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/be.po
Normal file
File diff suppressed because it is too large
Load diff
3437
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/bg.po
Normal file
3437
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/bg.po
Normal file
File diff suppressed because it is too large
Load diff
3416
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/bs.po
Normal file
3416
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/bs.po
Normal file
File diff suppressed because it is too large
Load diff
3478
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/ca.po
Normal file
3478
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/ca.po
Normal file
File diff suppressed because it is too large
Load diff
3461
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/cs.po
Normal file
3461
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/cs.po
Normal file
File diff suppressed because it is too large
Load diff
3450
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/da.po
Normal file
3450
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/da.po
Normal file
File diff suppressed because it is too large
Load diff
3473
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/de.po
Normal file
3473
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/de.po
Normal file
File diff suppressed because it is too large
Load diff
1768
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/el.po
Normal file
1768
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/el.po
Normal file
File diff suppressed because it is too large
Load diff
1511
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/en_AU.po
Normal file
1511
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/en_AU.po
Normal file
File diff suppressed because it is too large
Load diff
1531
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/en_GB.po
Normal file
1531
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/en_GB.po
Normal file
File diff suppressed because it is too large
Load diff
3465
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/es.po
Normal file
3465
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/es.po
Normal file
File diff suppressed because it is too large
Load diff
1511
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/es_CL.po
Normal file
1511
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/es_CL.po
Normal file
File diff suppressed because it is too large
Load diff
1565
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/es_CO.po
Normal file
1565
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/es_CO.po
Normal file
File diff suppressed because it is too large
Load diff
1508
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/es_CR.po
Normal file
1508
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/es_CR.po
Normal file
File diff suppressed because it is too large
Load diff
1515
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/es_DO.po
Normal file
1515
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/es_DO.po
Normal file
File diff suppressed because it is too large
Load diff
1564
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/es_EC.po
Normal file
1564
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/es_EC.po
Normal file
File diff suppressed because it is too large
Load diff
3466
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/es_MX.po
Normal file
3466
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/es_MX.po
Normal file
File diff suppressed because it is too large
Load diff
1515
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/es_PE.po
Normal file
1515
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/es_PE.po
Normal file
File diff suppressed because it is too large
Load diff
1511
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/es_VE.po
Normal file
1511
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/es_VE.po
Normal file
File diff suppressed because it is too large
Load diff
3443
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/et.po
Normal file
3443
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/et.po
Normal file
File diff suppressed because it is too large
Load diff
3475
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/fa.po
Normal file
3475
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/fa.po
Normal file
File diff suppressed because it is too large
Load diff
3463
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/fi.po
Normal file
3463
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/fi.po
Normal file
File diff suppressed because it is too large
Load diff
3465
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/fr.po
Normal file
3465
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/fr.po
Normal file
File diff suppressed because it is too large
Load diff
1511
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/fr_CA.po
Normal file
1511
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/fr_CA.po
Normal file
File diff suppressed because it is too large
Load diff
1510
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/gl.po
Normal file
1510
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/gl.po
Normal file
File diff suppressed because it is too large
Load diff
3393
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/gu.po
Normal file
3393
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/gu.po
Normal file
File diff suppressed because it is too large
Load diff
3443
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/he.po
Normal file
3443
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/he.po
Normal file
File diff suppressed because it is too large
Load diff
3424
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/hi.po
Normal file
3424
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/hi.po
Normal file
File diff suppressed because it is too large
Load diff
3440
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/hr.po
Normal file
3440
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/hr.po
Normal file
File diff suppressed because it is too large
Load diff
3458
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/hu.po
Normal file
3458
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/hu.po
Normal file
File diff suppressed because it is too large
Load diff
3388
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/hy.po
Normal file
3388
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/hy.po
Normal file
File diff suppressed because it is too large
Load diff
3455
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/id.po
Normal file
3455
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/id.po
Normal file
File diff suppressed because it is too large
Load diff
3396
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/is.po
Normal file
3396
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/is.po
Normal file
File diff suppressed because it is too large
Load diff
3457
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/it.po
Normal file
3457
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/it.po
Normal file
File diff suppressed because it is too large
Load diff
3427
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/ja.po
Normal file
3427
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/ja.po
Normal file
File diff suppressed because it is too large
Load diff
1516
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/ka.po
Normal file
1516
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/ka.po
Normal file
File diff suppressed because it is too large
Load diff
1513
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/kab.po
Normal file
1513
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/kab.po
Normal file
File diff suppressed because it is too large
Load diff
3426
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/km.po
Normal file
3426
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/km.po
Normal file
File diff suppressed because it is too large
Load diff
3430
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/ko.po
Normal file
3430
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/ko.po
Normal file
File diff suppressed because it is too large
Load diff
2019
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/lb.po
Normal file
2019
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/lb.po
Normal file
File diff suppressed because it is too large
Load diff
3423
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/lo.po
Normal file
3423
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/lo.po
Normal file
File diff suppressed because it is too large
Load diff
3444
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/lt.po
Normal file
3444
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/lt.po
Normal file
File diff suppressed because it is too large
Load diff
3428
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/lv.po
Normal file
3428
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/lv.po
Normal file
File diff suppressed because it is too large
Load diff
1527
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/mk.po
Normal file
1527
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/mk.po
Normal file
File diff suppressed because it is too large
Load diff
3422
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/ml.po
Normal file
3422
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/ml.po
Normal file
File diff suppressed because it is too large
Load diff
3429
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/mn.po
Normal file
3429
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/mn.po
Normal file
File diff suppressed because it is too large
Load diff
3394
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/ms.po
Normal file
3394
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/ms.po
Normal file
File diff suppressed because it is too large
Load diff
3435
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/nb.po
Normal file
3435
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/nb.po
Normal file
File diff suppressed because it is too large
Load diff
3465
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/nl.po
Normal file
3465
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/nl.po
Normal file
File diff suppressed because it is too large
Load diff
3392
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/no.po
Normal file
3392
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/no.po
Normal file
File diff suppressed because it is too large
Load diff
3478
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/pl.po
Normal file
3478
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/pl.po
Normal file
File diff suppressed because it is too large
Load diff
3438
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/pt.po
Normal file
3438
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/pt.po
Normal file
File diff suppressed because it is too large
Load diff
3463
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/pt_BR.po
Normal file
3463
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/pt_BR.po
Normal file
File diff suppressed because it is too large
Load diff
3460
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/ro.po
Normal file
3460
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/ro.po
Normal file
File diff suppressed because it is too large
Load diff
3471
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/ru.po
Normal file
3471
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/ru.po
Normal file
File diff suppressed because it is too large
Load diff
3446
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/sk.po
Normal file
3446
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/sk.po
Normal file
File diff suppressed because it is too large
Load diff
3444
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/sl.po
Normal file
3444
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/sl.po
Normal file
File diff suppressed because it is too large
Load diff
3388
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/sq.po
Normal file
3388
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/sq.po
Normal file
File diff suppressed because it is too large
Load diff
3444
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/sr.po
Normal file
3444
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/sr.po
Normal file
File diff suppressed because it is too large
Load diff
1731
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/sr@latin.po
Normal file
1731
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/sr@latin.po
Normal file
File diff suppressed because it is too large
Load diff
3470
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/sv.po
Normal file
3470
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/sv.po
Normal file
File diff suppressed because it is too large
Load diff
3388
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/sw.po
Normal file
3388
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/sw.po
Normal file
File diff suppressed because it is too large
Load diff
3388
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/ta.po
Normal file
3388
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/ta.po
Normal file
File diff suppressed because it is too large
Load diff
3447
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/th.po
Normal file
3447
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/th.po
Normal file
File diff suppressed because it is too large
Load diff
3470
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/tr.po
Normal file
3470
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/tr.po
Normal file
File diff suppressed because it is too large
Load diff
3460
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/uk.po
Normal file
3460
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/uk.po
Normal file
File diff suppressed because it is too large
Load diff
3454
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/vi.po
Normal file
3454
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/vi.po
Normal file
File diff suppressed because it is too large
Load diff
3416
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/web_editor.pot
Normal file
3416
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/web_editor.pot
Normal file
File diff suppressed because it is too large
Load diff
3428
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/zh_CN.po
Normal file
3428
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/zh_CN.po
Normal file
File diff suppressed because it is too large
Load diff
3422
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/zh_TW.po
Normal file
3422
odoo-bringout-oca-ocb-web_editor/web_editor/i18n/zh_TW.po
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import ir_attachment
|
||||
from . import ir_qweb_fields
|
||||
from . import ir_ui_view
|
||||
from . import ir_http
|
||||
from . import ir_websocket
|
||||
from . import models
|
||||
|
||||
from . import assets
|
||||
|
||||
from . import test_models
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue