mirror of
https://github.com/bringout/oca-ocb-test.git
synced 2026-04-18 04:02:01 +02:00
Initial commit: Test packages
This commit is contained in:
commit
080accd21c
338 changed files with 32413 additions and 0 deletions
52
odoo-bringout-oca-ocb-test_website/README.md
Normal file
52
odoo-bringout-oca-ocb-test_website/README.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# Website Test
|
||||
|
||||
This module contains tests related to website. Those are
|
||||
present in a separate module as we are testing module install/uninstall/upgrade
|
||||
and we don't want to reload the website module every time, including it's possible
|
||||
dependencies. Neither we want to add in website module some routes, views and
|
||||
models which only purpose is to run tests.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-ocb-test_website
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
This addon depends on:
|
||||
- web_unsplash
|
||||
- website
|
||||
- theme_default
|
||||
|
||||
## Manifest Information
|
||||
|
||||
- **Name**: Website Test
|
||||
- **Version**: 1.0
|
||||
- **Category**: Hidden
|
||||
- **License**: LGPL-3
|
||||
- **Installable**: True
|
||||
|
||||
## Source
|
||||
|
||||
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `test_website`.
|
||||
|
||||
## 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
|
||||
- Reports: doc/REPORTS.md
|
||||
- Security: doc/SECURITY.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-test_website/doc/ARCHITECTURE.md
Normal file
32
odoo-bringout-oca-ocb-test_website/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 Test_website Module - test_website
|
||||
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-test_website/doc/CONFIGURATION.md
Normal file
3
odoo-bringout-oca-ocb-test_website/doc/CONFIGURATION.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Configuration
|
||||
|
||||
Refer to Odoo settings for test_website. Configure related models, access rights, and options as needed.
|
||||
17
odoo-bringout-oca-ocb-test_website/doc/CONTROLLERS.md
Normal file
17
odoo-bringout-oca-ocb-test_website/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.
|
||||
7
odoo-bringout-oca-ocb-test_website/doc/DEPENDENCIES.md
Normal file
7
odoo-bringout-oca-ocb-test_website/doc/DEPENDENCIES.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Dependencies
|
||||
|
||||
This addon depends on:
|
||||
|
||||
- [web_unsplash](../../odoo-bringout-oca-ocb-web_unsplash)
|
||||
- [website](../../odoo-bringout-oca-ocb-website)
|
||||
- [theme_default](../../odoo-bringout-oca-ocb-theme_default)
|
||||
4
odoo-bringout-oca-ocb-test_website/doc/FAQ.md
Normal file
4
odoo-bringout-oca-ocb-test_website/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 test_website or install in UI.
|
||||
7
odoo-bringout-oca-ocb-test_website/doc/INSTALL.md
Normal file
7
odoo-bringout-oca-ocb-test_website/doc/INSTALL.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Install
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-ocb-test_website"
|
||||
# or
|
||||
uv pip install odoo-bringout-oca-ocb-test_website"
|
||||
```
|
||||
15
odoo-bringout-oca-ocb-test_website/doc/MODELS.md
Normal file
15
odoo-bringout-oca-ocb-test_website/doc/MODELS.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Models
|
||||
|
||||
Detected core models and extensions in test_website.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class test_model
|
||||
class test_model_multi_website
|
||||
class res_config_settings
|
||||
class website
|
||||
```
|
||||
|
||||
Notes
|
||||
- Classes show model technical names; fields omitted for brevity.
|
||||
- Items listed under _inherit are extensions of existing models.
|
||||
6
odoo-bringout-oca-ocb-test_website/doc/OVERVIEW.md
Normal file
6
odoo-bringout-oca-ocb-test_website/doc/OVERVIEW.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Overview
|
||||
|
||||
Packaged Odoo addon: test_website. Provides features documented in upstream Odoo 16 under this addon.
|
||||
|
||||
- Source: OCA/OCB 16.0, addon test_website
|
||||
- License: LGPL-3
|
||||
3
odoo-bringout-oca-ocb-test_website/doc/REPORTS.md
Normal file
3
odoo-bringout-oca-ocb-test_website/doc/REPORTS.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Reports
|
||||
|
||||
This module does not define custom reports.
|
||||
42
odoo-bringout-oca-ocb-test_website/doc/SECURITY.md
Normal file
42
odoo-bringout-oca-ocb-test_website/doc/SECURITY.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Security
|
||||
|
||||
Access control and security definitions in test_website.
|
||||
|
||||
## Access Control Lists (ACLs)
|
||||
|
||||
Model access permissions defined in:
|
||||
- **[ir.model.access.csv](../test_website/security/ir.model.access.csv)**
|
||||
- 3 model access rules
|
||||
|
||||
## Record Rules
|
||||
|
||||
Row-level security rules defined in:
|
||||
|
||||
## Security Groups & Configuration
|
||||
|
||||
Security groups and permissions defined in:
|
||||
- **[test_website_security.xml](../test_website/security/test_website_security.xml)**
|
||||
- 1 security groups defined
|
||||
|
||||
```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](../test_website/security/ir.model.access.csv)**
|
||||
- Model access permissions (CRUD rights)
|
||||
- **[test_website_security.xml](../test_website/security/test_website_security.xml)**
|
||||
- Security groups, categories, and XML-based rules
|
||||
|
||||
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
|
||||
|
|
@ -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-test_website/doc/USAGE.md
Normal file
7
odoo-bringout-oca-ocb-test_website/doc/USAGE.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Usage
|
||||
|
||||
Start Odoo including this addon (from repo root):
|
||||
|
||||
```bash
|
||||
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon test_website
|
||||
```
|
||||
3
odoo-bringout-oca-ocb-test_website/doc/WIZARDS.md
Normal file
3
odoo-bringout-oca-ocb-test_website/doc/WIZARDS.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Wizards
|
||||
|
||||
This module does not include UI wizards.
|
||||
44
odoo-bringout-oca-ocb-test_website/pyproject.toml
Normal file
44
odoo-bringout-oca-ocb-test_website/pyproject.toml
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
[project]
|
||||
name = "odoo-bringout-oca-ocb-test_website"
|
||||
version = "16.0.0"
|
||||
description = "Website Test - Website Test, mainly for module install/uninstall tests"
|
||||
authors = [
|
||||
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
|
||||
]
|
||||
dependencies = [
|
||||
"odoo-bringout-oca-ocb-web_unsplash>=16.0.0",
|
||||
"odoo-bringout-oca-ocb-website>=16.0.0",
|
||||
"odoo-bringout-oca-ocb-theme_default>=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/0"
|
||||
repository = "https://github.com/bringout/0"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.metadata]
|
||||
allow-direct-references = true
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["test_website"]
|
||||
|
||||
[tool.rye]
|
||||
managed = true
|
||||
dev-dependencies = [
|
||||
"pytest>=8.4.1",
|
||||
]
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Website Test',
|
||||
'version': '1.0',
|
||||
'category': 'Hidden',
|
||||
'sequence': 9876,
|
||||
'summary': 'Website Test, mainly for module install/uninstall tests',
|
||||
'description': """This module contains tests related to website. Those are
|
||||
present in a separate module as we are testing module install/uninstall/upgrade
|
||||
and we don't want to reload the website module every time, including it's possible
|
||||
dependencies. Neither we want to add in website module some routes, views and
|
||||
models which only purpose is to run tests.""",
|
||||
'depends': [
|
||||
'web_unsplash',
|
||||
'website',
|
||||
'theme_default',
|
||||
],
|
||||
'demo': [
|
||||
'data/test_website_demo.xml',
|
||||
],
|
||||
'data': [
|
||||
'views/templates.xml',
|
||||
'views/test_model_multi_website_views.xml',
|
||||
'views/test_model_views.xml',
|
||||
'data/test_website_data.xml',
|
||||
'security/test_website_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
'installable': True,
|
||||
'assets': {
|
||||
'web.assets_frontend': [
|
||||
'test_website/static/src/js/test_error.js',
|
||||
],
|
||||
'web.assets_tests': [
|
||||
'test_website/static/tests/tours/*',
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
'test_website/static/tests/*.js',
|
||||
],
|
||||
},
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import main
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
import werkzeug
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.addons.portal.controllers.web import Home
|
||||
from odoo.exceptions import UserError, ValidationError, AccessError, MissingError, AccessDenied
|
||||
|
||||
|
||||
class WebsiteTest(Home):
|
||||
|
||||
@http.route('/test_view', type='http', auth='public', website=True, sitemap=False)
|
||||
def test_view(self, **kwargs):
|
||||
return request.render('test_website.test_view')
|
||||
|
||||
@http.route('/ignore_args/converteronly/<string:a>', type='http', auth="public", website=True, sitemap=False)
|
||||
def test_ignore_args_converter_only(self, a):
|
||||
return request.make_response(json.dumps(dict(a=a, kw=None)))
|
||||
|
||||
@http.route('/ignore_args/none', type='http', auth="public", website=True, sitemap=False)
|
||||
def test_ignore_args_none(self):
|
||||
return request.make_response(json.dumps(dict(a=None, kw=None)))
|
||||
|
||||
@http.route('/ignore_args/a', type='http', auth="public", website=True, sitemap=False)
|
||||
def test_ignore_args_a(self, a):
|
||||
return request.make_response(json.dumps(dict(a=a, kw=None)))
|
||||
|
||||
@http.route('/ignore_args/kw', type='http', auth="public", website=True, sitemap=False)
|
||||
def test_ignore_args_kw(self, a, **kw):
|
||||
return request.make_response(json.dumps(dict(a=a, kw=kw)))
|
||||
|
||||
@http.route('/ignore_args/converter/<string:a>', type='http', auth="public", website=True, sitemap=False)
|
||||
def test_ignore_args_converter(self, a, b='youhou', **kw):
|
||||
return request.make_response(json.dumps(dict(a=a, b=b, kw=kw)))
|
||||
|
||||
@http.route('/ignore_args/converter/<string:a>/nokw', type='http', auth="public", website=True, sitemap=False)
|
||||
def test_ignore_args_converter_nokw(self, a, b='youhou'):
|
||||
return request.make_response(json.dumps(dict(a=a, b=b)))
|
||||
|
||||
@http.route('/multi_company_website', type='http', auth="public", website=True, sitemap=False)
|
||||
def test_company_context(self):
|
||||
return request.make_response(json.dumps(request.context.get('allowed_company_ids')))
|
||||
|
||||
@http.route('/test_lang_url/<model("res.country"):country>', type='http', auth='public', website=True, sitemap=False)
|
||||
def test_lang_url(self, **kwargs):
|
||||
return request.render('test_website.test_view')
|
||||
|
||||
# Test Session
|
||||
|
||||
@http.route('/test_get_dbname', type='json', auth='public', website=True, sitemap=False)
|
||||
def test_get_dbname(self, **kwargs):
|
||||
return request.env.cr.dbname
|
||||
|
||||
# Test Error
|
||||
|
||||
@http.route('/test_error_view', type='http', auth='public', website=True, sitemap=False)
|
||||
def test_error_view(self, **kwargs):
|
||||
return request.render('test_website.test_error_view')
|
||||
|
||||
@http.route('/test_user_error_http', type='http', auth='public', website=True, sitemap=False)
|
||||
def test_user_error_http(self, **kwargs):
|
||||
raise UserError("This is a user http test")
|
||||
|
||||
@http.route('/test_user_error_json', type='json', auth='public', website=True, sitemap=False)
|
||||
def test_user_error_json(self, **kwargs):
|
||||
raise UserError("This is a user rpc test")
|
||||
|
||||
@http.route('/test_validation_error_http', type='http', auth='public', website=True, sitemap=False)
|
||||
def test_validation_error_http(self, **kwargs):
|
||||
raise ValidationError("This is a validation http test")
|
||||
|
||||
@http.route('/test_validation_error_json', type='json', auth='public', website=True, sitemap=False)
|
||||
def test_validation_error_json(self, **kwargs):
|
||||
raise ValidationError("This is a validation rpc test")
|
||||
|
||||
@http.route('/test_access_error_json', type='json', auth='public', website=True, sitemap=False)
|
||||
def test_access_error_json(self, **kwargs):
|
||||
raise AccessError("This is an access rpc test")
|
||||
|
||||
@http.route('/test_access_error_http', type='http', auth='public', website=True, sitemap=False)
|
||||
def test_access_error_http(self, **kwargs):
|
||||
raise AccessError("This is an access http test")
|
||||
|
||||
@http.route('/test_missing_error_json', type='json', auth='public', website=True, sitemap=False)
|
||||
def test_missing_error_json(self, **kwargs):
|
||||
raise MissingError("This is a missing rpc test")
|
||||
|
||||
@http.route('/test_missing_error_http', type='http', auth='public', website=True, sitemap=False)
|
||||
def test_missing_error_http(self, **kwargs):
|
||||
raise MissingError("This is a missing http test")
|
||||
|
||||
@http.route('/test_internal_error_json', type='json', auth='public', website=True, sitemap=False)
|
||||
def test_internal_error_json(self, **kwargs):
|
||||
raise werkzeug.exceptions.InternalServerError()
|
||||
|
||||
@http.route('/test_internal_error_http', type='http', auth='public', website=True, sitemap=False)
|
||||
def test_internal_error_http(self, **kwargs):
|
||||
raise werkzeug.exceptions.InternalServerError()
|
||||
|
||||
@http.route('/test_access_denied_json', type='json', auth='public', website=True, sitemap=False)
|
||||
def test_denied_error_json(self, **kwargs):
|
||||
raise AccessDenied("This is an access denied rpc test")
|
||||
|
||||
@http.route('/test_access_denied_http', type='http', auth='public', website=True, sitemap=False)
|
||||
def test_denied_error_http(self, **kwargs):
|
||||
raise AccessDenied("This is an access denied http test")
|
||||
|
||||
@http.route(['/get'], type='http', auth="public", methods=['GET'], website=True, sitemap=False)
|
||||
def get_method(self, **kw):
|
||||
return request.make_response('get')
|
||||
|
||||
@http.route(['/post'], type='http', auth="public", methods=['POST'], website=True, sitemap=False)
|
||||
def post_method(self, **kw):
|
||||
return request.make_response('post')
|
||||
|
||||
@http.route(['/get_post'], type='http', auth="public", methods=['GET', 'POST'], website=True, sitemap=False)
|
||||
def get_post_method(self, **kw):
|
||||
return request.make_response('get_post')
|
||||
|
||||
@http.route(['/get_post_nomultilang'], type='http', auth="public", methods=['GET', 'POST'], website=True, multilang=False, sitemap=False)
|
||||
def get_post_method_no_multilang(self, **kw):
|
||||
return request.make_response('get_post_nomultilang')
|
||||
|
||||
# Test Perfs
|
||||
|
||||
@http.route(['/empty_controller_test'], type='http', auth='public', website=True, multilang=False, sitemap=False)
|
||||
def empty_controller_test(self, **kw):
|
||||
return 'Basic Controller Content'
|
||||
|
||||
# Test Redirects
|
||||
@http.route(['/test_website/country/<model("res.country"):country>'], type='http', auth="public", website=True, sitemap=False)
|
||||
def test_model_converter_country(self, country, **kw):
|
||||
return request.render('test_website.test_redirect_view', {'country': country})
|
||||
|
||||
@http.route(['/test_website/200/<model("test.model"):rec>'], type='http', auth="public", website=True, sitemap=False)
|
||||
def test_model_converter_seoname(self, rec, **kw):
|
||||
return request.make_response('ok')
|
||||
|
||||
@http.route(['/test_website/model_item/<int:record_id>'], type='http', methods=['GET'], auth="public", website=True, sitemap=False)
|
||||
def test_model_item(self, record_id):
|
||||
values = {
|
||||
'record': request.env['test.model'].sudo().browse(record_id),
|
||||
}
|
||||
return request.render("test_website.model_item", values)
|
||||
|
||||
@http.route(['/test_website/test_redirect_view_qs'], type='http', auth="public", website=True, sitemap=False)
|
||||
def test_redirect_view_qs(self, **kw):
|
||||
return request.render('test_website.test_redirect_view_qs')
|
||||
|
||||
@http.route([
|
||||
'/test_countries_308',
|
||||
'/test_countries_308/<model("test.model"):rec>',
|
||||
], type='http', auth='public', website=True, sitemap=False)
|
||||
def test_countries_308(self, **kwargs):
|
||||
return request.make_response('ok')
|
||||
|
||||
# Test Sitemap
|
||||
def sitemap_test(env, rule, qs):
|
||||
if not qs or qs.lower() in '/test_website_sitemap':
|
||||
yield {'loc': '/test_website_sitemap'}
|
||||
|
||||
@http.route([
|
||||
'/test_website_sitemap',
|
||||
'/test_website_sitemap/something/<model("test.model"):rec>',
|
||||
], type='http', auth='public', website=True, sitemap=sitemap_test)
|
||||
def test_sitemap(self, rec=None, **kwargs):
|
||||
return request.make_response('Sitemap Testing Page')
|
||||
|
||||
@http.route('/test_model/<model("test.model"):test_model>', type='http', auth='public', website=True, sitemap=False)
|
||||
def test_model(self, test_model, **kwargs):
|
||||
return request.render('test_website.test_model_page_layout', {'main_object': test_model, 'test_model': test_model})
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="test_model_publish" model="ir.rule">
|
||||
<field name="name">Public user: read only website published</field>
|
||||
<field name="model_id" ref="test_website.model_test_model"/>
|
||||
<field name="groups" eval="[(4, ref('base.group_public'))]"/>
|
||||
<field name="domain_force">[('website_published','=', True)]</field>
|
||||
<field name="perm_read" eval="True"/>
|
||||
</record>
|
||||
|
||||
<!-- SOME DEFAULT TEST.MODEL RECORDS WITH DIFFERENT WEBSITE_ID -->
|
||||
<record id="test_model_generic" model="test.model">
|
||||
<field name="name">Test Model</field>
|
||||
</record>
|
||||
<record id="test_model_multi_generic" model="test.model.multi.website">
|
||||
<field name="name">Test Multi Model Generic</field>
|
||||
</record>
|
||||
<record id="test_model_multi_website_1" model="test.model.multi.website">
|
||||
<field name="name">Test Multi Model Website 1</field>
|
||||
<field name="website_id" ref="website.default_website"/>
|
||||
</record>
|
||||
|
||||
<!-- RECORDS FOR RESET VIEWS TESTS -->
|
||||
<record id="test_view" model="ir.ui.view">
|
||||
<field name="name">Test View</field>
|
||||
<field name="type">qweb</field>
|
||||
<field name="key">test_website.test_view</field>
|
||||
<field name="arch" type="xml">
|
||||
<t name="Test View" priority="29" t-name="test_website.test_view">
|
||||
<t t-call="website.layout">
|
||||
<p>Test View</p>
|
||||
<p>placeholder</p>
|
||||
</t>
|
||||
</t>
|
||||
</field>
|
||||
</record>
|
||||
<record id="test_page_view" model="ir.ui.view">
|
||||
<field name="name">Test Page View</field>
|
||||
<field name="type">qweb</field>
|
||||
<field name="key">test_website.test_page_view</field>
|
||||
<field name="arch" type="xml">
|
||||
<t name="Test Page View" priority="29" t-name="test_website.test_page_view">
|
||||
<t t-call="website.layout">
|
||||
<div id="oe_structure_test_website_page" class="oe_structure oe_empty"/>
|
||||
<p>Test Page View</p>
|
||||
<p>placeholder</p>
|
||||
</t>
|
||||
</t>
|
||||
</field>
|
||||
</record>
|
||||
<record id="test_error_view" model="ir.ui.view">
|
||||
<field name="name">Test Error View</field>
|
||||
<field name="type">qweb</field>
|
||||
<field name="key">test_website.test_error_view</field>
|
||||
<field name="arch" type="xml">
|
||||
<t name="Test Error View" t-name="test_website.test_error_view">
|
||||
<t t-call="website.layout">
|
||||
<div class="container">
|
||||
<h1>Test Error View</h1>
|
||||
<div class="row">
|
||||
<ul class="list-group http_error col-6">
|
||||
<li class="list-group-item list-group-item-primary"><h2>http Errors</h2></li>
|
||||
<li class="list-group-item"><a href="/test_user_error_http">http UserError (400)</a></li>
|
||||
<li class="list-group-item"><a href="/test_validation_error_http">http ValidationError (400)</a></li>
|
||||
<li class="list-group-item"><a href="/test_missing_error_http">http MissingError (400)</a></li>
|
||||
<li class="list-group-item"><a href="/test_access_error_http">http AccessError (403)</a></li>
|
||||
<li class="list-group-item"><a href="/test_access_denied_http">http AccessDenied (403)</a></li>
|
||||
<li class="list-group-item"><a href="/test_internal_error_http">http InternalServerError (500)</a></li>
|
||||
<li class="list-group-item"><a href="/test_not_found_http">http NotFound (404)</a></li>
|
||||
</ul>
|
||||
<ul class="list-group rpc_error col-6">
|
||||
<li class="list-group-item list-group-item-primary"><h2>rpc Warnings</h2></li>
|
||||
<li class="list-group-item"><a href="/test_user_error_json">rpc UserError</a></li>
|
||||
<li class="list-group-item"><a href="/test_validation_error_json">rpc ValidationError</a></li>
|
||||
<li class="list-group-item"><a href="/test_missing_error_json">rpc MissingError</a></li>
|
||||
<li class="list-group-item"><a href="/test_access_error_json">rpc AccessError</a></li>
|
||||
<li class="list-group-item"><a href="/test_access_denied_json">rpc AccessDenied</a></li>
|
||||
<li class="list-group-item list-group-item-primary"><h2>rpc Errors</h2></li>
|
||||
<li class="list-group-item"><a href="/test_internal_error_json">rpc InternalServerError</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</field>
|
||||
</record>
|
||||
<record id="test_page" model="website.page">
|
||||
<field name="is_published">True</field>
|
||||
<field name="url">/test_page_view</field>
|
||||
<field name="view_id" ref="test_page_view"/>
|
||||
<field name="website_indexed" eval="False"/>
|
||||
</record>
|
||||
<record id="test_view_to_be_t_called" model="ir.ui.view">
|
||||
<field name="name">Test View To Be t-called</field>
|
||||
<field name="type">qweb</field>
|
||||
<field name="key">test_website.test_view_to_be_t_called</field>
|
||||
<field name="arch" type="xml">
|
||||
<t name="Test View To Be t-called" priority="29" t-name="test_website.test_view_to_be_t_called">
|
||||
<p>Test View To Be t-called</p>
|
||||
<p>placeholder</p>
|
||||
</t>
|
||||
</field>
|
||||
</record>
|
||||
<template id="test_view_child_broken" inherit_id="test_website.test_view" active="False">
|
||||
<xpath expr="//p[last()]" position="replace">
|
||||
<p>Test View Child Broken</p>
|
||||
<p>placeholder</p>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- RECORDS FOR MODULE OPERATION TESTS -->
|
||||
<template id="update_module_base_view">
|
||||
<div>I am a base view</div>
|
||||
</template>
|
||||
|
||||
<!-- RECORDS FOR REDIRECT TESTS -->
|
||||
<template id="test_redirect_view">
|
||||
<t t-esc="country.name"/>
|
||||
<t t-if="not request.env.user._is_public()" t-esc="'Logged In'"/>
|
||||
<!-- `href` is send through `url_for` for non editor users -->
|
||||
<a href="/test_website/country/andorra-1">I am a link</a>
|
||||
</template>
|
||||
<template id="test_redirect_view_qs">
|
||||
<a href="/empty_controller_test?a=a">Home</a>
|
||||
</template>
|
||||
|
||||
<record id="test_image_progress" model="website.page">
|
||||
<field name="name">Test Image Progress</field>
|
||||
<field name="url">/test_image_progress</field>
|
||||
<field name="type">qweb</field>
|
||||
<field name="key">test_website.test_image_progress</field>
|
||||
<field name="arch" type="xml">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap" class="oe_structure oe_empty"/>
|
||||
</t>
|
||||
</field>
|
||||
<field name="website_indexed" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Test model record -->
|
||||
<record id="test_model_record" model="test.model">
|
||||
<field name="name">Test Model Record</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="test_model_multi_website_2" model="test.model.multi.website">
|
||||
<field name="name">Test Model Multi Website 2</field>
|
||||
<field name="website_id" ref="website.website2"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from . import model
|
||||
from . import res_config_settings
|
||||
from . import website
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class TestModel(models.Model):
|
||||
_name = 'test.model'
|
||||
_inherit = [
|
||||
'website.seo.metadata',
|
||||
'website.published.mixin',
|
||||
'website.searchable.mixin',
|
||||
]
|
||||
_description = 'Website Model Test'
|
||||
|
||||
name = fields.Char(required=1)
|
||||
|
||||
@api.model
|
||||
def _search_get_detail(self, website, order, options):
|
||||
return {
|
||||
'model': 'test.model',
|
||||
'base_domain': [],
|
||||
'search_fields': ['name'],
|
||||
'fetch_fields': ['name'],
|
||||
'mapping': {
|
||||
'name': {'name': 'name', 'type': 'text', 'match': True},
|
||||
'website_url': {'name': 'name', 'type': 'text', 'truncate': False},
|
||||
},
|
||||
'icon': 'fa-check-square-o',
|
||||
'order': 'name asc, id desc',
|
||||
}
|
||||
|
||||
def open_website_url(self):
|
||||
self.ensure_one()
|
||||
return self.env['website'].get_client_action(f'/test_model/{self.id}')
|
||||
|
||||
|
||||
class TestModelMultiWebsite(models.Model):
|
||||
_name = 'test.model.multi.website'
|
||||
_inherit = [
|
||||
'website.published.multi.mixin',
|
||||
]
|
||||
_description = 'Multi Website Model Test'
|
||||
|
||||
name = fields.Char(required=1)
|
||||
# `cascade` is needed as there is demo data for this model which are bound
|
||||
# to website 2 (demo website). But some tests are unlinking the website 2,
|
||||
# which would fail if the `cascade` is not set. Note that the website 2 is
|
||||
# never set on any records in all other modules.
|
||||
website_id = fields.Many2one('website', string='Website', ondelete='cascade')
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
def action_website_test_setting(self):
|
||||
return self.env['website'].get_client_action('/')
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class Website(models.Model):
|
||||
_inherit = "website"
|
||||
|
||||
def _search_get_details(self, search_type, order, options):
|
||||
result = super()._search_get_details(search_type, order, options)
|
||||
if search_type in ['test']:
|
||||
result.append(self.env['test.model']._search_get_detail(self, order, options))
|
||||
return result
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_test_model,access_test_model,model_test_model,,1,0,0,0
|
||||
access_test_model_multi_website,access_test_model_multi_website,model_test_model_multi_website,,1,0,0,0
|
||||
access_test_model_tester,access_test_model,model_test_model,test_website.group_test_website_tester,1,1,1,1
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record model="ir.module.category" id="test_website.module_category_test_website">
|
||||
<field name="name">Tests about Website with additional model</field>
|
||||
<field name="sequence">24</field>
|
||||
</record>
|
||||
|
||||
<record id="group_test_website_tester" model="res.groups">
|
||||
<field name="name">Tester</field>
|
||||
<field name="category_id" ref="test_website.module_category_test_website"/>
|
||||
</record>
|
||||
|
||||
<record id="base.user_admin" model="res.users">
|
||||
<field name="groups_id" eval="[(4, ref('test_website.group_test_website_tester'))]"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
odoo.define('website_forum.test_error', function (require) {
|
||||
'use strict';
|
||||
|
||||
var publicWidget = require('web.public.widget');
|
||||
|
||||
publicWidget.registry.testError = publicWidget.Widget.extend({
|
||||
selector: '.rpc_error',
|
||||
events: {
|
||||
'click a': '_onRpcErrorClick',
|
||||
},
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Handlers
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* make a rpc call with the href of the DOM element clicked
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_onRpcErrorClick: function (ev) {
|
||||
ev.preventDefault();
|
||||
var $link = $(ev.currentTarget);
|
||||
return this._rpc({
|
||||
route: $link.attr('href'),
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { FileSelectorControlPanel } from '@web_editor/components/media_dialog/file_selector';
|
||||
import { getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { HtmlField } from '@web_editor/js/backend/html_field';
|
||||
import {registry} from '@web/core/registry';
|
||||
import testUtils from 'web.test_utils';
|
||||
import { uploadService } from '@web_editor/components/upload_progress_toast/upload_service';
|
||||
import { unsplashService } from '@web_unsplash/services/unsplash_service';
|
||||
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
|
||||
import weTestUtils from 'web_editor.test_utils';
|
||||
import Wysiwyg from 'web_editor.wysiwyg';
|
||||
|
||||
const { useEffect } = owl;
|
||||
|
||||
QUnit.module('field html file upload', {
|
||||
beforeEach: function () {
|
||||
this.data = weTestUtils.wysiwygData({
|
||||
'mail.compose.message': {
|
||||
fields: {
|
||||
display_name: {
|
||||
string: "Displayed name",
|
||||
type: "char"
|
||||
},
|
||||
body: {
|
||||
string: "Message Body inline (to send)",
|
||||
type: "html"
|
||||
},
|
||||
attachment_ids: {
|
||||
string: "Attachments",
|
||||
type: "many2many",
|
||||
relation: "ir.attachment",
|
||||
}
|
||||
},
|
||||
records: [{
|
||||
id: 1,
|
||||
display_name: "Some Composer",
|
||||
body: "Hello",
|
||||
attachment_ids: [],
|
||||
}],
|
||||
},
|
||||
});
|
||||
},
|
||||
}, function () {
|
||||
QUnit.test('media dialog: upload', async function (assert) {
|
||||
assert.expect(4);
|
||||
const onAttachmentChangeTriggered = testUtils.makeTestPromise();
|
||||
patchWithCleanup(HtmlField.prototype, {
|
||||
'_onAttachmentChange': function (event) {
|
||||
this._super(event);
|
||||
onAttachmentChangeTriggered.resolve(true);
|
||||
}
|
||||
});
|
||||
const defFileSelector = testUtils.makeTestPromise();
|
||||
const onChangeTriggered = testUtils.makeTestPromise();
|
||||
patchWithCleanup(FileSelectorControlPanel.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
useEffect(() => {
|
||||
defFileSelector.resolve(true);
|
||||
}, () => []);
|
||||
},
|
||||
async onChangeFileInput() {
|
||||
this._super();
|
||||
onChangeTriggered.resolve(true);
|
||||
}
|
||||
});
|
||||
// create and load form view
|
||||
const serviceRegistry = registry.category("services");
|
||||
serviceRegistry.add("upload", uploadService);
|
||||
serviceRegistry.add("unsplash", unsplashService);
|
||||
const serverData = {
|
||||
models: this.data,
|
||||
};
|
||||
serverData.actions = {
|
||||
1: {
|
||||
id: 1,
|
||||
name: "test",
|
||||
res_model: "mail.compose.message",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "form"]],
|
||||
},
|
||||
};
|
||||
serverData.views = {
|
||||
"mail.compose.message,false,search": "<search></search>",
|
||||
"mail.compose.message,false,form": `
|
||||
<form>
|
||||
<field name="body" type="html"/>
|
||||
<field name="attachment_ids" widget="many2many_binary"/>
|
||||
</form>`,
|
||||
};
|
||||
const mockRPC = (route, args) => {
|
||||
if (route === "/web_editor/attachment/add_data") {
|
||||
return Promise.resolve({"id": 5, "name": "test.jpg", "description": false, "mimetype": "image/jpeg", "checksum": "7951a43bbfb08fd742224ada280913d1897b89ab",
|
||||
"url": false, "type": "binary", "res_id": 1, "res_model": "note.note", "public": false, "access_token": false,
|
||||
"image_src": "/web/image/1-a0e63e61/test.jpg", "image_width": 1, "image_height": 1, "original_id": false
|
||||
});
|
||||
}
|
||||
else if (route === "/web/dataset/call_kw/ir.attachment/generate_access_token") {
|
||||
return Promise.resolve(["129a52e1-6bf2-470a-830e-8e368b022e13"]);
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 1);
|
||||
//trigger wysiwyg mediadialog
|
||||
const fixture = getFixture();
|
||||
const formField = fixture.querySelector('.o_field_html[name="body"]');
|
||||
const textInput = formField.querySelector('.note-editable p');
|
||||
textInput.innerText = "test";
|
||||
const pText = $(textInput).contents()[0];
|
||||
Wysiwyg.setRange(pText, 1, pText, 2);
|
||||
await new Promise((resolve) => setTimeout(resolve)); //ensure fully set up
|
||||
const wysiwyg = $(textInput.parentElement).data('wysiwyg');
|
||||
wysiwyg.openMediaDialog();
|
||||
assert.ok(await Promise.race([defFileSelector, new Promise((res, _) => setTimeout(() => res(false), 400))]), "File Selector did not mount");
|
||||
// upload test
|
||||
const fileInputs = document.querySelectorAll(".o_select_media_dialog input.d-none.o_file_input");
|
||||
const fileB64 = '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iiigD//2Q==';
|
||||
const fileBytes = new Uint8Array(atob(fileB64).split('').map(char => char.charCodeAt(0)));
|
||||
// redefine 'files' so we can put mock data in through js
|
||||
fileInputs.forEach((input) => Object.defineProperty(input, 'files', {
|
||||
value: [new File(fileBytes, "test.jpg", { type: 'image/jpeg' })],
|
||||
}));
|
||||
fileInputs.forEach(input => {
|
||||
input.dispatchEvent(new Event('change', {}));
|
||||
});
|
||||
|
||||
assert.ok(await Promise.race([onChangeTriggered, new Promise((res, _) => setTimeout(() => res(false), 400))]),
|
||||
"File change event was not triggered");
|
||||
assert.ok(await Promise.race([onAttachmentChangeTriggered, new Promise((res, _) => setTimeout(() => res(false), 400))]),
|
||||
"_onAttachmentChange was not called with the new attachment, necessary for unsused upload cleanup on backend");
|
||||
|
||||
// wait to check that dom is properly updated
|
||||
await new Promise((res, _) => setTimeout(() => res(false), 400));
|
||||
assert.ok(fixture.querySelector('.o_attachment[title="test.jpg"]'));
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
odoo.define('test_website.custom_snippets', function (require) {
|
||||
'use strict';
|
||||
|
||||
const wTourUtils = require('website.tour_utils');
|
||||
|
||||
/**
|
||||
* The purpose of this tour is to check the custom snippets flow:
|
||||
*
|
||||
* -> go to edit mode
|
||||
* -> drag a banner into page content
|
||||
* -> customize banner (set text)
|
||||
* -> save banner as custom snippet
|
||||
* -> confirm save
|
||||
* -> ensure custom snippet is available
|
||||
* -> drag custom snippet
|
||||
* -> ensure block appears as banner
|
||||
* -> ensure block appears as custom banner
|
||||
* -> rename custom banner
|
||||
* -> verify rename took effect
|
||||
* -> delete custom snippet
|
||||
* -> confirm delete
|
||||
* -> ensure it was deleted
|
||||
*/
|
||||
|
||||
wTourUtils.registerWebsitePreviewTour('test_custom_snippet', {
|
||||
url: '/',
|
||||
edition: true,
|
||||
test: true,
|
||||
}, [
|
||||
{
|
||||
content: "drop a snippet",
|
||||
trigger: ".oe_snippet[name='Banner'] .oe_snippet_thumbnail:not(.o_we_already_dragging)",
|
||||
moveTrigger: ".oe_drop_zone",
|
||||
run: "drag_and_drop iframe #wrap",
|
||||
},
|
||||
{
|
||||
content: "customize snippet",
|
||||
trigger: "iframe #wrapwrap .s_banner h1",
|
||||
run: "text",
|
||||
consumeEvent: "input",
|
||||
},
|
||||
{
|
||||
content: "save custom snippet",
|
||||
trigger: ".snippet-option-SnippetSave we-button",
|
||||
},
|
||||
{
|
||||
content: "confirm reload",
|
||||
trigger: ".modal-dialog button span:contains('Save and Reload')",
|
||||
},
|
||||
{
|
||||
content: "ensure custom snippet appeared",
|
||||
trigger: "#oe_snippets.o_loaded .oe_snippet[name='Custom Banner']",
|
||||
run: function () {
|
||||
$("#oe_snippets .oe_snippet[name='Custom Banner'] .o_rename_btn").attr("style", "display: block;");
|
||||
// hover is needed for rename button to appear
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "rename custom snippet",
|
||||
trigger: ".oe_snippet[name='Custom Banner'] we-button.o_rename_btn",
|
||||
extra_trigger: ".oe_snippet[name='Custom Banner'] .oe_snippet_thumbnail:not(.o_we_already_dragging)",
|
||||
},
|
||||
{
|
||||
content: "set name",
|
||||
trigger: ".oe_snippet[name='Custom Banner'] input",
|
||||
run: "text Bruce Banner",
|
||||
},
|
||||
{
|
||||
content: "confirm rename",
|
||||
trigger: ".oe_snippet[name='Custom Banner'] we-button.o_we_confirm_btn",
|
||||
},
|
||||
{
|
||||
content: "drop custom snippet",
|
||||
trigger: ".oe_snippet[name='Bruce Banner'] .oe_snippet_thumbnail:not(.o_we_already_dragging)",
|
||||
extra_trigger: "iframe body.editor_enable",
|
||||
moveTrigger: ".oe_drop_zone",
|
||||
run: "drag_and_drop iframe #wrap",
|
||||
},
|
||||
{
|
||||
content: "ensure banner section exists",
|
||||
trigger: "iframe #wrap section[data-name='Banner']",
|
||||
run: function () {}, // check
|
||||
},
|
||||
{
|
||||
content: "ensure custom banner section exists",
|
||||
trigger: "iframe #wrap section[data-name='Bruce Banner']",
|
||||
run: function () {
|
||||
$("#oe_snippets .oe_snippet[name='Bruce Banner'] .o_delete_btn").attr("style", "display: block;");
|
||||
// hover is needed for delete button to appear
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "delete custom snippet",
|
||||
trigger: ".oe_snippet[name='Bruce Banner'] we-button.o_delete_btn",
|
||||
extra_trigger: ".oe_snippet[name='Bruce Banner'] .oe_snippet_thumbnail:not(.o_we_already_dragging)",
|
||||
},
|
||||
{
|
||||
content: "confirm delete",
|
||||
trigger: ".modal-dialog button:has(span:contains('Yes'))",
|
||||
},
|
||||
{
|
||||
content: "ensure custom snippet disappeared",
|
||||
trigger: "#oe_snippets:not(:has(.oe_snippet[name='Bruce Banner']))",
|
||||
run: function () {}, // check
|
||||
},
|
||||
]);
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
odoo.define('test_website.error_views', function (require) {
|
||||
'use strict';
|
||||
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
tour.register('test_error_website', {
|
||||
test: true,
|
||||
url: '/test_error_view',
|
||||
},
|
||||
[
|
||||
// RPC ERROR
|
||||
{
|
||||
content: "trigger rpc user error",
|
||||
trigger: 'a[href="/test_user_error_json"]',
|
||||
}, {
|
||||
content: "rpc user error modal has message",
|
||||
extra_trigger: 'div.o_notification_content:contains("This is a user rpc test")',
|
||||
trigger: 'button.o_notification_close',
|
||||
}, {
|
||||
content: "trigger rpc access error",
|
||||
trigger: 'a[href="/test_access_error_json"]',
|
||||
}, {
|
||||
content: "rpc access error modal has message",
|
||||
extra_trigger: 'div.o_notification_content:contains("This is an access rpc test")',
|
||||
trigger: 'button.o_notification_close',
|
||||
}, {
|
||||
content: "trigger validation rpc error",
|
||||
trigger: 'a[href="/test_validation_error_json"]',
|
||||
}, {
|
||||
content: "rpc validation error modal has message",
|
||||
extra_trigger: 'div.o_notification_content:contains("This is a validation rpc test")',
|
||||
trigger: 'button.o_notification_close',
|
||||
}, {
|
||||
content: "trigger rpc missing error",
|
||||
trigger: 'a[href="/test_missing_error_json"]',
|
||||
}, {
|
||||
content: "rpc missing error modal has message",
|
||||
extra_trigger: 'div.o_notification_content:contains("This is a missing rpc test")',
|
||||
trigger: 'button.o_notification_close',
|
||||
}, {
|
||||
content: "trigger rpc error 403",
|
||||
trigger: 'a[href="/test_access_denied_json"]',
|
||||
}, {
|
||||
content: "rpc error 403 modal has message",
|
||||
extra_trigger: 'div.o_notification_content:contains("This is an access denied rpc test")',
|
||||
trigger: 'button.o_notification_close',
|
||||
}, {
|
||||
content: "trigger rpc error 500",
|
||||
trigger: 'a[href="/test_internal_error_json"]',
|
||||
}, {
|
||||
content: "rpc error 500 modal is an ErrorDialog",
|
||||
extra_trigger: 'div.o_dialog_error.modal-content div.alert.alert-warning',
|
||||
trigger: '.modal-footer button.btn.btn-primary',
|
||||
},
|
||||
// HTTP ERROR
|
||||
{
|
||||
content: "trigger http user error",
|
||||
trigger: 'body',
|
||||
run: function () {
|
||||
window.location.href = window.location.origin + '/test_user_error_http?debug=0';
|
||||
},
|
||||
}, {
|
||||
content: "http user error page has title and message",
|
||||
extra_trigger: 'h1:contains("Something went wrong.")',
|
||||
trigger: 'div.container pre:contains("This is a user http test")',
|
||||
run: function () {
|
||||
window.location.href = window.location.origin + '/test_user_error_http?debug=1';
|
||||
},
|
||||
}, {
|
||||
content: "http user error page debug has title and message open",
|
||||
extra_trigger: 'h1:contains("Something went wrong.")',
|
||||
trigger: 'div#error_main.collapse.show pre:contains("This is a user http test")',
|
||||
run: function () {},
|
||||
}, {
|
||||
content: "http user error page debug has traceback closed",
|
||||
trigger: 'body:has(div#error_traceback.collapse:not(.show) pre#exception_traceback)',
|
||||
run: function () {
|
||||
window.location.href = window.location.origin + '/test_validation_error_http?debug=0';
|
||||
},
|
||||
}, {
|
||||
content: "http validation error page has title and message",
|
||||
extra_trigger: 'h1:contains("Something went wrong.")',
|
||||
trigger: 'div.container pre:contains("This is a validation http test")',
|
||||
run: function () {
|
||||
window.location.href = window.location.origin + '/test_validation_error_http?debug=1';
|
||||
},
|
||||
}, {
|
||||
content: "http validation error page debug has title and message open",
|
||||
extra_trigger: 'h1:contains("Something went wrong.")',
|
||||
trigger: 'div#error_main.collapse.show pre:contains("This is a validation http test")',
|
||||
run: function () {},
|
||||
}, {
|
||||
content: "http validation error page debug has traceback closed",
|
||||
trigger: 'body:has(div#error_traceback.collapse:not(.show) pre#exception_traceback)',
|
||||
run: function () {
|
||||
window.location.href = window.location.origin + '/test_access_error_http?debug=0';
|
||||
},
|
||||
}, {
|
||||
content: "http access error page has title and message",
|
||||
extra_trigger: 'h1:contains("403: Forbidden")',
|
||||
trigger: 'div.container pre:contains("This is an access http test")',
|
||||
run: function () {
|
||||
window.location.href = window.location.origin + '/test_access_error_http?debug=1';
|
||||
},
|
||||
}, {
|
||||
content: "http access error page debug has title and message open",
|
||||
extra_trigger: 'h1:contains("403: Forbidden")',
|
||||
trigger: 'div#error_main.collapse.show pre:contains("This is an access http test")',
|
||||
run: function () {},
|
||||
}, {
|
||||
content: "http access error page debug has traceback closed",
|
||||
trigger: 'body:has(div#error_traceback.collapse:not(.show) pre#exception_traceback)',
|
||||
run: function () {
|
||||
window.location.href = window.location.origin + '/test_missing_error_http?debug=0';
|
||||
},
|
||||
}, {
|
||||
content: "http missing error page has title and message",
|
||||
extra_trigger: 'h1:contains("Something went wrong.")',
|
||||
trigger: 'div.container pre:contains("This is a missing http test")',
|
||||
run: function () {
|
||||
window.location.href = window.location.origin + '/test_missing_error_http?debug=1';
|
||||
},
|
||||
}, {
|
||||
content: "http missing error page debug has title and message open",
|
||||
extra_trigger: 'h1:contains("Something went wrong.")',
|
||||
trigger: 'div#error_main.collapse.show pre:contains("This is a missing http test")',
|
||||
run: function () {},
|
||||
}, {
|
||||
content: "http missing error page debug has traceback closed",
|
||||
trigger: 'body:has(div#error_traceback.collapse:not(.show) pre#exception_traceback)',
|
||||
run: function () {
|
||||
window.location.href = window.location.origin + '/test_access_denied_http?debug=0';
|
||||
},
|
||||
}, {
|
||||
content: "http error 403 page has title but no message",
|
||||
extra_trigger: 'h1:contains("403: Forbidden")',
|
||||
trigger: 'div#wrap:not(:has(pre:contains("This is an access denied http test"))', //See ir_http.py handle_exception, the exception is replaced so there is no message !
|
||||
run: function () {
|
||||
window.location.href = window.location.origin + '/test_access_denied_http?debug=1';
|
||||
},
|
||||
}, {
|
||||
content: "http 403 error page debug has title but no message",
|
||||
extra_trigger: 'h1:contains("403: Forbidden")',
|
||||
trigger: 'div#debug_infos:not(:has(#error_main))',
|
||||
run: function () {},
|
||||
}, {
|
||||
content: "http 403 error page debug has traceback open",
|
||||
trigger: 'body:has(div#error_traceback.collapse.show pre#exception_traceback)',
|
||||
run: function () {},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import wTourUtils from 'website.tour_utils';
|
||||
|
||||
/**
|
||||
* The purpose of this tour is to check the link on image flow.
|
||||
*/
|
||||
|
||||
const selectImageSteps = [{
|
||||
content: "select block",
|
||||
trigger: "iframe #wrapwrap .s_text_image",
|
||||
}, {
|
||||
content: "check link popover disappeared",
|
||||
trigger: "iframe body:not(:has(.o_edit_menu_popover))",
|
||||
run: () => {}, // check
|
||||
}, {
|
||||
content: "select image",
|
||||
trigger: "iframe #wrapwrap .s_text_image img",
|
||||
}];
|
||||
|
||||
wTourUtils.registerWebsitePreviewTour('test_image_link', {
|
||||
test: true,
|
||||
url: '/',
|
||||
edition: true,
|
||||
}, [
|
||||
wTourUtils.dragNDrop({
|
||||
id: 's_text_image',
|
||||
name: 'Text - Image',
|
||||
}),
|
||||
...selectImageSteps,
|
||||
{
|
||||
content: "enable link",
|
||||
trigger: "#oe_snippets we-customizeblock-options:has(we-title:contains('Image')) we-customizeblock-option:has(we-title:contains(Media)) we-button.fa-link",
|
||||
}, {
|
||||
content: "enter site URL",
|
||||
trigger: "#oe_snippets we-customizeblock-options:has(we-title:contains('Image')) we-input:contains(Your URL) input",
|
||||
run: "text odoo.com",
|
||||
},
|
||||
...selectImageSteps,
|
||||
{
|
||||
content: "check popover content has site URL",
|
||||
trigger: "iframe .o_edit_menu_popover a.o_we_url_link[href='http://odoo.com/']:contains(http://odoo.com/)",
|
||||
run: () => {}, // check
|
||||
}, {
|
||||
content: "remove URL",
|
||||
trigger: "#oe_snippets we-customizeblock-options:has(we-title:contains('Image')) we-input:contains(Your URL) input",
|
||||
run: "remove_text",
|
||||
},
|
||||
...selectImageSteps,
|
||||
{
|
||||
content: "check popover content has no URL",
|
||||
trigger: "iframe .o_edit_menu_popover a.o_we_url_link:not([href]):contains(No URL specified)",
|
||||
run: () => {}, // check
|
||||
}, {
|
||||
content: "enter email URL",
|
||||
trigger: "#oe_snippets we-customizeblock-options:has(we-title:contains('Image')) we-input:contains(Your URL) input",
|
||||
run: "text mailto:test@test.com",
|
||||
},
|
||||
...selectImageSteps,
|
||||
{
|
||||
content: "check popover content has mail URL",
|
||||
trigger: "iframe .o_edit_menu_popover:has(.fa-envelope-o) a.o_we_url_link[href='mailto:test@test.com']:contains(mailto:test@test.com)",
|
||||
run: () => {}, // check
|
||||
}, {
|
||||
content: "enter phone URL",
|
||||
trigger: "#oe_snippets we-customizeblock-options:has(we-title:contains('Image')) we-input:contains(Your URL) input",
|
||||
run: "text tel:555-2368",
|
||||
},
|
||||
...selectImageSteps,
|
||||
{
|
||||
content: "check popover content has phone URL",
|
||||
trigger: "iframe .o_edit_menu_popover:has(.fa-phone) a.o_we_url_link[href='tel:555-2368']:contains(tel:555-2368)",
|
||||
run: () => {}, // check
|
||||
}, {
|
||||
content: "remove URL",
|
||||
trigger: "#oe_snippets we-customizeblock-options:has(we-title:contains('Image')) we-input:contains(Your URL) input",
|
||||
run: "remove_text",
|
||||
},
|
||||
...selectImageSteps,
|
||||
{
|
||||
content: "check popover content has no URL",
|
||||
trigger: "iframe .o_edit_menu_popover a.o_we_url_link:not([href]):contains(No URL specified)",
|
||||
run: () => {}, // check
|
||||
},
|
||||
]);
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
odoo.define('test_website.image_upload_progress', function (require) {
|
||||
'use strict';
|
||||
|
||||
const wTourUtils = require('website.tour_utils');
|
||||
|
||||
const { FileSelectorControlPanel } = require('@web_editor/components/media_dialog/file_selector');
|
||||
const { patch, unpatch } = require('web.utils');
|
||||
|
||||
let patchWithError = false;
|
||||
const patchMediaDialog = () => patch(FileSelectorControlPanel.prototype, 'test_website.mock_image_widgets', {
|
||||
async onChangeFileInput() {
|
||||
const getFileFromB64 = (fileData) => {
|
||||
const binary = atob(fileData[2]);
|
||||
let len = binary.length;
|
||||
const arr = new Uint8Array(len);
|
||||
while (len--) {
|
||||
arr[len] = binary.charCodeAt(len);
|
||||
}
|
||||
return new File([arr], fileData[1], {type: fileData[0]});
|
||||
};
|
||||
|
||||
let files = [
|
||||
getFileFromB64(['image/vnd.microsoft.icon', 'icon.ico', "AAABAAEAAQEAAAEAIAAwAAAAFgAAACgAAAABAAAAAgAAAAEAIAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAA=="]),
|
||||
getFileFromB64(['image/webp', 'image.webp', "UklGRhwAAABXRUJQVlA4TBAAAAAvE8AEAAfQhuh//wMR0f8A"]),
|
||||
getFileFromB64(['image/png', 'image.png', "iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAApElEQVR42u3RAQ0AAAjDMO5fNCCDkC5z0HTVrisFCBABASIgQAQEiIAAAQJEQIAICBABASIgQAREQIAICBABASIgQAREQIAICBABASIgQAREQIAICBABASIgQAREQIAICBABASIgQAREQIAICBABASIgQAREQIAICBABASIgQAREQIAICBABASIgQAREQIAICBABASIgQAQECBAgAgJEQIAIyPcGFY7HnV2aPXoAAAAASUVORK5CYII="]),
|
||||
getFileFromB64(['image/jpeg', 'image.jpeg', "/9j/4AAQSkZJRgABAQAAAQABAAD//gAfQ29tcHJlc3NlZCBieSBqcGVnLXJlY29tcHJlc3P/2wCEAA0NDQ0ODQ4QEA4UFhMWFB4bGRkbHi0gIiAiIC1EKjIqKjIqRDxJOzc7STxsVUtLVWx9aWNpfZeHh5e+tb75+f8BDQ0NDQ4NDhAQDhQWExYUHhsZGRseLSAiICIgLUQqMioqMipEPEk7NztJPGxVS0tVbH1pY2l9l4eHl761vvn5///CABEIAEsASwMBIgACEQEDEQH/xAAVAAEBAAAAAAAAAAAAAAAAAAAABv/aAAgBAQAAAACHAAAAAAAAAAAAAAAAH//EABUBAQEAAAAAAAAAAAAAAAAAAAAH/9oACAECEAAAAKYAAAB//8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/2gAIAQMQAAAAngAAAf/EABQQAQAAAAAAAAAAAAAAAAAAAGD/2gAIAQEAAT8ASf/EABQRAQAAAAAAAAAAAAAAAAAAAED/2gAIAQIBAT8AT//EABQRAQAAAAAAAAAAAAAAAAAAAED/2gAIAQMBAT8AT//Z"]),
|
||||
];
|
||||
|
||||
if (!this.props.multiSelect) {
|
||||
if (patchWithError) {
|
||||
files = [files[0]];
|
||||
} else {
|
||||
files = [files[2]];
|
||||
}
|
||||
}
|
||||
await this.props.uploadFiles(files);
|
||||
}
|
||||
});
|
||||
|
||||
const unpatchMediaDialog = () => unpatch(FileSelectorControlPanel.prototype, 'test_website.mock_image_widgets');
|
||||
|
||||
const setupSteps = [{
|
||||
content: "reload to load patch",
|
||||
trigger: ".o_website_preview",
|
||||
run: () => {
|
||||
patchMediaDialog();
|
||||
},
|
||||
}, {
|
||||
content: "drop a snippet",
|
||||
trigger: "#oe_snippets .oe_snippet[name='Text - Image'] .oe_snippet_thumbnail:not(.o_we_already_dragging)",
|
||||
moveTrigger: "iframe .oe_drop_zone",
|
||||
run: "drag_and_drop iframe #wrap",
|
||||
}, {
|
||||
content: "drop a snippet",
|
||||
trigger: "#oe_snippets .oe_snippet[name='Image Gallery'] .oe_snippet_thumbnail:not(.o_we_already_dragging)",
|
||||
extra_trigger: "body.editor_has_snippets",
|
||||
moveTrigger: ".oe_drop_zone",
|
||||
run: "drag_and_drop iframe #wrap",
|
||||
}];
|
||||
|
||||
const formatErrorMsg = "format is not supported. Try with: .gif, .jpe, .jpeg, .jpg, .png, .svg";
|
||||
|
||||
wTourUtils.registerWebsitePreviewTour('test_image_upload_progress', {
|
||||
url: '/test_image_progress',
|
||||
test: true,
|
||||
edition: true,
|
||||
}, [
|
||||
...setupSteps,
|
||||
// 1. Check multi image upload
|
||||
{
|
||||
content: "click on dropped snippet",
|
||||
trigger: "iframe #wrap .s_image_gallery .img",
|
||||
}, {
|
||||
content: "click on add images to open image dialog (in multi mode)",
|
||||
trigger: 'we-customizeblock-option [data-add-images]',
|
||||
}, {
|
||||
content: "manually trigger input change",
|
||||
trigger: ".o_select_media_dialog .o_upload_media_button",
|
||||
run: () => {
|
||||
// This will trigger upload of dummy files for test purpose, as a
|
||||
// test can't select local files to upload into the input.
|
||||
document.body.querySelector('.o_select_media_dialog .o_file_input').dispatchEvent(new Event('change'));
|
||||
},
|
||||
}, {
|
||||
content: "check upload progress bar is correctly shown (1)",
|
||||
trigger: `.o_we_progressbar:contains('icon.ico'):contains('${formatErrorMsg}')`,
|
||||
in_modal: false,
|
||||
run: function () {}, // it's a check
|
||||
}, {
|
||||
content: "check upload progress bar is correctly shown (2)",
|
||||
trigger: `.o_we_progressbar:contains('image.webp'):contains('${formatErrorMsg}')`,
|
||||
in_modal: false,
|
||||
run: function () {}, // it's a check
|
||||
}, {
|
||||
content: "check upload progress bar is correctly shown (3)",
|
||||
trigger: ".o_we_progressbar:contains('image.png'):contains('File has been uploaded')",
|
||||
in_modal: false,
|
||||
run: function () {}, // it's a check
|
||||
}, {
|
||||
content: "check upload progress bar is correctly shown (4)",
|
||||
trigger: ".o_we_progressbar:contains('image.jpeg'):contains('File has been uploaded')",
|
||||
in_modal: false,
|
||||
run: function () {}, // it's a check
|
||||
}, {
|
||||
content: "there should only have one notification toaster",
|
||||
trigger: ".o_notification",
|
||||
in_modal: false,
|
||||
run: () => {
|
||||
const notificationCount = $('.o_notification').length;
|
||||
if (notificationCount !== 1) {
|
||||
console.error("There should be one noficiation toaster opened, and only one.");
|
||||
}
|
||||
}
|
||||
}, {
|
||||
content: "close notification",
|
||||
trigger: '.o_notification_close',
|
||||
in_modal: false,
|
||||
}, {
|
||||
content: "close media dialog",
|
||||
trigger: '.modal-footer .btn-secondary',
|
||||
},
|
||||
// 2. Check success single image upload
|
||||
{
|
||||
content: "click on dropped snippet",
|
||||
trigger: "iframe #wrap .s_text_image .img",
|
||||
}, {
|
||||
content: "click on replace media to open image dialog",
|
||||
trigger: 'we-customizeblock-option [data-replace-media]',
|
||||
}, {
|
||||
content: "manually trigger input change",
|
||||
trigger: ".o_select_media_dialog .o_upload_media_button",
|
||||
run: () => {
|
||||
// This will trigger upload of dummy files for test purpose, as a
|
||||
// test can't select local files to upload into the input.
|
||||
document.body.querySelector('.o_select_media_dialog .o_file_input').dispatchEvent(new Event('change'));
|
||||
},
|
||||
}, {
|
||||
content: "check upload progress bar is correctly shown",
|
||||
trigger: ".o_we_progressbar:contains('image.png')",
|
||||
in_modal: false,
|
||||
run: function () {}, // it's a check
|
||||
}, {
|
||||
content: "there should only have one notification toaster",
|
||||
trigger: ".o_notification",
|
||||
in_modal: false,
|
||||
run: () => {
|
||||
const notificationCount = $('.o_notification').length;
|
||||
if (notificationCount !== 1) {
|
||||
console.error("There should be one noficiation toaster opened, and only one.");
|
||||
}
|
||||
}
|
||||
}, {
|
||||
content: "media dialog has closed after the upload",
|
||||
trigger: 'body:not(:has(.o_select_media_dialog))',
|
||||
run: () => {}, // It's a check.
|
||||
}, {
|
||||
content: "the upload progress toast was updated",
|
||||
trigger: ".o_we_progressbar:contains('image.png'):contains('File has been uploaded')",
|
||||
run: () => {}, // It's a check.
|
||||
}, {
|
||||
content: "toaster should disappear after a few seconds if the uploaded image is successful",
|
||||
trigger: "body:not(:has(.o_we_progressbar))",
|
||||
run: function () {}, // it's a check
|
||||
},
|
||||
// 3. Check error single image upload
|
||||
{
|
||||
content: "click on dropped snippet",
|
||||
trigger: "iframe #wrap .s_text_image .img",
|
||||
}, {
|
||||
content: "click on replace media to open image dialog",
|
||||
trigger: 'we-customizeblock-option [data-replace-media]',
|
||||
}, {
|
||||
content: "manually trigger input change",
|
||||
trigger: ".o_select_media_dialog .o_upload_media_button",
|
||||
in_modal: false,
|
||||
run: () => {
|
||||
patchWithError = true;
|
||||
// This will trigger upload of dummy files for test purpose, as a
|
||||
// test can't select local files to upload into the input.
|
||||
document.body.querySelector('.o_select_media_dialog .o_file_input').dispatchEvent(new Event('change'));
|
||||
|
||||
},
|
||||
}, {
|
||||
content: "check upload progress bar is correctly shown",
|
||||
trigger: `.o_we_progressbar:contains('icon.ico'):contains('${formatErrorMsg}')`,
|
||||
in_modal: false,
|
||||
run: function () {
|
||||
patchWithError = false;
|
||||
},
|
||||
}, {
|
||||
content: "there should only have one notification toaster",
|
||||
trigger: ".o_notification",
|
||||
in_modal: false,
|
||||
run: () => {
|
||||
const notificationCount = $('.o_notification').length;
|
||||
if (notificationCount !== 1) {
|
||||
console.error("There should be one noficiation toaster opened, and only one.");
|
||||
}
|
||||
unpatchMediaDialog();
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
wTourUtils.registerWebsitePreviewTour('test_image_upload_progress_unsplash', {
|
||||
url: '/test_image_progress',
|
||||
test: true,
|
||||
edition: true,
|
||||
}, [
|
||||
...setupSteps,
|
||||
// 1. Check multi image upload
|
||||
{
|
||||
content: "click on dropped snippet",
|
||||
trigger: "iframe #wrap .s_image_gallery .img",
|
||||
}, {
|
||||
content: "click on replace media to open image dialog",
|
||||
trigger: 'we-customizeblock-option [data-replace-media]',
|
||||
}, {
|
||||
content: "search 'fox' images",
|
||||
trigger: ".o_we_search",
|
||||
run: "text fox",
|
||||
}, {
|
||||
content: "click on unsplash result", // note that unsplash is mocked
|
||||
trigger: "img[alt~=fox]"
|
||||
}, {
|
||||
content: "check that the upload progress bar is correctly shown",
|
||||
// ensure it is there so we are sure next step actually test something
|
||||
extra_trigger: '.o_notification_close',
|
||||
trigger: ".o_we_progressbar:contains('fox'):contains('File has been uploaded')",
|
||||
in_modal: false,
|
||||
run: function () {}, // it's a check
|
||||
}, {
|
||||
content: "notification should close after 3 seconds",
|
||||
trigger: 'body:not(:has(.o_notification_close))',
|
||||
in_modal: false,
|
||||
}, {
|
||||
content: "unsplash image (mocked to logo) should have been used",
|
||||
trigger: "iframe #wrap .s_image_gallery .img[data-original-src^='/unsplash/HQqIOc8oYro/fox']",
|
||||
run: () => {
|
||||
unpatchMediaDialog();
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
odoo.define('test_website.json_auth', function (require) {
|
||||
'use strict';
|
||||
|
||||
var tour = require('web_tour.tour');
|
||||
var session = require('web.session')
|
||||
|
||||
tour.register('test_json_auth', {
|
||||
test: true,
|
||||
}, [{
|
||||
trigger: 'body',
|
||||
run: async function () {
|
||||
await session.rpc('/test_get_dbname').then( function (result){
|
||||
return session.rpc("/web/session/authenticate", {
|
||||
db: result,
|
||||
login: 'admin',
|
||||
password: 'admin'
|
||||
});
|
||||
});
|
||||
window.location.href = window.location.origin;
|
||||
},
|
||||
}, {
|
||||
trigger: 'span:contains(Mitchell Admin), span:contains(Administrator)',
|
||||
run: function () {},
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import tour from 'web_tour.tour';
|
||||
|
||||
tour.register('test_website_page_manager', {
|
||||
test: true,
|
||||
url: '/web#action=test_website.action_test_model_multi_website',
|
||||
}, [
|
||||
// Part 1: check that the website filter is working
|
||||
{
|
||||
content: "Check that we see records from My Website",
|
||||
trigger: ".o_list_table .o_data_row .o_data_cell[name=name]:contains('Test Multi Model Website 1') " +
|
||||
"~ .o_data_cell[name=website_id]:contains('My Website')",
|
||||
run: () => null, // it's a check
|
||||
}, {
|
||||
content: "Check that there is only 2 records in the pager",
|
||||
trigger: ".o_pager .o_pager_value:contains('1-2')",
|
||||
run: () => null, // it's a check
|
||||
}, {
|
||||
content: "Click on the 'Select all records' checkbox",
|
||||
trigger: "thead .o_list_record_selector",
|
||||
}, {
|
||||
content: "Check that there is only 2 records selected",
|
||||
trigger: ".o_list_selection_box:contains('2 selected')",
|
||||
run: () => null, // it's a check
|
||||
}, {
|
||||
content: "Click on My Website search filter",
|
||||
trigger: "button.dropdown-toggle:contains('My Website')",
|
||||
}, {
|
||||
content: "Select My Website 2",
|
||||
trigger: ".dropdown-menu.show > .dropdown-item:contains('My Website 2')",
|
||||
}, {
|
||||
// This step is just here to ensure there is more records than the 2
|
||||
// available on website 1, to ensure the test is actually testing something.
|
||||
content: "Check that we see records from My Website 2",
|
||||
trigger: ".o_list_table .o_data_row .o_data_cell[name=name]:contains('Test Model Multi Website 2') " +
|
||||
"~ .o_data_cell[name=website_id]:contains('My Website 2')",
|
||||
run: () => null, // it's a check
|
||||
},
|
||||
// Part 2: ensure Kanban View is working / not crashing
|
||||
{
|
||||
content: "Click on Kanban View",
|
||||
trigger: '.o_cp_switch_buttons .o_kanban',
|
||||
}, {
|
||||
content: "Click on List View",
|
||||
extra_trigger: '.o_kanban_renderer',
|
||||
trigger: '.o_cp_switch_buttons .o_list',
|
||||
}, {
|
||||
content: "Wait for List View to be loaded",
|
||||
trigger: '.o_list_renderer',
|
||||
run: () => null, // it's a check
|
||||
}]);
|
||||
|
||||
tour.register('test_website_page_manager_js_class_bug', {
|
||||
test: true,
|
||||
url: '/web#action=test_website.action_test_model_multi_website_js_class_bug',
|
||||
}, [{
|
||||
content: "Click on Kanban View",
|
||||
trigger: '.o_cp_switch_buttons .o_kanban',
|
||||
}, {
|
||||
content: "Wait for Kanban View to be loaded",
|
||||
trigger: '.o_kanban_renderer',
|
||||
run: () => null, // it's a check
|
||||
}]);
|
||||
|
||||
tour.register('test_website_page_manager_no_website_id', {
|
||||
test: true,
|
||||
url: '/web#action=test_website.action_test_model',
|
||||
}, [{
|
||||
content: "Click on Kanban View",
|
||||
trigger: '.o_cp_switch_buttons .o_kanban',
|
||||
}, {
|
||||
content: "Wait for Kanban View to be loaded",
|
||||
trigger: '.o_kanban_renderer',
|
||||
run: () => null, // it's a check
|
||||
}]);
|
||||
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { patch } from '@web/core/utils/patch';
|
||||
import { VideoSelector } from '@web_editor/components/media_dialog/video_selector';
|
||||
import wTourUtils from 'website.tour_utils';
|
||||
|
||||
const VIDEO_URL = 'https://www.youtube.com/watch?v=Dpq87YCHmJc';
|
||||
|
||||
/**
|
||||
* The purpose of this tour is to check the media replacement flow.
|
||||
*/
|
||||
wTourUtils.registerWebsitePreviewTour('test_replace_media', {
|
||||
url: '/',
|
||||
test: true,
|
||||
edition: true,
|
||||
}, [
|
||||
{
|
||||
trigger: "body",
|
||||
run: function () {
|
||||
// Patch the VideoDialog so that it does not do external calls
|
||||
// during the test (note that we don't unpatch but as the patch
|
||||
// is only done after the execution of a test_website test and
|
||||
// specific to an URL only, it is acceptable).
|
||||
// TODO if we ever give the possibility to upload its own videos,
|
||||
// this won't be necessary anymore.
|
||||
patch(VideoSelector.prototype, "Video selector patch", {
|
||||
async _getVideoURLData(src, options) {
|
||||
if (src === VIDEO_URL || src === 'about:blank') {
|
||||
return {platform: 'youtube', embed_url: 'about:blank'};
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "drop picture snippet",
|
||||
trigger: "#oe_snippets .oe_snippet[name='Picture'] .oe_snippet_thumbnail:not(.o_we_already_dragging)",
|
||||
moveTrigger: "iframe .oe_drop_zone",
|
||||
run: "drag_and_drop iframe #wrap",
|
||||
},
|
||||
{
|
||||
content: "select image",
|
||||
trigger: "iframe .s_picture figure img",
|
||||
},
|
||||
{
|
||||
content: "ensure image size is displayed",
|
||||
trigger: "#oe_snippets we-title:contains('Image') .o_we_image_weight:contains('kb')",
|
||||
run: function () {}, // check
|
||||
},
|
||||
wTourUtils.changeOption("ImageTools", 'we-select[data-name="shape_img_opt"] we-toggler'),
|
||||
wTourUtils.changeOption("ImageTools", "we-button[data-set-img-shape]"),
|
||||
{
|
||||
content: "replace image",
|
||||
trigger: "#oe_snippets we-button[data-replace-media]",
|
||||
},
|
||||
{
|
||||
content: "select svg",
|
||||
trigger: ".o_select_media_dialog img[title='sample.svg']",
|
||||
},
|
||||
{
|
||||
content: "ensure the svg doesn't have a shape",
|
||||
trigger: "iframe .s_picture figure img:not([data-shape])",
|
||||
run: function () {}, // check
|
||||
},
|
||||
{
|
||||
content: "ensure image size is not displayed",
|
||||
trigger: "#oe_snippets we-title:contains('Image'):not(:has(.o_we_image_weight:visible))",
|
||||
run: function () {}, // check
|
||||
},
|
||||
{
|
||||
content: "replace image",
|
||||
trigger: "#oe_snippets we-button[data-replace-media]",
|
||||
},
|
||||
{
|
||||
content: "go to pictogram tab",
|
||||
trigger: ".o_select_media_dialog .nav-link:contains('Icons')",
|
||||
},
|
||||
{
|
||||
content: "select an icon",
|
||||
trigger: ".o_select_media_dialog:has(.nav-link.active:contains('Icons')) .tab-content span.fa-lemon-o",
|
||||
},
|
||||
{
|
||||
content: "ensure icon block is displayed",
|
||||
trigger: "#oe_snippets we-customizeblock-options we-title:contains('Icon')",
|
||||
run: function () {}, // check
|
||||
},
|
||||
{
|
||||
content: "select footer",
|
||||
trigger: "iframe footer",
|
||||
},
|
||||
{
|
||||
content: "select icon",
|
||||
trigger: "iframe .s_picture figure span.fa-lemon-o",
|
||||
},
|
||||
{
|
||||
content: "ensure icon block is still displayed",
|
||||
trigger: "#oe_snippets we-customizeblock-options we-title:contains('Icon')",
|
||||
run: function () {}, // check
|
||||
},
|
||||
{
|
||||
content: "replace icon",
|
||||
trigger: "#oe_snippets we-button[data-replace-media]",
|
||||
},
|
||||
{
|
||||
content: "go to video tab",
|
||||
trigger: ".o_select_media_dialog .nav-link:contains('Video')",
|
||||
},
|
||||
{
|
||||
content: "enter a video URL",
|
||||
trigger: ".o_select_media_dialog #o_video_text",
|
||||
// Design your first web page.
|
||||
run: `text ${VIDEO_URL}`,
|
||||
},
|
||||
{
|
||||
content: "wait for preview to appear",
|
||||
// "about:blank" because the VideoWidget was patched at the start of this tour
|
||||
trigger: ".o_select_media_dialog div.media_iframe_video iframe[src='about:blank']",
|
||||
run: function () {}, // check
|
||||
},
|
||||
{
|
||||
content: "confirm selection",
|
||||
trigger: ".o_select_media_dialog .modal-footer .btn-primary",
|
||||
},
|
||||
{
|
||||
content: "ensure video option block is displayed",
|
||||
trigger: "#oe_snippets we-customizeblock-options we-title:contains('Video')",
|
||||
run: function () {}, // check
|
||||
},
|
||||
{
|
||||
content: "replace image",
|
||||
trigger: "#oe_snippets we-button[data-replace-media]",
|
||||
},
|
||||
{
|
||||
content: "go to pictogram tab",
|
||||
trigger: ".o_select_media_dialog .nav-link:contains('Icons')",
|
||||
},
|
||||
{
|
||||
content: "select an icon",
|
||||
trigger: ".o_select_media_dialog:has(.nav-link.active:contains('Icons')) .tab-content span.fa-lemon-o",
|
||||
},
|
||||
{
|
||||
content: "ensure icon block is displayed",
|
||||
trigger: "#oe_snippets we-customizeblock-options we-title:contains('Icon')",
|
||||
run: function () {}, // check
|
||||
},
|
||||
{
|
||||
content: "select footer",
|
||||
trigger: "iframe footer",
|
||||
},
|
||||
{
|
||||
content: "select icon",
|
||||
trigger: "iframe .s_picture figure span.fa-lemon-o",
|
||||
},
|
||||
{
|
||||
content: "ensure icon block is still displayed",
|
||||
trigger: "#oe_snippets we-customizeblock-options we-title:contains('Icon')",
|
||||
run: function () {}, // check
|
||||
},
|
||||
]);
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
/* global ace */
|
||||
odoo.define('test_website.reset_views', function (require) {
|
||||
'use strict';
|
||||
|
||||
const wTourUtils = require('website.tour_utils');
|
||||
|
||||
var BROKEN_STEP = {
|
||||
// because saving a broken template opens a recovery page with no assets
|
||||
// there's no way for the tour to resume on the new page, and thus no way
|
||||
// to properly wait for the page to be saved & reloaded in order to fix the
|
||||
// race condition of a tour ending on a side-effect (with the possible
|
||||
// exception of somehow telling the harness / browser to do it)
|
||||
trigger: 'body',
|
||||
run: function () {}
|
||||
};
|
||||
wTourUtils.registerWebsitePreviewTour('test_reset_page_view_complete_flow_part1', {
|
||||
test: true,
|
||||
url: '/test_page_view',
|
||||
// 1. Edit the page through Edit Mode, it will COW the view
|
||||
edition: true,
|
||||
},
|
||||
[
|
||||
{
|
||||
content: "drop a snippet",
|
||||
trigger: ".oe_snippet:has(.s_cover) .oe_snippet_thumbnail",
|
||||
// id starting by 'oe_structure..' will actually create an inherited view
|
||||
run: "drag_and_drop iframe #oe_structure_test_website_page",
|
||||
},
|
||||
{
|
||||
content: "save the page",
|
||||
extra_trigger: 'iframe #oe_structure_test_website_page.o_dirty',
|
||||
trigger: "button[data-action=save]",
|
||||
},
|
||||
// 2. Edit that COW'd view in the HTML editor to break it.
|
||||
{
|
||||
content: "open site menu",
|
||||
extra_trigger: "iframe body:not(.editor_enable)",
|
||||
trigger: 'button[data-menu-xmlid="website.menu_site"]',
|
||||
},
|
||||
{
|
||||
content: "open html editor",
|
||||
trigger: 'a[data-menu-xmlid="website.menu_ace_editor"]',
|
||||
},
|
||||
{
|
||||
content: "add a broken t-field in page DOM",
|
||||
trigger: 'div.ace_line .ace_xml:contains("placeholder")',
|
||||
run: function () {
|
||||
ace.edit('ace-view-editor').getSession().insert({row: 4, column: 1}, '<t t-field="not.exist"/>\n');
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "save the html editor",
|
||||
extra_trigger: '.ace_content:contains("not.exist")',
|
||||
trigger: ".o_ace_view_editor button[data-action=save]",
|
||||
},
|
||||
BROKEN_STEP
|
||||
]
|
||||
);
|
||||
|
||||
wTourUtils.registerWebsitePreviewTour('test_reset_page_view_complete_flow_part2', {
|
||||
test: true,
|
||||
url: '/test_page_view',
|
||||
},
|
||||
[
|
||||
{
|
||||
content: "check that the view got fixed",
|
||||
trigger: 'iframe p:containsExact("Test Page View")',
|
||||
run: function () {}, // it's a check
|
||||
},
|
||||
{
|
||||
content: "check that the inherited COW view is still there (created during edit mode)",
|
||||
trigger: 'iframe #oe_structure_test_website_page .s_cover',
|
||||
run: function () {}, // it's a check
|
||||
},
|
||||
//4. Now break the inherited view created when dropping a snippet
|
||||
{
|
||||
content: "open site menu",
|
||||
trigger: 'button[data-menu-xmlid="website.menu_site"]',
|
||||
},
|
||||
{
|
||||
content: "open html editor",
|
||||
trigger: 'a[data-menu-xmlid="website.menu_ace_editor"]',
|
||||
},
|
||||
{
|
||||
content: "select oe_structure view",
|
||||
trigger: '#s2id_ace-view-list', // use select2 version
|
||||
run: function () {
|
||||
var viewId = $('#ace-view-list option:contains("oe_structure_test_website_page")').val();
|
||||
$('#ace-view-list').val(viewId).trigger('change');
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "add a broken t-field in page DOM",
|
||||
trigger: 'div.ace_line .ace_xml:contains("oe_structure_test_website_page")',
|
||||
run: function () {
|
||||
ace.edit('ace-view-editor').getSession().insert({row: 4, column: 1}, '<t t-field="not.exist"/>\n');
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "save the html editor",
|
||||
trigger: ".o_ace_view_editor button[data-action=save]",
|
||||
},
|
||||
BROKEN_STEP
|
||||
]
|
||||
);
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import wTourUtils from 'website.tour_utils';
|
||||
|
||||
/**
|
||||
* The purpose of these tours is to check the systray visibility:
|
||||
*
|
||||
* - as an administrator
|
||||
* - as a restricted editor with "tester" right
|
||||
* - as a restricted editor without "tester" right
|
||||
* - as a "tester" who is not a restricted editor
|
||||
* - as an unrelated user (neither "tester" nor restricted editor)
|
||||
*/
|
||||
|
||||
const canPublish = [{
|
||||
content: 'Publish',
|
||||
trigger: '.o_menu_systray .o_menu_systray_item:contains("Unpublished")',
|
||||
}, {
|
||||
content: 'Wait for Publish',
|
||||
trigger: '.o_menu_systray .o_menu_systray_item:contains("Published"):not([data-processing])',
|
||||
run: () => {}, // This is a check.
|
||||
}, {
|
||||
content: 'Unpublish',
|
||||
trigger: '.o_menu_systray .o_menu_systray_item:contains("Published")',
|
||||
}, {
|
||||
content: 'Wait for Unpublish',
|
||||
trigger: '.o_menu_systray .o_menu_systray_item:contains("Unpublished"):not([data-processing])',
|
||||
run: () => {}, // This is a check.
|
||||
}];
|
||||
|
||||
const cannotPublish = [{
|
||||
content: 'Check has no Publish/Unpublish',
|
||||
trigger: '.o_menu_systray:not(:has(.o_menu_systray_item:contains("ublished")))',
|
||||
run: () => {}, // This is a check.
|
||||
}];
|
||||
|
||||
const canToggleMobilePreview = [{
|
||||
content: 'Enable mobile preview',
|
||||
trigger: '.o_menu_systray .o_menu_systray_item.o_mobile_preview:not(.o_mobile_preview_active)',
|
||||
}, {
|
||||
content: 'Disable mobile preview',
|
||||
trigger: '.o_menu_systray .o_menu_systray_item.o_mobile_preview.o_mobile_preview_active',
|
||||
}];
|
||||
|
||||
const cannotToggleMobilePreview = [{
|
||||
content: 'Enable mobile preview',
|
||||
trigger: '.o_menu_systray:not(:has(.o_menu_systray_item.o_mobile_preview))',
|
||||
run: () => {}, // This is a check.
|
||||
}];
|
||||
|
||||
// For non-website users, switching across website only works if the domains are
|
||||
// specified. Within the scope of test tours, this cannot be achieved.
|
||||
const canSwitchWebsiteNoCheck = [{
|
||||
content: 'Open website switcher',
|
||||
trigger: '.o_menu_systray .o_menu_systray_item.o_website_switcher_container .dropdown-toggle:contains("My Website"):not(:contains("My Website 2"))',
|
||||
}, {
|
||||
content: 'Switch to other website',
|
||||
trigger: '.o_menu_systray .o_menu_systray_item.o_website_switcher_container .dropdown-item:contains("Other")',
|
||||
run: () => {}, // This is a check.
|
||||
}];
|
||||
|
||||
|
||||
const canSwitchWebsite = [{
|
||||
content: 'Open website switcher',
|
||||
trigger: '.o_menu_systray .o_menu_systray_item.o_website_switcher_container .dropdown-toggle:contains("My Website"):not(:contains("My Website 2"))',
|
||||
}, {
|
||||
content: 'Switch to other website',
|
||||
trigger: '.o_menu_systray .o_menu_systray_item.o_website_switcher_container .dropdown-item:contains("Other")',
|
||||
}, {
|
||||
content: 'Wait for other website',
|
||||
trigger: 'iframe body:contains("Test Model") div:contains("Other")',
|
||||
run: () => {}, // This is a check.
|
||||
}];
|
||||
|
||||
const canAddNewContent = [{
|
||||
content: 'Open +New content',
|
||||
trigger: '.o_menu_systray .o_menu_systray_item.o_new_content_container',
|
||||
}, {
|
||||
content: 'Close +New content',
|
||||
trigger: '#o_new_content_menu_choices',
|
||||
}];
|
||||
|
||||
const cannotAddNewContent = [{
|
||||
content: 'No +New content',
|
||||
trigger: '.o_menu_systray:not(:has(.o_menu_systray_item.o_new_content_container))',
|
||||
run: () => {}, // This is a check.
|
||||
}];
|
||||
|
||||
const canEditInBackEnd = [{
|
||||
content: 'Edit in backend',
|
||||
trigger: '.o_menu_systray .o_website_edit_in_backend a',
|
||||
}, {
|
||||
content: 'Check that the form is editable',
|
||||
trigger: '.o_form_view_container .o_form_editable',
|
||||
run: () => {}, // This is a check.
|
||||
}, {
|
||||
content: 'Return to website',
|
||||
trigger: '.oe_button_box .fa-globe',
|
||||
}];
|
||||
|
||||
const canViewInBackEnd = [{
|
||||
content: 'Go to backend',
|
||||
trigger: '.o_menu_systray .o_website_edit_in_backend a',
|
||||
}, {
|
||||
content: 'Check that the form is read-only',
|
||||
trigger: '.o_form_view_container .o_form_readonly',
|
||||
run: () => {}, // This is a check.
|
||||
}, {
|
||||
content: 'Return to website',
|
||||
trigger: '.oe_button_box .fa-globe',
|
||||
}];
|
||||
|
||||
const canEdit = [
|
||||
...wTourUtils.clickOnEditAndWaitEditMode(),
|
||||
{
|
||||
content: 'Click on name',
|
||||
trigger: 'iframe span[data-oe-expression="test_model.name"][contenteditable="true"]',
|
||||
}, {
|
||||
content: 'Change name',
|
||||
trigger: 'iframe span[data-oe-expression="test_model.name"][contenteditable="true"]',
|
||||
run: 'text Better name',
|
||||
}, {
|
||||
content: 'Check that field becomes dirty',
|
||||
trigger: 'iframe span[data-oe-expression="test_model.name"].o_dirty',
|
||||
run: () => {}, // This is a check.
|
||||
},
|
||||
...wTourUtils.clickOnSave(),
|
||||
{
|
||||
content: 'Check whether name is saved',
|
||||
trigger: 'iframe span[data-oe-expression="test_model.name"]:contains("Better name")',
|
||||
run: () => {}, // This is a check.
|
||||
},
|
||||
];
|
||||
|
||||
const cannotEdit = [{
|
||||
content: 'Check Edit is not available',
|
||||
trigger: '.o_menu_systray:not(:has(.o_edit_website_container))',
|
||||
run: () => {}, // This is a check.
|
||||
}];
|
||||
|
||||
const canEditButCannotChange = [
|
||||
...wTourUtils.clickOnEditAndWaitEditMode(),
|
||||
{
|
||||
content: 'Cannot change name',
|
||||
trigger: 'iframe main:not(:has([data-oe-expression])):contains("Test Model")',
|
||||
run: () => {}, // This is a check.
|
||||
},
|
||||
];
|
||||
|
||||
const register = (title, steps) => {
|
||||
wTourUtils.registerWebsitePreviewTour(title, {
|
||||
url: '/test_model/1',
|
||||
test: true,
|
||||
}, steps);
|
||||
};
|
||||
|
||||
register('test_systray_admin', [
|
||||
...canPublish,
|
||||
...canToggleMobilePreview,
|
||||
...canSwitchWebsite,
|
||||
...canAddNewContent,
|
||||
...canEditInBackEnd,
|
||||
...canEdit,
|
||||
]);
|
||||
|
||||
register('test_systray_reditor_tester', [
|
||||
...canPublish,
|
||||
...canToggleMobilePreview,
|
||||
...canSwitchWebsite,
|
||||
...canAddNewContent,
|
||||
...canEditInBackEnd,
|
||||
...canEdit,
|
||||
]);
|
||||
|
||||
register('test_systray_reditor_not_tester', [
|
||||
...cannotPublish,
|
||||
...canToggleMobilePreview,
|
||||
...canSwitchWebsite,
|
||||
...canAddNewContent,
|
||||
...canViewInBackEnd,
|
||||
...canEditButCannotChange,
|
||||
]);
|
||||
|
||||
register('test_systray_not_reditor_tester', [
|
||||
...canPublish,
|
||||
...cannotToggleMobilePreview,
|
||||
...canSwitchWebsiteNoCheck,
|
||||
...cannotAddNewContent,
|
||||
...canEditInBackEnd,
|
||||
...cannotEdit,
|
||||
]);
|
||||
|
||||
register('test_systray_not_reditor_not_tester', [
|
||||
...cannotPublish,
|
||||
...cannotToggleMobilePreview,
|
||||
...canSwitchWebsiteNoCheck,
|
||||
...cannotAddNewContent,
|
||||
...canViewInBackEnd,
|
||||
...cannotEdit,
|
||||
]);
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import tour from "web_tour.tour";
|
||||
|
||||
const websiteName = "Website Test Settings";
|
||||
|
||||
tour.register("website_settings_m2o_dirty", {
|
||||
test: true,
|
||||
url: "/web",
|
||||
},
|
||||
[
|
||||
tour.stepUtils.showAppsMenuItem(),
|
||||
{
|
||||
content: "open settings",
|
||||
trigger: ".o_app[data-menu-xmlid='base.menu_administration'",
|
||||
}, {
|
||||
content: "open website settings",
|
||||
trigger: ".settings_tab .tab[data-key='website']",
|
||||
}, {
|
||||
content: "check that the 'Shared Customers Accounts' setting is checked",
|
||||
trigger: "input#shared_user_account:checked",
|
||||
run: function () {}, // it's a check
|
||||
}, {
|
||||
content: "open website switcher",
|
||||
trigger: "input#website_id",
|
||||
}, {
|
||||
content: `select ${websiteName} in the website switcher`,
|
||||
trigger: `li:has(.dropdown-item:contains('${websiteName}'))`,
|
||||
}, {
|
||||
content: `check that the settings of ${websiteName} are loaded (Shared Customers Accounts)`,
|
||||
trigger: "input#shared_user_account:not(:checked)",
|
||||
run: function () {}, // it's a check
|
||||
}, {
|
||||
content: "click on the fake website setting after checking the edited website",
|
||||
trigger: "button[name='action_website_test_setting']",
|
||||
}, {
|
||||
content: "check that we are on '/'",
|
||||
trigger: "iframe body div#wrap",
|
||||
run: function () {
|
||||
if (window.location.pathname !== "/") {
|
||||
// If this fails, it's probably because the change of website
|
||||
// in the settings dirty the record and so there is a dialog
|
||||
// save/discard displayed. This test ensure that does not happen
|
||||
// because it makes actions unreachable in multi website.
|
||||
console.error("We should be on '/' the settings didn't work");
|
||||
}
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_controller_args
|
||||
from . import test_custom_snippet
|
||||
from . import test_error
|
||||
from . import test_fuzzy
|
||||
from . import test_image_upload_progress
|
||||
from . import test_is_multilang
|
||||
from . import test_media
|
||||
from . import test_menu
|
||||
from . import test_multi_company
|
||||
from . import test_page_manager
|
||||
from . import test_page
|
||||
from . import test_performance
|
||||
from . import test_redirect
|
||||
from . import test_reset_views
|
||||
from . import test_session
|
||||
from . import test_settings
|
||||
from . import test_systray
|
||||
from . import test_views_during_module_operation
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import odoo.tests
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@odoo.tests.common.tagged('post_install', '-at_install')
|
||||
class TestWebsiteControllerArgs(odoo.tests.HttpCase):
|
||||
|
||||
@mute_logger('odoo.http')
|
||||
def test_crawl_args(self):
|
||||
req = self.url_open('/ignore_args/converter/valueA/?b=valueB&c=valueC')
|
||||
self.assertEqual(req.status_code, 200)
|
||||
self.assertEqual(req.json(), {'a': 'valueA', 'b': 'valueB', 'kw': {'c': 'valueC'}})
|
||||
|
||||
req = self.url_open('/ignore_args/converter/valueA/nokw?b=valueB&c=valueC')
|
||||
self.assertEqual(req.status_code, 200)
|
||||
self.assertEqual(req.json(), {'a': 'valueA', 'b': 'valueB'})
|
||||
|
||||
req = self.url_open('/ignore_args/converteronly/valueA/?b=valueB&c=valueC')
|
||||
self.assertEqual(req.status_code, 200)
|
||||
self.assertEqual(req.json(), {'a': 'valueA', 'kw': None})
|
||||
|
||||
req = self.url_open('/ignore_args/none?a=valueA&b=valueB')
|
||||
self.assertEqual(req.status_code, 200)
|
||||
self.assertEqual(req.json(), {'a': None, 'kw': None})
|
||||
|
||||
req = self.url_open('/ignore_args/a?a=valueA&b=valueB')
|
||||
self.assertEqual(req.status_code, 200)
|
||||
self.assertEqual(req.json(), {'a': 'valueA', 'kw': None})
|
||||
|
||||
req = self.url_open('/ignore_args/kw?a=valueA&b=valueB')
|
||||
self.assertEqual(req.status_code, 200)
|
||||
self.assertEqual(req.json(), {'a': 'valueA', 'kw': {'b': 'valueB'}})
|
||||
|
||||
req = self.url_open('/test_website/country/whatever-999999')
|
||||
self.assertEqual(req.status_code, 404,
|
||||
"Model converter record does not exist, return a 404.")
|
||||
|
||||
|
||||
@odoo.tests.common.tagged('post_install', '-at_install')
|
||||
class TestWebsiteControllers(odoo.tests.TransactionCase):
|
||||
|
||||
def test_01_sitemap(self):
|
||||
website = self.env['website'].browse(1)
|
||||
locs = website.with_user(website.user_id)._enumerate_pages(query_string='test_website_sitemap')
|
||||
self.assertEqual(len(list(locs)), 1, "The same URL should only be shown once")
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import odoo.tests
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@odoo.tests.common.tagged('post_install', '-at_install')
|
||||
class TestCustomSnippet(odoo.tests.HttpCase):
|
||||
|
||||
@mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
|
||||
def test_01_run_tour(self):
|
||||
self.start_tour(self.env['website'].get_client_action_url('/'), 'test_custom_snippet', login="admin")
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import odoo.tests
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@odoo.tests.common.tagged('post_install', '-at_install')
|
||||
class TestWebsiteError(odoo.tests.HttpCase):
|
||||
|
||||
@mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
|
||||
def test_01_run_test(self):
|
||||
self.start_tour("/test_error_view", 'test_error_website')
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
import psycopg2
|
||||
|
||||
from odoo.addons.website.controllers.main import Website
|
||||
from odoo.addons.website.tools import MockRequest
|
||||
import odoo.tests
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@odoo.tests.tagged('-at_install', 'post_install')
|
||||
class TestAutoComplete(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.website = cls.env['website'].browse(1)
|
||||
cls.WebsiteController = Website()
|
||||
|
||||
def _autocomplete(self, term, expected_count, expected_fuzzy_term, search_type="test", options=None):
|
||||
""" Calls the autocomplete for a given term and performs general checks """
|
||||
with MockRequest(self.env, website=self.website):
|
||||
suggestions = self.WebsiteController.autocomplete(
|
||||
search_type=search_type, term=term, max_nb_chars=50, options=options or {},
|
||||
)
|
||||
self.assertEqual(expected_count, suggestions['results_count'], "Wrong number of suggestions")
|
||||
self.assertEqual(expected_fuzzy_term, suggestions.get('fuzzy_search', 'Not found'), "Wrong fuzzy match")
|
||||
|
||||
def _autocomplete_page(self, term, expected_count, expected_fuzzy_term):
|
||||
self._autocomplete(term, expected_count, expected_fuzzy_term, search_type="pages", options={
|
||||
'displayDescription': False, 'displayDetail': False,
|
||||
'displayExtraDetail': False, 'displayExtraLink': False,
|
||||
'displayImage': False, 'allowFuzzy': True
|
||||
})
|
||||
|
||||
def test_01_many_records(self):
|
||||
# REF1000~REF3999
|
||||
data = [{
|
||||
'name': 'REF%s' % count,
|
||||
'is_published': True,
|
||||
} for count in range(1000, 4000)]
|
||||
self.env['test.model'].create(data)
|
||||
# NUM1000~NUM1998
|
||||
data = [{
|
||||
'name': 'NUM%s' % count,
|
||||
'is_published': True,
|
||||
} for count in range(1000, 1999)]
|
||||
self.env['test.model'].create(data)
|
||||
# There are more than 1000 "R*" records
|
||||
# => Find exact match through the fallback
|
||||
self._autocomplete('REF3000', 1, False)
|
||||
# => No exact match => Find fuzzy within first 1000 (distance=3: replace D by F, move 3, add 1)
|
||||
self._autocomplete('RED3000', 1, 'ref3000' if self.env.registry.has_trigram else 'ref1003')
|
||||
# => Find exact match through the fallback
|
||||
self._autocomplete('REF300', 10, False)
|
||||
# => Find exact match through the fallback
|
||||
self._autocomplete('REF1', 1000, False)
|
||||
# => No exact match => Nothing close enough (min distance=5)
|
||||
self._autocomplete('REFX', 0, "Not found")
|
||||
# => Find exact match through the fallback - unfortunate because already in the first 1000 records
|
||||
self._autocomplete('REF1230', 1, False)
|
||||
# => Find exact match through the fallback
|
||||
self._autocomplete('REF2230', 1, False)
|
||||
|
||||
# There are less than 1000 "N*" records
|
||||
# => Fuzzy within N* (distance=1: add 1)
|
||||
self._autocomplete('NUM000', 1, "num1000")
|
||||
# => Exact match (distance=0 shortcut logic)
|
||||
self._autocomplete('NUM100', 10, False)
|
||||
# => Exact match (distance=0 shortcut logic)
|
||||
self._autocomplete('NUM199', 9, False)
|
||||
# => Exact match (distance=0 shortcut logic)
|
||||
self._autocomplete('NUM1998', 1, False)
|
||||
# => Fuzzy within N* (distance=1: replace 1 by 9)
|
||||
self._autocomplete('NUM1999', 1, 'num1199')
|
||||
# => Fuzzy within N* (distance=1: add 1)
|
||||
self._autocomplete('NUM200', 1, 'num1200')
|
||||
|
||||
# There are no "X*" records
|
||||
self._autocomplete('XEF1000', 0, "Not found")
|
||||
|
||||
def test_02_pages_search(self):
|
||||
if not self.env.registry.has_trigram:
|
||||
try:
|
||||
self.env.cr.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm")
|
||||
self.env.registry.has_trigram = True
|
||||
except psycopg2.Error:
|
||||
_logger.warning("pg_trgm extension can't be installed, which is required to run this test")
|
||||
return
|
||||
|
||||
with MockRequest(self.env, website=self.env['website'].browse(1)):
|
||||
# This should not crash. This ensures that when searching on `name`
|
||||
# field of `website.page` model, it works properly when `pg_trgm` is
|
||||
# activated.
|
||||
# Indeed, `name` is a field of `website.page` record but only at the
|
||||
# ORM level, not in SQL, due to how `inherits` works.
|
||||
self.env['website'].browse(1)._search_with_fuzzy(
|
||||
'pages', 'test', limit=5, order='name asc, website_id desc, id', options={
|
||||
'displayDescription': False, 'displayDetail': False,
|
||||
'displayExtraDetail': False, 'displayExtraLink': False,
|
||||
'displayImage': False, 'allowFuzzy': True
|
||||
}
|
||||
)
|
||||
|
||||
test_page = self.env.ref('test_website.test_page')
|
||||
test_page.name = 'testTotallyUnique'
|
||||
|
||||
# Editor and Designer see pages in result
|
||||
self._autocomplete_page('testTotallyUnique', 1, False)
|
||||
|
||||
test_page.visibility = 'connected'
|
||||
self._autocomplete_page('testTotallyUnique', 1, False)
|
||||
test_page.visibility = False
|
||||
|
||||
test_page.groups_id = self.env.ref('base.group_public')
|
||||
self._autocomplete_page('testTotallyUnique', 1, False)
|
||||
test_page.groups_id = False
|
||||
|
||||
# Public user don't see restricted page
|
||||
saved_env = self.env
|
||||
self.website.env = self.env = self.env(user=self.website.user_id)
|
||||
self._autocomplete_page('testTotallyUnique', 0, "Not found")
|
||||
|
||||
test_page.website_indexed = True
|
||||
self._autocomplete_page('testTotallyUnique', 1, False)
|
||||
|
||||
test_page.groups_id = self.env.ref('base.group_system')
|
||||
self._autocomplete_page('testTotallyUnique', 0, "Not found")
|
||||
|
||||
test_page.groups_id = self.env.ref('base.group_public')
|
||||
self._autocomplete_page('testTotallyUnique', 1, False)
|
||||
test_page.groups_id = False
|
||||
|
||||
test_page.visibility = 'password'
|
||||
self._autocomplete_page('testTotallyUnique', 0, "Not found")
|
||||
|
||||
test_page.visibility = 'connected'
|
||||
self._autocomplete_page('testTotallyUnique', 0, "Not found")
|
||||
|
||||
# restore website env for next tests
|
||||
self.website.env = self.env = saved_env
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.web_editor.controllers.main import Web_Editor
|
||||
from odoo.addons.web_unsplash.controllers.main import Web_Unsplash
|
||||
|
||||
import odoo.tests
|
||||
|
||||
from odoo import http
|
||||
|
||||
|
||||
@odoo.tests.common.tagged('post_install', '-at_install')
|
||||
class TestImageUploadProgress(odoo.tests.HttpCase):
|
||||
|
||||
def test_01_image_upload_progress(self):
|
||||
self.start_tour(self.env['website'].get_client_action_url('/test_image_progress'), 'test_image_upload_progress', login="admin")
|
||||
|
||||
def test_02_image_upload_progress_unsplash(self):
|
||||
BASE_URL = self.base_url()
|
||||
|
||||
@http.route('/web_editor/media_library_search', type='json', auth="user", website=True)
|
||||
def media_library_search(self, **params):
|
||||
return {"results": 0, "media": []}
|
||||
# because not preprocessed by ControllerType metaclass
|
||||
media_library_search.original_endpoint.routing_type = 'json'
|
||||
# disable undraw, no third party should be called in tests
|
||||
self.patch(Web_Editor, 'media_library_search', media_library_search)
|
||||
|
||||
@http.route("/web_unsplash/fetch_images", type='json', auth="user")
|
||||
def fetch_unsplash_images(self, **post):
|
||||
return {
|
||||
'total': 1434,
|
||||
'total_pages': 48,
|
||||
'results': [{
|
||||
'id': 'HQqIOc8oYro',
|
||||
'alt_description': 'brown fox sitting on green grass field during daytime',
|
||||
'urls': {
|
||||
# 'regular': 'https://images.unsplash.com/photo-1462953491269-9aff00919695?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwzMDUwOHwwfDF8c2VhcmNofDF8fGZveHxlbnwwfHx8fDE2MzEwMzIzNDE&ixlib=rb-1.2.1&q=80&w=1080',
|
||||
'regular': BASE_URL + '/website/static/src/img/phone.png',
|
||||
},
|
||||
'links': {
|
||||
# 'download_location': 'https://api.unsplash.com/photos/HQqIOc8oYro/download?ixid=MnwzMDUwOHwwfDF8c2VhcmNofDF8fGZveHxlbnwwfHx8fDE2MzEwMzIzNDE'
|
||||
'download_location': BASE_URL + '/website/static/src/img/phone.png',
|
||||
},
|
||||
'user': {
|
||||
'name': 'Mitchell Admin',
|
||||
'links': {
|
||||
'html': BASE_URL,
|
||||
},
|
||||
},
|
||||
}]
|
||||
}
|
||||
# because not preprocessed by ControllerType metaclass
|
||||
fetch_unsplash_images.original_endpoint.routing_type = 'json'
|
||||
# disable undraw, no third party should be called in tests
|
||||
self.patch(Web_Unsplash, 'fetch_unsplash_images', fetch_unsplash_images)
|
||||
|
||||
self.start_tour(self.env['website'].get_client_action_url('/test_image_progress'), 'test_image_upload_progress_unsplash', login="admin")
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from urllib.parse import urlparse
|
||||
import odoo.tests
|
||||
import lxml
|
||||
|
||||
|
||||
@odoo.tests.common.tagged('post_install', '-at_install')
|
||||
class TestIsMultiLang(odoo.tests.HttpCase):
|
||||
|
||||
def test_01_is_multilang_url(self):
|
||||
website = self.env['website'].search([], limit=1)
|
||||
fr = self.env.ref('base.lang_fr').sudo()
|
||||
en = self.env.ref('base.lang_en').sudo()
|
||||
|
||||
fr.active = True
|
||||
fr_prefix = "/" + fr.iso_code
|
||||
|
||||
website.default_lang_id = en
|
||||
website.language_ids = en + fr
|
||||
|
||||
for data in [None, {'post': True}]: # GET / POST
|
||||
body = lxml.html.fromstring(self.url_open('/fr/multi_url', data=data).content)
|
||||
|
||||
self.assertEqual(fr_prefix + '/get', body.find('./a[@id="get"]').get('href'))
|
||||
self.assertEqual(fr_prefix + '/post', body.find('./form[@id="post"]').get('action'))
|
||||
self.assertEqual(fr_prefix + '/get_post', body.find('./a[@id="get_post"]').get('href'))
|
||||
self.assertEqual('/get_post_nomultilang', body.find('./a[@id="get_post_nomultilang"]').get('href'))
|
||||
|
||||
def test_02_url_lang_code_underscore(self):
|
||||
website = self.env['website'].browse(1)
|
||||
it = self.env.ref('base.lang_it').sudo()
|
||||
en = self.env.ref('base.lang_en').sudo()
|
||||
be = self.env.ref('base.lang_fr_BE').sudo()
|
||||
country1 = self.env['res.country'].create({'name': "My Super Country"})
|
||||
|
||||
it.active = True
|
||||
be.active = True
|
||||
website.domain = self.base_url() # for _is_canonical_url
|
||||
website.default_lang_id = en
|
||||
website.language_ids = en + it + be
|
||||
country1.update_field_translations('name', {
|
||||
it.code: country1.name + ' Italia',
|
||||
be.code: country1.name + ' Belgium'
|
||||
})
|
||||
|
||||
r = self.url_open(f'/test_lang_url/{country1.id}')
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(urlparse(r.url).path, f'/test_lang_url/my-super-country-{country1.id}')
|
||||
|
||||
r = self.url_open(f'/{it.url_code}/test_lang_url/{country1.id}')
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(urlparse(r.url).path, f'/{it.url_code}/test_lang_url/my-super-country-italia-{country1.id}')
|
||||
|
||||
body = lxml.html.fromstring(r.content)
|
||||
# Note: this test is indirectly testing the `ref=canonical` tag is correctly set,
|
||||
# as it is required in order for `rel=alternate` tags to be inserted in the DOM
|
||||
it_href = body.find('./head/link[@rel="alternate"][@hreflang="it"]').get('href')
|
||||
fr_href = body.find('./head/link[@rel="alternate"][@hreflang="fr"]').get('href')
|
||||
en_href = body.find('./head/link[@rel="alternate"][@hreflang="en"]').get('href')
|
||||
|
||||
self.assertEqual(urlparse(it_href).path, f'/{it.url_code}/test_lang_url/my-super-country-italia-{country1.id}')
|
||||
self.assertEqual(urlparse(fr_href).path, f'/{be.url_code}/test_lang_url/my-super-country-belgium-{country1.id}')
|
||||
self.assertEqual(urlparse(en_href).path, f'/test_lang_url/my-super-country-{country1.id}')
|
||||
|
||||
def test_03_head_alternate_href(self):
|
||||
website = self.env['website'].search([], limit=1)
|
||||
be = self.env.ref('base.lang_fr_BE').sudo()
|
||||
en = self.env.ref('base.lang_en').sudo()
|
||||
|
||||
be.active = True
|
||||
be_prefix = "/" + be.iso_code
|
||||
|
||||
website.default_lang_id = en
|
||||
website.language_ids = en + be
|
||||
|
||||
# alternate href should be use the current url.
|
||||
self.url_open(be_prefix)
|
||||
self.url_open(be_prefix + '/contactus')
|
||||
r = self.url_open(be_prefix)
|
||||
self.assertRegex(r.text, r'<link rel="alternate" hreflang="en" href="http://[^"]+/"/>')
|
||||
r = self.url_open(be_prefix + '/contactus')
|
||||
self.assertRegex(r.text, r'<link rel="alternate" hreflang="en" href="http://[^"]+/contactus"/>')
|
||||
|
||||
def test_04_multilang_false(self):
|
||||
website = self.env['website'].search([], limit=1)
|
||||
fr = self.env.ref('base.lang_fr').sudo()
|
||||
en = self.env.ref('base.lang_en').sudo()
|
||||
fr.active = True
|
||||
|
||||
website.default_lang_id = en
|
||||
website.language_ids = en + fr
|
||||
self.opener.cookies['frontend_lang'] = fr.iso_code
|
||||
|
||||
res = self.url_open('/get_post_nomultilang', allow_redirects=False)
|
||||
res.raise_for_status()
|
||||
|
||||
self.assertEqual(res.status_code, 200, "Should not be redirected")
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
|
||||
import odoo.tests
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@odoo.tests.common.tagged('post_install', '-at_install')
|
||||
class TestMedia(odoo.tests.HttpCase):
|
||||
|
||||
@mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
|
||||
def test_01_replace_media(self):
|
||||
SVG = base64.b64encode(b'<svg xmlns="http://www.w3.org/2000/svg"></svg>')
|
||||
self.env['ir.attachment'].create({
|
||||
'name': 'sample.svg',
|
||||
'public': True,
|
||||
'mimetype': 'image/svg+xml',
|
||||
'datas': SVG,
|
||||
})
|
||||
self.start_tour("/", 'test_replace_media', login="admin")
|
||||
|
||||
def test_02_image_link(self):
|
||||
self.start_tour("/", 'test_image_link', login="admin")
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from lxml import html
|
||||
|
||||
from odoo.addons.website.tools import MockRequest
|
||||
from odoo.tests import tagged, HttpCase
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestWebsiteMenu(HttpCase):
|
||||
|
||||
def test_menu_active_element(self):
|
||||
records = self.env['test.model'].create([{
|
||||
'name': "Record 1",
|
||||
'is_published': True,
|
||||
}, {
|
||||
'name': "Record 2",
|
||||
'is_published': True,
|
||||
}])
|
||||
|
||||
controller_url = '/test_website/model_item/'
|
||||
website = self.env['website'].browse(1)
|
||||
|
||||
self.env['website.menu'].create([{
|
||||
'name': records[0].name,
|
||||
'url': f"{controller_url}{records[0].id}",
|
||||
'parent_id': website.menu_id.id,
|
||||
'website_id': website.id,
|
||||
'sequence': 10,
|
||||
}, {
|
||||
'name': records[1].name,
|
||||
'url': f"{controller_url}{records[1].id}",
|
||||
'parent_id': website.menu_id.id,
|
||||
'website_id': website.id,
|
||||
'sequence': 20,
|
||||
}])
|
||||
for record in records:
|
||||
record_url = f"{controller_url}{record.id}"
|
||||
with MockRequest(self.env, website=website, url_root='', path=record_url):
|
||||
tree = html.fromstring(self.env['ir.qweb']._render('test_website.model_item', {
|
||||
'record': record,
|
||||
'main_object': record,
|
||||
}))
|
||||
menu_link_el = tree.xpath(".//*[@id='top_menu']//a[@href='%s' and contains(@class, 'active')]" % record_url)
|
||||
self.assertEqual(len(menu_link_el), 1, "The menu link related to the current record should be active")
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests.common import HttpCase, tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestMultiCompany(HttpCase):
|
||||
|
||||
def test_company_in_context(self):
|
||||
""" Test website company is set in context """
|
||||
website = self.env.ref('website.default_website')
|
||||
company = self.env['res.company'].create({'name': "Adaa"})
|
||||
website.company_id = company
|
||||
response = self.url_open('/multi_company_website')
|
||||
self.assertEqual(response.json()[0], company.id)
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import HttpCase, tagged
|
||||
from odoo.tests.common import HOST
|
||||
from odoo.tools import config, mute_logger
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class WithContext(HttpCase):
|
||||
def test_01_homepage_url(self):
|
||||
# Setup
|
||||
website = self.env['website'].browse([1])
|
||||
website.write({
|
||||
'name': 'Test Website',
|
||||
'domain': f'http://{HOST}:{config["http_port"]}',
|
||||
'homepage_url': '/unexisting',
|
||||
})
|
||||
home_url = '/'
|
||||
contactus_url = '/contactus'
|
||||
contactus_url_full = website.domain + contactus_url
|
||||
contactus_content = b'content="Contact Us | Test Website"'
|
||||
self.env['website.menu'].search([
|
||||
('website_id', '=', website.id),
|
||||
('url', '=', contactus_url),
|
||||
]).sequence = 1
|
||||
|
||||
# 404 shouldn't be served but fallback on first menu
|
||||
# -------------------------------------------
|
||||
# / page exists | first menu | homepage_url
|
||||
# -------------------------------------------
|
||||
# yes | /contactus | /unexisting
|
||||
# -------------------------------------------
|
||||
r = self.url_open(website.homepage_url)
|
||||
self.assertEqual(r.status_code, 404, "The website homepage_url should be a 404")
|
||||
r = self.url_open(home_url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.history[0].status_code, 303)
|
||||
self.assertURLEqual(r.url, contactus_url_full)
|
||||
self.assertIn(contactus_content, r.content)
|
||||
|
||||
# same with 403
|
||||
# -------------------------------------------
|
||||
# / page exists | first menu | homepage_url
|
||||
# -------------------------------------------
|
||||
# yes | /contactus | /test_website/200/name-1
|
||||
# -------------------------------------------
|
||||
rec_unpublished = self.env['test.model'].create({
|
||||
'name': 'name',
|
||||
'is_published': False,
|
||||
})
|
||||
website.homepage_url = f"/test_website/200/name-{rec_unpublished.id}"
|
||||
with mute_logger('odoo.http'): # mute 403 warning
|
||||
r = self.url_open(website.homepage_url)
|
||||
self.assertEqual(r.status_code, 404, "The website homepage_url should be a 404")
|
||||
r = self.url_open(home_url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.history[0].status_code, 303)
|
||||
self.assertURLEqual(r.url, contactus_url_full)
|
||||
self.assertIn(contactus_content, r.content)
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import odoo.tests
|
||||
|
||||
|
||||
@odoo.tests.common.tagged('post_install', '-at_install')
|
||||
class TestWebsitePageManager(odoo.tests.HttpCase):
|
||||
def test_page_manager_test_model(self):
|
||||
if self.env['website'].search_count([]) == 1:
|
||||
website2 = self.env['website'].create({
|
||||
'name': 'My Website 2',
|
||||
'domain': '',
|
||||
'sequence': 20,
|
||||
})
|
||||
else:
|
||||
website2 = self.env['website'].search([], order='id desc', limit=1)
|
||||
self.env['test.model.multi.website'].create({'name': 'Test Model Multi Website 2', 'website_id': website2.id})
|
||||
self.assertTrue(
|
||||
len(set([t.website_id.id for t in self.env['test.model.multi.website'].search([])])) >= 3,
|
||||
"There should at least be one record without website_id and one for 2 different websites",
|
||||
)
|
||||
self.assertNotIn('website_id', self.env['test.model']._fields)
|
||||
self.start_tour('/web#action=test_website.action_test_model_multi_website', 'test_website_page_manager', login="admin")
|
||||
# This second test is about ensuring that you can switch from a list
|
||||
# view which has no `website_pages_list` js_class to its kanban view
|
||||
self.start_tour('/web#action=test_website.action_test_model_multi_website_js_class_bug', 'test_website_page_manager_js_class_bug', login="admin")
|
||||
self.start_tour('/web#action=test_website.action_test_model', 'test_website_page_manager_no_website_id', login="admin")
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.website.tests.test_performance import UtilPerf
|
||||
|
||||
|
||||
class TestPerformance(UtilPerf):
|
||||
def test_10_perf_sql_website_controller_minimalist(self):
|
||||
url = '/empty_controller_test'
|
||||
select_tables_perf = {
|
||||
'base_registry_signaling': 1,
|
||||
}
|
||||
self._check_url_hot_query(url, 1, select_tables_perf)
|
||||
self.assertEqual(self._get_url_hot_query(url, cache=False), 1)
|
||||
|
|
@ -0,0 +1,320 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import odoo
|
||||
from odoo.tests import HttpCase, tagged
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.addons.http_routing.models.ir_http import slug
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestRedirect(HttpCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestRedirect, self).setUp()
|
||||
|
||||
self.user_portal = self.env['res.users'].with_context({'no_reset_password': True}).create({
|
||||
'name': 'Test Website Portal User',
|
||||
'login': 'portal_user',
|
||||
'password': 'portal_user',
|
||||
'email': 'portal_user@mail.com',
|
||||
'groups_id': [(6, 0, [self.env.ref('base.group_portal').id])]
|
||||
})
|
||||
|
||||
def test_01_redirect_308_model_converter(self):
|
||||
|
||||
self.env['website.rewrite'].create({
|
||||
'name': 'Test Website Redirect',
|
||||
'redirect_type': '308',
|
||||
'url_from': '/test_website/country/<model("res.country"):country>',
|
||||
'url_to': '/redirected/country/<model("res.country"):country>',
|
||||
})
|
||||
country_ad = self.env.ref('base.ad')
|
||||
|
||||
""" Ensure 308 redirect with model converter works fine, including:
|
||||
- Correct & working redirect as public user
|
||||
- Correct & working redirect as logged in user
|
||||
- Correct replace of url_for() URLs in DOM
|
||||
"""
|
||||
url = '/test_website/country/' + slug(country_ad)
|
||||
redirect_url = url.replace('test_website', 'redirected')
|
||||
|
||||
# [Public User] Open the original url and check redirect OK
|
||||
r = self.url_open(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(r.url.endswith(redirect_url), "Ensure URL got redirected")
|
||||
self.assertTrue(country_ad.name in r.text, "Ensure the controller returned the expected value")
|
||||
self.assertTrue(redirect_url in r.text, "Ensure the url_for has replaced the href URL in the DOM")
|
||||
|
||||
# [Logged In User] Open the original url and check redirect OK
|
||||
self.authenticate("portal_user", "portal_user")
|
||||
r = self.url_open(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(r.url.endswith(redirect_url), "Ensure URL got redirected (2)")
|
||||
self.assertTrue('Logged In' in r.text, "Ensure logged in")
|
||||
self.assertTrue(country_ad.name in r.text, "Ensure the controller returned the expected value (2)")
|
||||
self.assertTrue(redirect_url in r.text, "Ensure the url_for has replaced the href URL in the DOM")
|
||||
|
||||
def test_redirect_308_by_method_url_rewrite(self):
|
||||
self.env['website.rewrite'].create({
|
||||
'name': 'Test Website Redirect',
|
||||
'redirect_type': '308',
|
||||
'url_from': url_from,
|
||||
'url_to': f'{url_from}_new',
|
||||
} for url_from in ('/get', '/post', '/get_post'))
|
||||
|
||||
self.env.ref('test_website.test_view').arch = '''
|
||||
<t>
|
||||
<a href="/get"></a><a href="/post"></a><a href="/get_post"></a>
|
||||
</t>
|
||||
'''
|
||||
|
||||
# [Public User] Open the /test_view url and ensure urls are rewritten
|
||||
r = self.url_open('/test_view')
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(
|
||||
r.content.strip(),
|
||||
b'<a href="/get_new"></a><a href="/post_new"></a><a href="/get_post_new"></a>'
|
||||
)
|
||||
|
||||
@mute_logger('odoo.http') # mute 403 warning
|
||||
def test_02_redirect_308_RequestUID(self):
|
||||
self.env['website.rewrite'].create({
|
||||
'name': 'Test Website Redirect',
|
||||
'redirect_type': '308',
|
||||
'url_from': '/test_website/200/<model("test.model"):rec>',
|
||||
'url_to': '/test_website/308/<model("test.model"):rec>',
|
||||
})
|
||||
|
||||
rec_published = self.env['test.model'].create({'name': 'name', 'website_published': True})
|
||||
rec_unpublished = self.env['test.model'].create({'name': 'name', 'website_published': False})
|
||||
|
||||
WebsiteHttp = odoo.addons.website.models.ir_http.Http
|
||||
|
||||
def _get_error_html(env, code, value):
|
||||
return str(code).split('_')[-1], f"CUSTOM {code}"
|
||||
|
||||
with patch.object(WebsiteHttp, '_get_error_html', _get_error_html):
|
||||
# Patch will avoid to display real 404 page and regenerate assets each time and unlink old one.
|
||||
# And it allow to be sur that exception id handled by handle_exception and return a "managed error" page.
|
||||
|
||||
# published
|
||||
resp = self.url_open(f"/test_website/200/name-{rec_published.id}", allow_redirects=False)
|
||||
self.assertEqual(resp.status_code, 308)
|
||||
self.assertURLEqual(resp.headers.get('Location'), f"/test_website/308/name-{rec_published.id}")
|
||||
|
||||
resp = self.url_open(f"/test_website/308/name-{rec_published.id}", allow_redirects=False)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
resp = self.url_open(f"/test_website/200/xx-{rec_published.id}", allow_redirects=False)
|
||||
self.assertEqual(resp.status_code, 308)
|
||||
self.assertURLEqual(resp.headers.get('Location'), f"/test_website/308/xx-{rec_published.id}")
|
||||
|
||||
resp = self.url_open(f"/test_website/308/xx-{rec_published.id}", allow_redirects=False)
|
||||
self.assertEqual(resp.status_code, 301)
|
||||
self.assertURLEqual(resp.headers.get('Location'), f"/test_website/308/name-{rec_published.id}")
|
||||
|
||||
resp = self.url_open(f"/test_website/200/xx-{rec_published.id}", allow_redirects=True)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertURLEqual(resp.url, f"/test_website/308/name-{rec_published.id}")
|
||||
|
||||
# unexisting
|
||||
resp = self.url_open("/test_website/200/name-100", allow_redirects=False)
|
||||
self.assertEqual(resp.status_code, 308)
|
||||
self.assertURLEqual(resp.headers.get('Location'), "/test_website/308/name-100")
|
||||
|
||||
resp = self.url_open("/test_website/308/name-100", allow_redirects=False)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
self.assertEqual(resp.text, "CUSTOM 404")
|
||||
|
||||
resp = self.url_open("/test_website/200/xx-100", allow_redirects=False)
|
||||
self.assertEqual(resp.status_code, 308)
|
||||
self.assertURLEqual(resp.headers.get('Location'), "/test_website/308/xx-100")
|
||||
|
||||
resp = self.url_open("/test_website/308/xx-100", allow_redirects=False)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
self.assertEqual(resp.text, "CUSTOM 404")
|
||||
|
||||
# unpublish
|
||||
resp = self.url_open(f"/test_website/200/name-{rec_unpublished.id}", allow_redirects=False)
|
||||
self.assertEqual(resp.status_code, 308)
|
||||
self.assertURLEqual(resp.headers.get('Location'), f"/test_website/308/name-{rec_unpublished.id}")
|
||||
|
||||
resp = self.url_open(f"/test_website/308/name-{rec_unpublished.id}", allow_redirects=False)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
self.assertEqual(resp.text, "CUSTOM 404")
|
||||
|
||||
resp = self.url_open(f"/test_website/200/xx-{rec_unpublished.id}", allow_redirects=False)
|
||||
self.assertEqual(resp.status_code, 308)
|
||||
self.assertURLEqual(resp.headers.get('Location'), f"/test_website/308/xx-{rec_unpublished.id}")
|
||||
|
||||
resp = self.url_open(f"/test_website/308/xx-{rec_unpublished.id}", allow_redirects=False)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
self.assertEqual(resp.text, "CUSTOM 404")
|
||||
|
||||
# with seo_name as slug
|
||||
rec_published.seo_name = "seo_name"
|
||||
rec_unpublished.seo_name = "seo_name"
|
||||
|
||||
resp = self.url_open(f"/test_website/200/seo-name-{rec_published.id}", allow_redirects=False)
|
||||
self.assertEqual(resp.status_code, 308)
|
||||
self.assertURLEqual(resp.headers.get('Location'), f"/test_website/308/seo-name-{rec_published.id}")
|
||||
|
||||
resp = self.url_open(f"/test_website/308/seo-name-{rec_published.id}", allow_redirects=False)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
resp = self.url_open(f"/test_website/200/xx-{rec_unpublished.id}", allow_redirects=False)
|
||||
self.assertEqual(resp.status_code, 308)
|
||||
self.assertURLEqual(resp.headers.get('Location'), f"/test_website/308/xx-{rec_unpublished.id}")
|
||||
|
||||
resp = self.url_open(f"/test_website/308/xx-{rec_unpublished.id}", allow_redirects=False)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
self.assertEqual(resp.text, "CUSTOM 404")
|
||||
|
||||
resp = self.url_open("/test_website/200/xx-100", allow_redirects=False)
|
||||
self.assertEqual(resp.status_code, 308)
|
||||
self.assertURLEqual(resp.headers.get('Location'), "/test_website/308/xx-100")
|
||||
|
||||
resp = self.url_open("/test_website/308/xx-100", allow_redirects=False)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
self.assertEqual(resp.text, "CUSTOM 404")
|
||||
|
||||
def test_03_redirect_308_qs(self):
|
||||
self.env['website.rewrite'].create({
|
||||
'name': 'Test QS Redirect',
|
||||
'redirect_type': '308',
|
||||
'url_from': '/empty_controller_test',
|
||||
'url_to': '/empty_controller_test_redirected',
|
||||
})
|
||||
r = self.url_open('/test_website/test_redirect_view_qs?a=a')
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertIn(
|
||||
'href="/empty_controller_test_redirected?a=a"', r.text,
|
||||
"Redirection should have been applied, and query string should not have been duplicated.",
|
||||
)
|
||||
|
||||
@mute_logger('odoo.http') # mute 403 warning
|
||||
def test_04_redirect_301_route_unpublished_record(self):
|
||||
# 1. Accessing published record: Normal case, expecting 200
|
||||
rec1 = self.env['test.model'].create({
|
||||
'name': '301 test record',
|
||||
'is_published': True,
|
||||
})
|
||||
url_rec1 = '/test_website/200/' + slug(rec1)
|
||||
r = self.url_open(url_rec1)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# 2. Accessing unpublished record: expecting 404 for public users
|
||||
rec1.is_published = False
|
||||
r = self.url_open(url_rec1)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
# 3. Accessing unpublished record with redirect to a 404: expecting 404
|
||||
redirect = self.env['website.rewrite'].create({
|
||||
'name': 'Test 301 Redirect route unpublished record',
|
||||
'redirect_type': '301',
|
||||
'url_from': url_rec1,
|
||||
'url_to': '/404',
|
||||
})
|
||||
r = self.url_open(url_rec1)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
# 4. Accessing unpublished record with redirect to another published
|
||||
# record: expecting redirect to that record
|
||||
rec2 = rec1.copy({'is_published': True})
|
||||
url_rec2 = '/test_website/200/' + slug(rec2)
|
||||
redirect.url_to = url_rec2
|
||||
r = self.url_open(url_rec1)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(
|
||||
r.url.endswith(url_rec2),
|
||||
"Unpublished record should redirect to published record set in redirect")
|
||||
|
||||
@mute_logger('odoo.http')
|
||||
def test_05_redirect_404_notfound_record(self):
|
||||
# 1. Accessing unexisting record: raise 404
|
||||
url_rec1 = '/test_website/200/unexisting-100000'
|
||||
r = self.url_open(url_rec1)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
# 2. Accessing unpublished record with redirect to a 404: expecting 404
|
||||
redirect = self.env['website.rewrite'].create({
|
||||
'name': 'Test 301 Redirect route unexisting record',
|
||||
'redirect_type': '301',
|
||||
'url_from': url_rec1,
|
||||
'url_to': '/get',
|
||||
})
|
||||
r = self.url_open(url_rec1, allow_redirects=False)
|
||||
self.assertEqual(r.status_code, 301)
|
||||
self.assertURLEqual(r.headers.get('Location'), redirect.url_to)
|
||||
|
||||
r = self.url_open(url_rec1, allow_redirects=True)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertURLEqual(r.url, redirect.url_to)
|
||||
|
||||
def test_redirect_308_multiple_url_endpoint(self):
|
||||
self.env['website.rewrite'].create({
|
||||
'name': 'Test Multi URL 308',
|
||||
'redirect_type': '308',
|
||||
'url_from': '/test_countries_308',
|
||||
'url_to': '/test_countries_308_redirected',
|
||||
})
|
||||
rec1 = self.env['test.model'].create({
|
||||
'name': '301 test record',
|
||||
'is_published': True,
|
||||
})
|
||||
url_rec1 = f"/test_countries_308/{slug(rec1)}"
|
||||
|
||||
resp = self.url_open("/test_countries_308", allow_redirects=False)
|
||||
self.assertEqual(resp.status_code, 308)
|
||||
self.assertURLEqual(resp.headers.get('Location'), "/test_countries_308_redirected")
|
||||
|
||||
resp = self.url_open(url_rec1)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTrue(resp.url.endswith(url_rec1))
|
||||
|
||||
def test_redirect_with_qs(self):
|
||||
self.env['website.rewrite'].create({
|
||||
'name': 'Test 301 Redirect with qs',
|
||||
'redirect_type': '301',
|
||||
'url_from': '/foo?bar=1',
|
||||
'url_to': '/new-page-01',
|
||||
})
|
||||
self.env['website.rewrite'].create({
|
||||
'name': 'Test 301 Redirect with qs',
|
||||
'redirect_type': '301',
|
||||
'url_from': '/foo?bar=2',
|
||||
'url_to': '/new-page-10?qux=2',
|
||||
})
|
||||
self.env['website.rewrite'].create({
|
||||
'name': 'Test 301 Redirect without qs',
|
||||
'redirect_type': '301',
|
||||
'url_from': '/foo',
|
||||
'url_to': '/new-page-11',
|
||||
})
|
||||
|
||||
# should match qs first
|
||||
resp = self.url_open("/foo?bar=1", allow_redirects=False)
|
||||
self.assertEqual(resp.status_code, 301)
|
||||
self.assertURLEqual(resp.headers.get('Location'), "/new-page-01?bar=1")
|
||||
|
||||
# should match qs first
|
||||
resp = self.url_open("/foo?bar=2", allow_redirects=False)
|
||||
self.assertEqual(resp.status_code, 301)
|
||||
self.assertURLEqual(resp.headers.get('Location'), "/new-page-10?qux=2&bar=2")
|
||||
|
||||
# should match no qs
|
||||
resp = self.url_open("/foo?bar=3", allow_redirects=False)
|
||||
self.assertEqual(resp.status_code, 301)
|
||||
self.assertURLEqual(resp.headers.get('Location'), "/new-page-11?bar=3")
|
||||
|
||||
resp = self.url_open("/foo", allow_redirects=False)
|
||||
self.assertEqual(resp.status_code, 301)
|
||||
self.assertURLEqual(resp.headers.get('Location'), "/new-page-11")
|
||||
|
||||
# we dont support wrong get order
|
||||
# purpose is to support simple case like content.asp?id=xx
|
||||
resp = self.url_open("/foo?oups=1&bar=2", allow_redirects=False)
|
||||
self.assertEqual(resp.status_code, 301)
|
||||
self.assertURLEqual(resp.headers.get('Location'), "/new-page-11?oups=1&bar=2")
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import re
|
||||
|
||||
import odoo.tests
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
def break_view(view, fr='<p>placeholder</p>', to='<p t-field="no_record.exist"/>'):
|
||||
view.arch = view.arch.replace(fr, to)
|
||||
|
||||
|
||||
@odoo.tests.common.tagged('post_install', '-at_install')
|
||||
class TestWebsiteResetViews(odoo.tests.HttpCase):
|
||||
|
||||
def fix_it(self, page, mode='soft'):
|
||||
self.authenticate("admin", "admin")
|
||||
resp = self.url_open(page)
|
||||
self.assertEqual(resp.status_code, 500, "Waiting 500")
|
||||
self.assertTrue('<button data-mode="soft" class="reset_templates_button' in resp.text)
|
||||
data = {'view_id': self.find_template(resp), 'redirect': page, 'mode': mode}
|
||||
resp = self.url_open('/website/reset_template', data)
|
||||
self.assertEqual(resp.status_code, 200, "Waiting 200")
|
||||
|
||||
def find_template(self, response):
|
||||
find = re.search(r'<input.*type="hidden".*name="view_id".*value="([0-9]+)?"', response.text)
|
||||
return find and find.group(1)
|
||||
|
||||
def setUp(self):
|
||||
super(TestWebsiteResetViews, self).setUp()
|
||||
self.Website = self.env['website']
|
||||
self.View = self.env['ir.ui.view']
|
||||
self.test_view = self.Website.viewref('test_website.test_view')
|
||||
|
||||
@mute_logger('odoo.http')
|
||||
def test_01_reset_specific_page_view(self):
|
||||
self.test_page_view = self.Website.viewref('test_website.test_page_view')
|
||||
total_views = self.View.search_count([('type', '=', 'qweb')])
|
||||
# Trigger COW then break the QWEB XML on it
|
||||
break_view(self.test_page_view.with_context(website_id=1))
|
||||
self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view")
|
||||
self.fix_it('/test_page_view')
|
||||
|
||||
@mute_logger('odoo.http')
|
||||
def test_02_reset_specific_view_controller(self):
|
||||
total_views = self.View.search_count([('type', '=', 'qweb')])
|
||||
# Trigger COW then break the QWEB XML on it
|
||||
# `t-att-data="no_record.exist"` will test the case where exception.html contains branding
|
||||
break_view(self.test_view.with_context(website_id=1), to='<p t-att-data="no_record.exist" />')
|
||||
self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view")
|
||||
self.fix_it('/test_view')
|
||||
|
||||
@mute_logger('odoo.http')
|
||||
def test_03_reset_specific_view_controller_t_called(self):
|
||||
self.test_view_to_be_t_called = self.Website.viewref('test_website.test_view_to_be_t_called')
|
||||
|
||||
total_views = self.View.search_count([('type', '=', 'qweb')])
|
||||
# Trigger COW then break the QWEB XML on it
|
||||
break_view(self.test_view_to_be_t_called.with_context(website_id=1))
|
||||
break_view(self.test_view, to='<t t-call="test_website.test_view_to_be_t_called"/>')
|
||||
self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view")
|
||||
self.fix_it('/test_view')
|
||||
|
||||
@mute_logger('odoo.http')
|
||||
def test_04_reset_specific_view_controller_inherit(self):
|
||||
self.test_view_child_broken = self.Website.viewref('test_website.test_view_child_broken')
|
||||
|
||||
# Activate and break the inherited view
|
||||
self.test_view_child_broken.active = True
|
||||
break_view(self.test_view_child_broken.with_context(website_id=1, load_all_views=True))
|
||||
|
||||
self.fix_it('/test_view')
|
||||
|
||||
# This test work in real life, but not in test mode since we cannot rollback savepoint.
|
||||
# @mute_logger('odoo.http', 'odoo.addons.website.models.ir_ui_view')
|
||||
# def test_05_reset_specific_view_controller_broken_request(self):
|
||||
# total_views = self.View.search_count([('type', '=', 'qweb')])
|
||||
# # Trigger COW then break the QWEB XML on it
|
||||
# break_view(self.test_view.with_context(website_id=1), to='<t t-esc="request.env[\'website\'].browse(\'a\').name" />')
|
||||
# self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view (1)")
|
||||
# self.fix_it('/test_view')
|
||||
|
||||
# also mute ir.ui.view as `_get_view_id()` will raise "Could not find view object with xml_id 'no_record.exist'""
|
||||
@mute_logger('odoo.http', 'odoo.addons.website.models.ir_ui_view')
|
||||
def test_06_reset_specific_view_controller_inexisting_template(self):
|
||||
total_views = self.View.search_count([('type', '=', 'qweb')])
|
||||
# Trigger COW then break the QWEB XML on it
|
||||
break_view(self.test_view.with_context(website_id=1), to='<t t-call="no_record.exist"/>')
|
||||
self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view (2)")
|
||||
self.fix_it('/test_view')
|
||||
|
||||
@mute_logger('odoo.http')
|
||||
def test_07_reset_page_view_complete_flow(self):
|
||||
self.start_tour(self.env['website'].get_client_action_url('/test_page_view'), 'test_reset_page_view_complete_flow_part1', login="admin")
|
||||
self.fix_it('/test_page_view')
|
||||
self.start_tour(self.env['website'].get_client_action_url('/test_page_view'), 'test_reset_page_view_complete_flow_part2', login="admin")
|
||||
self.fix_it('/test_page_view')
|
||||
|
||||
@mute_logger('odoo.http')
|
||||
def test_08_reset_specific_page_view_hard_mode(self):
|
||||
self.test_page_view = self.Website.viewref('test_website.test_page_view')
|
||||
total_views = self.View.search_count([('type', '=', 'qweb')])
|
||||
# Trigger COW then break the QWEB XML on it
|
||||
break_view(self.test_page_view.with_context(website_id=1))
|
||||
# Break it again to have a previous arch different than file arch
|
||||
break_view(self.test_page_view.with_context(website_id=1))
|
||||
self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view")
|
||||
with self.assertRaises(AssertionError):
|
||||
# soft reset should not be able to reset the view as previous
|
||||
# version is also broken
|
||||
self.fix_it('/test_page_view')
|
||||
self.fix_it('/test_page_view', 'hard')
|
||||
# hard reset should set arch_updated to false
|
||||
self.assertFalse(self.test_page_view.arch_updated)
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from lxml import html
|
||||
|
||||
import odoo.tests
|
||||
|
||||
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
|
||||
|
||||
|
||||
@odoo.tests.common.tagged('post_install', '-at_install')
|
||||
class TestWebsiteSession(HttpCaseWithUserDemo):
|
||||
|
||||
def test_01_run_test(self):
|
||||
self.start_tour('/', 'test_json_auth')
|
||||
|
||||
def test_branding_cache(self):
|
||||
def has_branding(html_text):
|
||||
el = html.fromstring(html_text)
|
||||
return el.xpath('//*[@data-oe-model="test.model"]')
|
||||
|
||||
self.user_demo.groups_id += self.env.ref('website.group_website_restricted_editor')
|
||||
self.user_demo.groups_id -= self.env.ref('website.group_website_designer')
|
||||
|
||||
# Create session for demo user.
|
||||
public_session = self.authenticate(None, None)
|
||||
demo_session = self.authenticate('demo', 'demo')
|
||||
record = self.env['test.model'].search([], limit=1)
|
||||
result = self.url_open(f'/test_website/model_item/{record.id}')
|
||||
self.assertTrue(has_branding(result.text), "Should have branding for user demo")
|
||||
|
||||
# Public user.
|
||||
self.opener.cookies['session_id'] = public_session.sid
|
||||
result = self.url_open(f'/test_website/model_item/{record.id}')
|
||||
self.assertFalse(has_branding(result.text), "Should have no branding for public user")
|
||||
|
||||
# Back to demo user.
|
||||
self.opener.cookies['session_id'] = demo_session.sid
|
||||
result = self.url_open(f'/test_website/model_item/{record.id}')
|
||||
self.assertTrue(has_branding(result.text), "Should have branding for user demo")
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import odoo
|
||||
import odoo.tests
|
||||
|
||||
@odoo.tests.tagged('-at_install', 'post_install')
|
||||
class TestWebsiteSettings(odoo.tests.HttpCase):
|
||||
def test_01_multi_website_settings(self):
|
||||
# If not enabled (like in demo data), landing on res.config will try
|
||||
# to disable module_sale_quotation_builder and raise an issue
|
||||
group_order_template = self.env.ref('sale_management.group_sale_order_template', raise_if_not_found=False)
|
||||
if group_order_template:
|
||||
self.env.ref('base.group_user').write({"implied_ids": [(4, group_order_template.id)]})
|
||||
self.env['website'].create({'name': "Website Test Settings", 'specific_user_account': True})
|
||||
self.start_tour("/web", 'website_settings_m2o_dirty', login="admin")
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests.common import HOST, new_test_user, tagged
|
||||
from odoo.tools import config, mute_logger
|
||||
|
||||
from odoo.addons.base.tests.common import HttpCase
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSystray(HttpCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.group_restricted_editor = cls.env.ref('website.group_website_restricted_editor')
|
||||
cls.group_tester = cls.env.ref('test_website.group_test_website_tester')
|
||||
# Do not rely on HttpCaseWithUserDemo to avoid having different user
|
||||
# definitions with and without demo data.
|
||||
cls.user_test = new_test_user(cls.env, login='testtest', website_id=False)
|
||||
other_website = cls.env['website'].create({
|
||||
'name': 'Other',
|
||||
})
|
||||
cls.env['ir.ui.view'].create({
|
||||
'name': "Patch to recognize other website",
|
||||
'website_id': other_website.id,
|
||||
'type': 'qweb',
|
||||
'inherit_id': cls.env.ref('test_website.test_model_page_layout').id,
|
||||
'arch': """
|
||||
<xpath expr="//span" position="after">
|
||||
<div>Other</div>
|
||||
</xpath>
|
||||
"""
|
||||
})
|
||||
|
||||
|
||||
@mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
|
||||
def test_01_admin(self):
|
||||
self.start_tour(self.env['website'].get_client_action_url('/test_model/1'), 'test_systray_admin', login="admin")
|
||||
|
||||
@mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
|
||||
def test_02_reditor_tester(self):
|
||||
self.user_test.groups_id |= self.group_restricted_editor
|
||||
self.user_test.groups_id |= self.group_tester
|
||||
self.start_tour(self.env['website'].get_client_action_url('/test_model/1'), 'test_systray_reditor_tester', login="testtest")
|
||||
|
||||
@mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
|
||||
def test_03_reditor_not_tester(self):
|
||||
self.user_test.groups_id |= self.group_restricted_editor
|
||||
self.user_test.groups_id = self.user_test.groups_id.filtered(lambda group: group != self.group_tester)
|
||||
self.assertNotIn(self.group_tester.id, self.user_test.groups_id.ids, "User should not be a group_tester")
|
||||
self.start_tour(self.env['website'].get_client_action_url('/test_model/1'), 'test_systray_reditor_not_tester', login="testtest")
|
||||
|
||||
@mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
|
||||
def test_04_not_reditor_tester(self):
|
||||
self.user_test.groups_id = self.user_test.groups_id.filtered(lambda group: group != self.group_restricted_editor)
|
||||
self.user_test.groups_id |= self.group_tester
|
||||
self.assertNotIn(self.group_restricted_editor.id, self.user_test.groups_id.ids, "User should not be a group_restricted_editor")
|
||||
self.start_tour(self.env['website'].get_client_action_url('/test_model/1'), 'test_systray_not_reditor_tester', login="testtest")
|
||||
|
||||
@mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
|
||||
def test_05_not_reditor_not_tester(self):
|
||||
self.user_test.groups_id = self.user_test.groups_id.filtered(lambda group: group not in [self.group_restricted_editor, self.group_tester])
|
||||
self.assertNotIn(self.group_restricted_editor.id, self.user_test.groups_id.ids, "User should not be a group_restricted_editor")
|
||||
self.assertNotIn(self.group_tester.id, self.user_test.groups_id.ids, "User should not be a group_tester")
|
||||
self.start_tour(self.env['website'].get_client_action_url('/test_model/1'), 'test_systray_not_reditor_not_tester', login="testtest")
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.website.tools import MockRequest
|
||||
from odoo.tests import standalone
|
||||
|
||||
|
||||
@standalone('cow_views', 'website_standalone')
|
||||
def test_01_cow_views_unlink_on_module_update(env):
|
||||
""" Ensure COW views are correctly removed during module update.
|
||||
Not removing the view could lead to traceback:
|
||||
- Having a view A
|
||||
- Having a view B that inherits from a view C
|
||||
- View B t-call view A
|
||||
- COW view B
|
||||
- Delete view A and B from module datas and update it
|
||||
- Rendering view C will crash since it will render child view B that
|
||||
t-call unexisting view A
|
||||
"""
|
||||
|
||||
View = env['ir.ui.view']
|
||||
Imd = env['ir.model.data']
|
||||
|
||||
update_module_base_view = env.ref('test_website.update_module_base_view')
|
||||
update_module_view_to_be_t_called = View.create({
|
||||
'name': 'View to be t-called',
|
||||
'type': 'qweb',
|
||||
'arch': '<div>I will be t-called</div>',
|
||||
'key': 'test_website.update_module_view_to_be_t_called',
|
||||
})
|
||||
update_module_child_view = View.create({
|
||||
'name': 'Child View',
|
||||
'mode': 'extension',
|
||||
'inherit_id': update_module_base_view.id,
|
||||
'arch': '''
|
||||
<div position="inside">
|
||||
<t t-call="test_website.update_module_view_to_be_t_called"/>
|
||||
</div>
|
||||
''',
|
||||
'key': 'test_website.update_module_child_view',
|
||||
})
|
||||
|
||||
# Create IMD so when updating the module the views will be removed (not found in file)
|
||||
Imd.create({
|
||||
'module': 'test_website',
|
||||
'name': 'update_module_view_to_be_t_called',
|
||||
'model': 'ir.ui.view',
|
||||
'res_id': update_module_view_to_be_t_called.id,
|
||||
})
|
||||
Imd.create({
|
||||
'module': 'test_website',
|
||||
'name': 'update_module_child_view',
|
||||
'model': 'ir.ui.view',
|
||||
'res_id': update_module_child_view.id,
|
||||
})
|
||||
|
||||
# Trigger COW on child view
|
||||
update_module_child_view.with_context(website_id=1).write({'name': 'Child View (W1)'})
|
||||
|
||||
# Ensure views are correctly setup
|
||||
msg = "View '%s' does not exist!"
|
||||
assert View.search_count([
|
||||
('type', '=', 'qweb'),
|
||||
('key', '=', update_module_child_view.key)
|
||||
]) == 2, msg % update_module_child_view.key
|
||||
assert bool(env.ref(update_module_view_to_be_t_called.key)),\
|
||||
msg % update_module_view_to_be_t_called.key
|
||||
assert bool(env.ref(update_module_base_view.key)), msg % update_module_base_view.key
|
||||
|
||||
# Upgrade the module
|
||||
test_website_module = env['ir.module.module'].search([('name', '=', 'test_website')])
|
||||
test_website_module.button_immediate_upgrade()
|
||||
env.reset() # clear the set of environments
|
||||
env = env() # get an environment that refers to the new registry
|
||||
|
||||
# Ensure generic views got removed
|
||||
view = env.ref('test_website.update_module_view_to_be_t_called', raise_if_not_found=False)
|
||||
assert not view, "Generic view did not get removed!"
|
||||
|
||||
# Ensure specific COW views got removed
|
||||
assert not env['ir.ui.view'].search_count([
|
||||
('type', '=', 'qweb'),
|
||||
('key', '=', 'test_website.update_module_child_view'),
|
||||
]), "Specific COW views did not get removed!"
|
||||
|
||||
|
||||
@standalone('theme_views', 'website_standalone')
|
||||
def test_02_copy_ids_views_unlink_on_module_update(env):
|
||||
""" Ensure copy_ids views are correctly removed during module update.
|
||||
- Having an ir.ui.view A in the codebase, eg `website.layout`
|
||||
- Having a theme.ir.ui.view B in a theme, inheriting ir.ui.view A
|
||||
- Removing the theme.ir.ui.view B from the XML file and then updating the
|
||||
theme for a particular website should:
|
||||
1. Remove the theme.ir.ui.view record, which is the record pointed by the
|
||||
ir.model.data
|
||||
-> This is done through the regular Odoo behavior related to the
|
||||
ir.model.data and XML file check on upgrade.
|
||||
2. Remove the theme.ir.ui.view's copy_ids (sort of the COW views)
|
||||
-> Not working for now
|
||||
3. (not impact other website using this theme, see below)
|
||||
-> This is done through odoo/odoo@96ef4885a79 but did not come with
|
||||
tests
|
||||
|
||||
Point 2. was not working, this test aims to ensure it will now.
|
||||
Note: This can't be done through a `ondelete=cascade` as this would
|
||||
impact other websites when modifying a specific website. This would
|
||||
be against the multi-website rule:
|
||||
"What is done on a website should not alter other websites."
|
||||
|
||||
Regarding the flow described above, if a theme module was updated
|
||||
through the command line (or via the UI, but this is not possible in
|
||||
standard as theme modules are hidden from the Apps), it should
|
||||
update every website using this theme.
|
||||
"""
|
||||
View = env['ir.ui.view']
|
||||
ThemeView = env['theme.ir.ui.view']
|
||||
Imd = env['ir.model.data']
|
||||
|
||||
website_1 = env['website'].browse(1)
|
||||
website_2 = env['website'].browse(2)
|
||||
theme_default = env.ref('base.module_theme_default')
|
||||
|
||||
# Install theme_default on website 1 and website 2
|
||||
(website_1 + website_2).theme_id = theme_default
|
||||
env['ir.module.module'].with_context(load_all_views=True)._theme_load(website_1)
|
||||
env['ir.module.module'].with_context(load_all_views=True)._theme_load(website_2)
|
||||
|
||||
key = 'theme_default.theme_child_view'
|
||||
domain = [
|
||||
('type', '=', 'qweb'),
|
||||
('key', '=', key),
|
||||
]
|
||||
|
||||
def _simulate_xml_view():
|
||||
# Simulate a theme.ir.ui.view inside theme_default XML files
|
||||
base_view = env.ref('test_website.update_module_base_view')
|
||||
theme_child_view = ThemeView.create({
|
||||
'name': 'Theme Child View',
|
||||
'mode': 'extension',
|
||||
'inherit_id': f'{base_view._name},{base_view.id}',
|
||||
'arch': '''
|
||||
<div position="inside">
|
||||
<p>, and I am inherited by a theme.ir.ui.view</p>
|
||||
</div>
|
||||
''',
|
||||
'key': key,
|
||||
})
|
||||
# Create IMD so when updating the module the views will be removed (not found in file)
|
||||
Imd.create({
|
||||
'module': 'theme_default',
|
||||
'name': 'theme_child_view',
|
||||
'model': 'theme.ir.ui.view',
|
||||
'res_id': theme_child_view.id,
|
||||
})
|
||||
# Simulate the theme.ir.ui.view being installed on website 1 and 2
|
||||
View.create([
|
||||
theme_child_view._convert_to_base_model(website_1),
|
||||
theme_child_view._convert_to_base_model(website_2),
|
||||
])
|
||||
|
||||
# Ensure views are correctly setup: the theme.ir.ui.view should have been
|
||||
# copied to an ir.ui.view for website 1
|
||||
view_website_1, view_website_2 = View.search(domain + [
|
||||
('theme_template_id', '=', theme_child_view.id),
|
||||
('website_id', 'in', (website_1 + website_2).ids),
|
||||
])
|
||||
assert (
|
||||
set((view_website_1 + view_website_2)).issubset(theme_child_view.copy_ids)
|
||||
and view_website_1.website_id == website_1
|
||||
and view_website_2.website_id == website_2
|
||||
), "Theme View should have been copied to the website."
|
||||
|
||||
return view_website_1, view_website_2, theme_child_view
|
||||
|
||||
##########################################
|
||||
# CASE 1: generic update (-u, migration) #
|
||||
##########################################
|
||||
|
||||
view_website_1, view_website_2, theme_child_view = _simulate_xml_view()
|
||||
|
||||
# Upgrade the module
|
||||
theme_default.button_immediate_upgrade()
|
||||
env.reset() # clear the set of environments
|
||||
env = env() # get an environment that refers to the new registry
|
||||
|
||||
# Ensure the theme.ir.ui.view got removed (since there is an IMD but not
|
||||
# present in XML files)
|
||||
view = env.ref('theme_default.theme_child_view', False)
|
||||
assert not view, "Theme view should have been removed during module update."
|
||||
assert not theme_child_view.exists(),\
|
||||
"Theme view should have been removed during module update. (2)"
|
||||
|
||||
# Ensure copy_ids view got removed (and is not a leftover orphan)
|
||||
assert not View.search(domain), "copy_ids views did not get removed!"
|
||||
assert not (view_website_1.exists() or view_website_2.exists()),\
|
||||
"copy_ids views did not get removed! (2)"
|
||||
|
||||
#####################################################
|
||||
# CASE 2: specific update (website theme selection) #
|
||||
#####################################################
|
||||
|
||||
view_website_1, view_website_2, theme_child_view = _simulate_xml_view()
|
||||
|
||||
# Upgrade the module
|
||||
with MockRequest(env, website=website_1):
|
||||
theme_default.button_immediate_upgrade()
|
||||
env.reset() # clear the set of environments
|
||||
env = env() # get an environment that refers to the new registry
|
||||
|
||||
# Ensure the theme.ir.ui.view got removed (since there is an IMD but not
|
||||
# present in XML files)
|
||||
view = env.ref('theme_default.theme_child_view', False)
|
||||
assert not view, "Theme view should have been removed during module update."
|
||||
assert not theme_child_view.exists(),\
|
||||
"Theme view should have been removed during module update. (2)"
|
||||
|
||||
# Ensure only website_1 copy_ids got removed, website_2 should be untouched
|
||||
assert not view_website_1.exists() and view_website_2.exists(),\
|
||||
"Only website_1 copy should be removed (2)"
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="multi_url" model="website.page">
|
||||
<field name="name">Multi URL test</field>
|
||||
<field name="url">/multi_url</field>
|
||||
<field name="website_published">False</field>
|
||||
<field name="type">qweb</field>
|
||||
<field name="key">test_website.multi_url</field>
|
||||
<field name="website_published">True</field>
|
||||
<field name="arch" type="xml">
|
||||
<t t-name='multi_url'>
|
||||
<div>
|
||||
<a id='get' href="/get">get</a>
|
||||
<form id='post' action="/post">post</form>>
|
||||
<a id='get_post' href="/get_post">get_post</a>
|
||||
<a id='get_post_nomultilang' href="/get_post_nomultilang">get_post_nomultilang</a>
|
||||
</div>
|
||||
</t>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- /model_item item page -->
|
||||
<template id="model_item" name="Model item">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap">
|
||||
<section t-cache="record">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col" t-field="record.name"/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.test.website</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="website.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@id='plausbile_setting']" position="after">
|
||||
<div class="col-12 col-lg-6 o_setting_box" id="website_test_setting">
|
||||
<button type="object" name="action_website_test_setting" string="Website test setting" class="btn-link" icon="fa-arrow-right"/>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Front end page for test model -->
|
||||
<template id="test_model_page_layout" name="Test Model">
|
||||
<t t-call="website.layout">
|
||||
<t t-set="additional_title" t-value="test_model.name" />
|
||||
<span t-field="test_model.name"/>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
|
||||
<!-- test.model.multi.website views -->
|
||||
<record id="test_model_multi_website_view_kanban" model="ir.ui.view">
|
||||
<field name="name">test.model.multi.website.kanban</field>
|
||||
<field name="model">test.model.multi.website</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban js_class="website_pages_kanban" class="o_kanban_mobile" action="open_website_url" type="object" sample="1">
|
||||
<field name="name"/>
|
||||
<field name="website_url"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click d-flex flex-column">
|
||||
<div class="row mb-auto">
|
||||
<strong class="col-8">
|
||||
<span class="o_text_overflow" t-esc="record.name.value"/>
|
||||
<div class="text-muted" t-if="record.website_id.value" groups="website.group_multi_website">
|
||||
<i class="fa fa-globe me-1" title="Website"/>
|
||||
<field name="website_id"/>
|
||||
</div>
|
||||
</strong>
|
||||
</div>
|
||||
<div class="border-top mt-2 pt-2">
|
||||
<field name="is_published" widget="boolean_toggle"/>
|
||||
<t t-if="record.is_published.raw_value">Published</t>
|
||||
<t t-else="">Not Published</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
<record id="test_model_multi_website_view_list" model="ir.ui.view">
|
||||
<field name="name">Test Multi Model Pages Tree</field>
|
||||
<field name="model">test.model.multi.website</field>
|
||||
<field name="priority">99</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree js_class="website_pages_list" type="object" action="open_website_url" multi_edit="1">
|
||||
<field name="name"/>
|
||||
<field name="website_url"/>
|
||||
<field name="website_id" groups="website.group_multi_website"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_test_model_multi_website" model="ir.actions.act_window">
|
||||
<field name="name">Test Multi Model Pages</field>
|
||||
<field name="res_model">test.model.multi.website</field>
|
||||
<field name="view_mode">tree,kanban,form</field>
|
||||
<field name="view_ids" eval="[(5, 0, 0),
|
||||
(0, 0, {'view_mode': 'tree', 'view_id': ref('test_model_multi_website_view_list')}),
|
||||
(0, 0, {'view_mode': 'kanban', 'view_id': ref('test_model_multi_website_view_kanban')}),
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
<!-- js_class bug records -->
|
||||
<record id="test_model_multi_website_view_list_js_class_bug" model="ir.ui.view">
|
||||
<field name="name">Test Multi Model Pages Tree js_class bug</field>
|
||||
<field name="model">test.model.multi.website</field>
|
||||
<field name="priority">99</field>
|
||||
<!-- Omitting `website_pages_list` on purpose to test it does not crash -->
|
||||
<field name="arch" type="xml">
|
||||
<tree type="object" action="open_website_url" multi_edit="1">
|
||||
<field name="name"/>
|
||||
<field name="website_url"/>
|
||||
<field name="website_id" groups="website.group_multi_website"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_test_model_multi_website_js_class_bug" model="ir.actions.act_window">
|
||||
<field name="name">Test Multi Model Pages js_class bug</field>
|
||||
<field name="res_model">test.model.multi.website</field>
|
||||
<field name="view_mode">tree,kanban,form</field>
|
||||
<field name="view_ids" eval="[(5, 0, 0),
|
||||
(0, 0, {'view_mode': 'tree', 'view_id': ref('test_model_multi_website_view_list_js_class_bug')}),
|
||||
(0, 0, {'view_mode': 'kanban', 'view_id': ref('test_model_multi_website_view_kanban')}),
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
|
||||
<!-- test.model views -->
|
||||
<record id="test_model_view_kanban" model="ir.ui.view">
|
||||
<field name="name">test.model.kanban</field>
|
||||
<field name="model">test.model</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban js_class="website_pages_kanban" class="o_kanban_mobile" action="open_website_url" type="object" sample="1">
|
||||
<field name="name"/>
|
||||
<field name="website_url"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click d-flex flex-column">
|
||||
<div class="row mb-auto">
|
||||
<strong class="col-8">
|
||||
<span class="o_text_overflow" t-esc="record.name.value"/>
|
||||
</strong>
|
||||
</div>
|
||||
<div class="border-top mt-2 pt-2">
|
||||
<field name="is_published" widget="boolean_toggle"/>
|
||||
<t t-if="record.is_published.raw_value">Published</t>
|
||||
<t t-else="">Not Published</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
<record id="test_model_view_list" model="ir.ui.view">
|
||||
<field name="name">Test Model Pages Tree</field>
|
||||
<field name="model">test.model</field>
|
||||
<field name="priority">99</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree js_class="website_pages_list" type="object" action="open_website_url" multi_edit="1">
|
||||
<field name="name"/>
|
||||
<field name="website_url"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_test_model" model="ir.actions.act_window">
|
||||
<field name="name">Test Model Pages</field>
|
||||
<field name="res_model">test.model</field>
|
||||
<field name="view_mode">tree,kanban,form</field>
|
||||
<field name="view_ids" eval="[(5, 0, 0),
|
||||
(0, 0, {'view_mode': 'tree', 'view_id': ref('test_model_view_list')}),
|
||||
(0, 0, {'view_mode': 'kanban', 'view_id': ref('test_model_view_kanban')}),
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
<!-- Backend access to test model -->
|
||||
<record id="action_test_website_test_model" model="ir.actions.act_window">
|
||||
<field name="name">Test Model</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">test.model</field>
|
||||
<field name="view_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
<menuitem name="Test Model"
|
||||
id="menu_test_website_test_model"
|
||||
action="action_test_website_test_model"
|
||||
parent="website.menu_website_global_configuration"
|
||||
sequence="100"
|
||||
groups="base.group_no_one"/>
|
||||
|
||||
<record id="view_test_model_form" model="ir.ui.view">
|
||||
<field name="name">test.model.form</field>
|
||||
<field name="model">test.model</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Test Model">
|
||||
<sheet>
|
||||
<div name="button_box" position="inside">
|
||||
<field name="is_published" widget="website_redirect_button"/>
|
||||
</div>
|
||||
<div class="oe_title" name="title">
|
||||
<label for="name" string="Name"/>
|
||||
<h1>
|
||||
<field name="name"/>
|
||||
</h1>
|
||||
</div>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Loading…
Add table
Add a link
Reference in a new issue