mirror of
https://github.com/camptocamp/odoo-cloud-platform.git
synced 2026-06-23 18:04:34 +00:00
Change CI to GitHub actions
Use copier template from oca/oca-addons-repo-template Apply linting
This commit is contained in:
@@ -0,0 +1,25 @@
|
|||||||
|
# Do NOT update manually; changes here will be overwritten by Copier
|
||||||
|
_commit: v1.14.2
|
||||||
|
_src_path: https://github.com/OCA/oca-addons-repo-template.git
|
||||||
|
ci: GitHub
|
||||||
|
dependency_installation_mode: PIP
|
||||||
|
generate_requirements_txt: false
|
||||||
|
github_check_license: true
|
||||||
|
github_ci_extra_env: {}
|
||||||
|
github_enable_codecov: true
|
||||||
|
github_enable_makepot: false
|
||||||
|
github_enable_stale_action: true
|
||||||
|
github_enforce_dev_status_compatibility: false
|
||||||
|
include_wkhtmltopdf: false
|
||||||
|
odoo_version: 16.0
|
||||||
|
org_name: Camptocamp
|
||||||
|
org_slug: camptocamp
|
||||||
|
rebel_module_groups:
|
||||||
|
- attachment_azure,cloud_platform_azure
|
||||||
|
repo_description: ''
|
||||||
|
repo_name: Odoo Cloud Addons
|
||||||
|
repo_slug: odoo-cloud-platform
|
||||||
|
repo_website: https://github.com/camptocamp/odoo-cloud-platform
|
||||||
|
travis_apt_packages: []
|
||||||
|
travis_apt_sources: []
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Configuration for known file extensions
|
||||||
|
[*.{css,js,json,less,md,py,rst,sass,scss,xml,yaml,yml}]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.{json,yml,yaml,rst,md}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# Do not configure editor for libs and autogenerated content
|
||||||
|
[{*/static/{lib,src/lib}/**,*/static/description/index.html,*/readme/../README.rst}]
|
||||||
|
charset = unset
|
||||||
|
end_of_line = unset
|
||||||
|
indent_size = unset
|
||||||
|
indent_style = unset
|
||||||
|
insert_final_newline = false
|
||||||
|
trim_trailing_whitespace = false
|
||||||
+187
@@ -0,0 +1,187 @@
|
|||||||
|
env:
|
||||||
|
browser: true
|
||||||
|
es6: true
|
||||||
|
|
||||||
|
# See https://github.com/OCA/odoo-community.org/issues/37#issuecomment-470686449
|
||||||
|
parserOptions:
|
||||||
|
ecmaVersion: 2019
|
||||||
|
|
||||||
|
overrides:
|
||||||
|
- files:
|
||||||
|
- "**/*.esm.js"
|
||||||
|
parserOptions:
|
||||||
|
sourceType: module
|
||||||
|
|
||||||
|
# Globals available in Odoo that shouldn't produce errorings
|
||||||
|
globals:
|
||||||
|
_: readonly
|
||||||
|
$: readonly
|
||||||
|
fuzzy: readonly
|
||||||
|
jQuery: readonly
|
||||||
|
moment: readonly
|
||||||
|
odoo: readonly
|
||||||
|
openerp: readonly
|
||||||
|
owl: readonly
|
||||||
|
|
||||||
|
# Styling is handled by Prettier, so we only need to enable AST rules;
|
||||||
|
# see https://github.com/OCA/maintainer-quality-tools/pull/618#issuecomment-558576890
|
||||||
|
rules:
|
||||||
|
accessor-pairs: warn
|
||||||
|
array-callback-return: warn
|
||||||
|
callback-return: warn
|
||||||
|
capitalized-comments:
|
||||||
|
- warn
|
||||||
|
- always
|
||||||
|
- ignoreConsecutiveComments: true
|
||||||
|
ignoreInlineComments: true
|
||||||
|
complexity:
|
||||||
|
- warn
|
||||||
|
- 15
|
||||||
|
constructor-super: warn
|
||||||
|
dot-notation: warn
|
||||||
|
eqeqeq: warn
|
||||||
|
global-require: warn
|
||||||
|
handle-callback-err: warn
|
||||||
|
id-blacklist: warn
|
||||||
|
id-match: warn
|
||||||
|
init-declarations: error
|
||||||
|
max-depth: warn
|
||||||
|
max-nested-callbacks: warn
|
||||||
|
max-statements-per-line: warn
|
||||||
|
no-alert: warn
|
||||||
|
no-array-constructor: warn
|
||||||
|
no-caller: warn
|
||||||
|
no-case-declarations: warn
|
||||||
|
no-class-assign: warn
|
||||||
|
no-cond-assign: error
|
||||||
|
no-const-assign: error
|
||||||
|
no-constant-condition: warn
|
||||||
|
no-control-regex: warn
|
||||||
|
no-debugger: error
|
||||||
|
no-delete-var: warn
|
||||||
|
no-div-regex: warn
|
||||||
|
no-dupe-args: error
|
||||||
|
no-dupe-class-members: error
|
||||||
|
no-dupe-keys: error
|
||||||
|
no-duplicate-case: error
|
||||||
|
no-duplicate-imports: error
|
||||||
|
no-else-return: warn
|
||||||
|
no-empty-character-class: warn
|
||||||
|
no-empty-function: error
|
||||||
|
no-empty-pattern: error
|
||||||
|
no-empty: warn
|
||||||
|
no-eq-null: error
|
||||||
|
no-eval: error
|
||||||
|
no-ex-assign: error
|
||||||
|
no-extend-native: warn
|
||||||
|
no-extra-bind: warn
|
||||||
|
no-extra-boolean-cast: warn
|
||||||
|
no-extra-label: warn
|
||||||
|
no-fallthrough: warn
|
||||||
|
no-func-assign: error
|
||||||
|
no-global-assign: error
|
||||||
|
no-implicit-coercion:
|
||||||
|
- warn
|
||||||
|
- allow: ["~"]
|
||||||
|
no-implicit-globals: warn
|
||||||
|
no-implied-eval: warn
|
||||||
|
no-inline-comments: warn
|
||||||
|
no-inner-declarations: warn
|
||||||
|
no-invalid-regexp: warn
|
||||||
|
no-irregular-whitespace: warn
|
||||||
|
no-iterator: warn
|
||||||
|
no-label-var: warn
|
||||||
|
no-labels: warn
|
||||||
|
no-lone-blocks: warn
|
||||||
|
no-lonely-if: error
|
||||||
|
no-mixed-requires: error
|
||||||
|
no-multi-str: warn
|
||||||
|
no-native-reassign: error
|
||||||
|
no-negated-condition: warn
|
||||||
|
no-negated-in-lhs: error
|
||||||
|
no-new-func: warn
|
||||||
|
no-new-object: warn
|
||||||
|
no-new-require: warn
|
||||||
|
no-new-symbol: warn
|
||||||
|
no-new-wrappers: warn
|
||||||
|
no-new: warn
|
||||||
|
no-obj-calls: warn
|
||||||
|
no-octal-escape: warn
|
||||||
|
no-octal: warn
|
||||||
|
no-param-reassign: warn
|
||||||
|
no-path-concat: warn
|
||||||
|
no-process-env: warn
|
||||||
|
no-process-exit: warn
|
||||||
|
no-proto: warn
|
||||||
|
no-prototype-builtins: warn
|
||||||
|
no-redeclare: warn
|
||||||
|
no-regex-spaces: warn
|
||||||
|
no-restricted-globals: warn
|
||||||
|
no-restricted-imports: warn
|
||||||
|
no-restricted-modules: warn
|
||||||
|
no-restricted-syntax: warn
|
||||||
|
no-return-assign: error
|
||||||
|
no-script-url: warn
|
||||||
|
no-self-assign: warn
|
||||||
|
no-self-compare: warn
|
||||||
|
no-sequences: warn
|
||||||
|
no-shadow-restricted-names: warn
|
||||||
|
no-shadow: warn
|
||||||
|
no-sparse-arrays: warn
|
||||||
|
no-sync: warn
|
||||||
|
no-this-before-super: warn
|
||||||
|
no-throw-literal: warn
|
||||||
|
no-undef-init: warn
|
||||||
|
no-undef: error
|
||||||
|
no-unmodified-loop-condition: warn
|
||||||
|
no-unneeded-ternary: error
|
||||||
|
no-unreachable: error
|
||||||
|
no-unsafe-finally: error
|
||||||
|
no-unused-expressions: error
|
||||||
|
no-unused-labels: error
|
||||||
|
no-unused-vars: error
|
||||||
|
no-use-before-define: error
|
||||||
|
no-useless-call: warn
|
||||||
|
no-useless-computed-key: warn
|
||||||
|
no-useless-concat: warn
|
||||||
|
no-useless-constructor: warn
|
||||||
|
no-useless-escape: warn
|
||||||
|
no-useless-rename: warn
|
||||||
|
no-void: warn
|
||||||
|
no-with: warn
|
||||||
|
operator-assignment: [error, always]
|
||||||
|
prefer-const: warn
|
||||||
|
radix: warn
|
||||||
|
require-yield: warn
|
||||||
|
sort-imports: warn
|
||||||
|
spaced-comment: [error, always]
|
||||||
|
strict: [error, function]
|
||||||
|
use-isnan: error
|
||||||
|
valid-jsdoc:
|
||||||
|
- warn
|
||||||
|
- prefer:
|
||||||
|
arg: param
|
||||||
|
argument: param
|
||||||
|
augments: extends
|
||||||
|
constructor: class
|
||||||
|
exception: throws
|
||||||
|
func: function
|
||||||
|
method: function
|
||||||
|
prop: property
|
||||||
|
return: returns
|
||||||
|
virtual: abstract
|
||||||
|
yield: yields
|
||||||
|
preferType:
|
||||||
|
array: Array
|
||||||
|
bool: Boolean
|
||||||
|
boolean: Boolean
|
||||||
|
number: Number
|
||||||
|
object: Object
|
||||||
|
str: String
|
||||||
|
string: String
|
||||||
|
requireParamDescription: false
|
||||||
|
requireReturn: false
|
||||||
|
requireReturnDescription: false
|
||||||
|
requireReturnType: false
|
||||||
|
valid-typeof: warn
|
||||||
|
yoda: warn
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[flake8]
|
||||||
|
max-line-length = 88
|
||||||
|
max-complexity = 16
|
||||||
|
# B = bugbear
|
||||||
|
# B9 = bugbear opinionated (incl line length)
|
||||||
|
select = C,E,F,W,B,B9
|
||||||
|
# E203: whitespace before ':' (black behaviour)
|
||||||
|
# E501: flake8 line length (covered by bugbear B950)
|
||||||
|
# W503: line break before binary operator (black behaviour)
|
||||||
|
ignore = E203,E501,W503
|
||||||
|
per-file-ignores=
|
||||||
|
__init__.py:F401
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
name: pre-commit
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- "16.0*"
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "16.0"
|
||||||
|
- "16.0-ocabot-*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pre-commit:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
- name: Get python version
|
||||||
|
run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV
|
||||||
|
- uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: ~/.cache/pre-commit
|
||||||
|
key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }}
|
||||||
|
- name: Install pre-commit
|
||||||
|
run: pip install pre-commit
|
||||||
|
- name: Run pre-commit
|
||||||
|
run: pre-commit run --all-files --show-diff-on-failure --color=always
|
||||||
|
- name: Check that all files generated by pre-commit are in git
|
||||||
|
run: |
|
||||||
|
newfiles="$(git ls-files --others --exclude-from=.gitignore)"
|
||||||
|
if [ "$newfiles" != "" ] ; then
|
||||||
|
echo "Please check-in the following files:"
|
||||||
|
echo "$newfiles"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
name: Mark stale issues and pull requests
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 12 * * 0"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Stale PRs and issues policy
|
||||||
|
uses: actions/stale@v4
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# General settings.
|
||||||
|
ascending: true
|
||||||
|
remove-stale-when-updated: true
|
||||||
|
# Pull Requests settings.
|
||||||
|
# 120+30 day stale policy for PRs
|
||||||
|
# * Except PRs marked as "no stale"
|
||||||
|
days-before-pr-stale: 120
|
||||||
|
days-before-pr-close: 30
|
||||||
|
exempt-pr-labels: "no stale"
|
||||||
|
stale-pr-label: "stale"
|
||||||
|
stale-pr-message: >
|
||||||
|
There hasn't been any activity on this pull request in the past 4 months, so
|
||||||
|
it has been marked as stale and it will be closed automatically if no
|
||||||
|
further activity occurs in the next 30 days.
|
||||||
|
|
||||||
|
If you want this PR to never become stale, please ask a PSC member to apply
|
||||||
|
the "no stale" label.
|
||||||
|
# Issues settings.
|
||||||
|
# 180+30 day stale policy for open issues
|
||||||
|
# * Except Issues marked as "no stale"
|
||||||
|
days-before-issue-stale: 180
|
||||||
|
days-before-issue-close: 30
|
||||||
|
exempt-issue-labels: "no stale,needs more information"
|
||||||
|
stale-issue-label: "stale"
|
||||||
|
stale-issue-message: >
|
||||||
|
There hasn't been any activity on this issue in the past 6 months, so it has
|
||||||
|
been marked as stale and it will be closed automatically if no further
|
||||||
|
activity occurs in the next 30 days.
|
||||||
|
|
||||||
|
If you want this issue to never become stale, please ask a PSC member to
|
||||||
|
apply the "no stale" label.
|
||||||
|
|
||||||
|
# 15+30 day stale policy for issues pending more information
|
||||||
|
# * Issues that are pending more information
|
||||||
|
# * Except Issues marked as "no stale"
|
||||||
|
- name: Needs more information stale issues policy
|
||||||
|
uses: actions/stale@v4
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
ascending: true
|
||||||
|
only-labels: "needs more information"
|
||||||
|
exempt-issue-labels: "no stale"
|
||||||
|
days-before-stale: 15
|
||||||
|
days-before-close: 30
|
||||||
|
days-before-pr-stale: -1
|
||||||
|
days-before-pr-close: -1
|
||||||
|
remove-stale-when-updated: true
|
||||||
|
stale-issue-label: "stale"
|
||||||
|
stale-issue-message: >
|
||||||
|
This issue needs more information and there hasn't been any activity
|
||||||
|
recently, so it has been marked as stale and it will be closed automatically
|
||||||
|
if no further activity occurs in the next 30 days.
|
||||||
|
|
||||||
|
If you think this is a mistake, please ask a PSC member to remove the "needs
|
||||||
|
more information" label.
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
name: tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- "16.0*"
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "16.0"
|
||||||
|
- "16.0-ocabot-*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
unreleased-deps:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Detect unreleased dependencies
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- run: |
|
||||||
|
for reqfile in requirements.txt test-requirements.txt ; do
|
||||||
|
if [ -f ${reqfile} ] ; then
|
||||||
|
result=0
|
||||||
|
# reject non-comment lines that contain a / (i.e. URLs, relative paths)
|
||||||
|
grep "^[^#].*/" ${reqfile} || result=$?
|
||||||
|
if [ $result -eq 0 ] ; then
|
||||||
|
echo "Unreleased dependencies found in ${reqfile}."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
container: ${{ matrix.container }}
|
||||||
|
name: ${{ matrix.name }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- container: ghcr.io/oca/oca-ci/py3.10-odoo16.0:latest
|
||||||
|
include: "attachment_azure,cloud_platform_azure"
|
||||||
|
makepot: "false"
|
||||||
|
name: test azure with Odoo
|
||||||
|
- container: ghcr.io/oca/oca-ci/py3.10-ocb16.0:latest
|
||||||
|
include: "attachment_azure,cloud_platform_azure"
|
||||||
|
name: test azure with OCA
|
||||||
|
- container: ghcr.io/oca/oca-ci/py3.10-odoo16.0:latest
|
||||||
|
exclude: "attachment_s3,cloud_platform_exoscale,attachment_swift,cloud_platform_ovh,attachment_azure,cloud_platform_azure"
|
||||||
|
makepot: "false"
|
||||||
|
name: test others with Odoo
|
||||||
|
- container: ghcr.io/oca/oca-ci/py3.10-ocb16.0:latest
|
||||||
|
exclude: "attachment_s3,cloud_platform_exoscale,attachment_swift,cloud_platform_ovh,attachment_azure,cloud_platform_azure"
|
||||||
|
name: test others with OCB
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:12.0
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: odoo
|
||||||
|
POSTGRES_PASSWORD: odoo
|
||||||
|
POSTGRES_DB: odoo
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
env:
|
||||||
|
INCLUDE: "${{ matrix.include }}"
|
||||||
|
EXCLUDE: "${{ matrix.exclude }}"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Install addons and dependencies
|
||||||
|
run: oca_install_addons
|
||||||
|
- name: Check licenses
|
||||||
|
run: manifestoo -d . check-licenses
|
||||||
|
- name: Check development status
|
||||||
|
run: manifestoo -d . check-dev-status --default-dev-status=Beta
|
||||||
|
continue-on-error: true
|
||||||
|
- name: Initialize test db
|
||||||
|
run: oca_init_test_database
|
||||||
|
- name: Run tests
|
||||||
|
run: oca_run_tests
|
||||||
|
- uses: codecov/codecov-action@v1
|
||||||
|
- name: Update .pot files
|
||||||
|
run: oca_export_and_push_pot https://x-access-token:${{ secrets.GIT_PUSH_TOKEN }}@github.com/${{ github.repository }}
|
||||||
|
if: ${{ matrix.makepot == 'true' && github.event_name == 'push' && github.repository_owner == 'camptocamp' }}
|
||||||
+20
-3
@@ -1,6 +1,8 @@
|
|||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
/.venv
|
||||||
|
/.pytest_cache
|
||||||
|
|
||||||
# C extensions
|
# C extensions
|
||||||
*.so
|
*.so
|
||||||
@@ -13,8 +15,6 @@ build/
|
|||||||
develop-eggs/
|
develop-eggs/
|
||||||
dist/
|
dist/
|
||||||
eggs/
|
eggs/
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
lib64/
|
||||||
parts/
|
parts/
|
||||||
sdist/
|
sdist/
|
||||||
@@ -22,6 +22,7 @@ var/
|
|||||||
*.egg-info/
|
*.egg-info/
|
||||||
.installed.cfg
|
.installed.cfg
|
||||||
*.egg
|
*.egg
|
||||||
|
*.eggs
|
||||||
|
|
||||||
# Installer logs
|
# Installer logs
|
||||||
pip-log.txt
|
pip-log.txt
|
||||||
@@ -41,6 +42,19 @@ coverage.xml
|
|||||||
# Pycharm
|
# Pycharm
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
# Eclipse
|
||||||
|
.settings
|
||||||
|
|
||||||
|
# Visual Studio cache/options directory
|
||||||
|
.vs/
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# OSX Files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
|
||||||
# Mr Developer
|
# Mr Developer
|
||||||
.mr.developer.cfg
|
.mr.developer.cfg
|
||||||
.project
|
.project
|
||||||
@@ -50,8 +64,11 @@ coverage.xml
|
|||||||
.ropeproject
|
.ropeproject
|
||||||
|
|
||||||
# Sphinx documentation
|
# Sphinx documentation
|
||||||
connector/doc/_build/
|
docs/_build/
|
||||||
|
|
||||||
# Backup files
|
# Backup files
|
||||||
*~
|
*~
|
||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
|
# OCA rules
|
||||||
|
!static/lib/
|
||||||
|
|||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
[settings]
|
||||||
|
; see https://github.com/psf/black
|
||||||
|
multi_line_output=3
|
||||||
|
include_trailing_comma=True
|
||||||
|
force_grid_wrap=0
|
||||||
|
combine_as_imports=True
|
||||||
|
use_parentheses=True
|
||||||
|
line_length=88
|
||||||
|
known_odoo=odoo
|
||||||
|
known_odoo_addons=odoo.addons
|
||||||
|
sections=FUTURE,STDLIB,THIRDPARTY,ODOO,ODOO_ADDONS,FIRSTPARTY,LOCALFOLDER
|
||||||
|
default_section=THIRDPARTY
|
||||||
|
ensure_newline_before_comments = True
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
exclude: |
|
||||||
|
(?x)
|
||||||
|
# NOT INSTALLABLE ADDONS
|
||||||
|
^attachment_s3/|
|
||||||
|
^attachment_swift/|
|
||||||
|
^base_fileurl_field/|
|
||||||
|
^cloud_platform_exoscale/|
|
||||||
|
^cloud_platform_ovh/|
|
||||||
|
^monitoring_log_requests/|
|
||||||
|
^monitoring_statsd/|
|
||||||
|
^test_base_fileurl_field/|
|
||||||
|
# END NOT INSTALLABLE ADDONS
|
||||||
|
# Files and folders generated by bots, to avoid loops
|
||||||
|
^setup/|/static/description/index\.html$|
|
||||||
|
# We don't want to mess with tool-generated files
|
||||||
|
.svg$|/tests/([^/]+/)?cassettes/|^.copier-answers.yml$|^.github/|
|
||||||
|
# Maybe reactivate this when all README files include prettier ignore tags?
|
||||||
|
^README\.md$|
|
||||||
|
# Library files can have extraneous formatting (even minimized)
|
||||||
|
/static/(src/)?lib/|
|
||||||
|
# Repos using Sphinx to generate docs don't need prettying
|
||||||
|
^docs/_templates/.*\.html$|
|
||||||
|
# You don't usually want a bot to modify your legal texts
|
||||||
|
(LICENSE.*|COPYING.*)
|
||||||
|
default_language_version:
|
||||||
|
python: python3
|
||||||
|
node: "16.17.0"
|
||||||
|
repos:
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
# These files are most likely copier diff rejection junks; if found,
|
||||||
|
# review them manually, fix the problem (if needed) and remove them
|
||||||
|
- id: forbidden-files
|
||||||
|
name: forbidden files
|
||||||
|
entry: found forbidden files; remove them
|
||||||
|
language: fail
|
||||||
|
files: "\\.rej$"
|
||||||
|
- id: en-po-files
|
||||||
|
name: en.po files cannot exist
|
||||||
|
entry: found a en.po file
|
||||||
|
language: fail
|
||||||
|
files: '[a-zA-Z0-9_]*/i18n/en\.po$'
|
||||||
|
- repo: https://github.com/oca/maintainer-tools
|
||||||
|
rev: 4cd2b852214dead80822e93e6749b16f2785b2fe
|
||||||
|
hooks:
|
||||||
|
# update the NOT INSTALLABLE ADDONS section above
|
||||||
|
- id: oca-update-pre-commit-excluded-addons
|
||||||
|
- id: oca-fix-manifest-website
|
||||||
|
args: ["https://github.com/camptocamp/odoo-cloud-platform"]
|
||||||
|
- repo: https://github.com/myint/autoflake
|
||||||
|
rev: v1.6.1
|
||||||
|
hooks:
|
||||||
|
- id: autoflake
|
||||||
|
args:
|
||||||
|
- --expand-star-imports
|
||||||
|
- --ignore-init-module-imports
|
||||||
|
- --in-place
|
||||||
|
- --remove-all-unused-imports
|
||||||
|
- --remove-duplicate-keys
|
||||||
|
- --remove-unused-variables
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 22.8.0
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
|
rev: v2.7.1
|
||||||
|
hooks:
|
||||||
|
- id: prettier
|
||||||
|
name: prettier (with plugin-xml)
|
||||||
|
additional_dependencies:
|
||||||
|
- "prettier@2.7.1"
|
||||||
|
- "@prettier/plugin-xml@2.2.0"
|
||||||
|
args:
|
||||||
|
- --plugin=@prettier/plugin-xml
|
||||||
|
files: \.(css|htm|html|js|json|jsx|less|md|scss|toml|ts|xml|yaml|yml)$
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||||
|
rev: v8.24.0
|
||||||
|
hooks:
|
||||||
|
- id: eslint
|
||||||
|
verbose: true
|
||||||
|
args:
|
||||||
|
- --color
|
||||||
|
- --fix
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v4.3.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
# exclude autogenerated files
|
||||||
|
exclude: /README\.rst$|\.pot?$
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
# exclude autogenerated files
|
||||||
|
exclude: /README\.rst$|\.pot?$
|
||||||
|
- id: debug-statements
|
||||||
|
- id: fix-encoding-pragma
|
||||||
|
args: ["--remove"]
|
||||||
|
- id: check-case-conflict
|
||||||
|
- id: check-docstring-first
|
||||||
|
- id: check-executables-have-shebangs
|
||||||
|
- id: check-merge-conflict
|
||||||
|
# exclude files where underlines are not distinguishable from merge conflicts
|
||||||
|
exclude: /README\.rst$|^docs/.*\.rst$
|
||||||
|
- id: check-symlinks
|
||||||
|
- id: check-xml
|
||||||
|
- id: mixed-line-ending
|
||||||
|
args: ["--fix=lf"]
|
||||||
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
|
rev: v2.38.2
|
||||||
|
hooks:
|
||||||
|
- id: pyupgrade
|
||||||
|
args: ["--keep-percent-format"]
|
||||||
|
- repo: https://github.com/PyCQA/isort
|
||||||
|
rev: 5.12.0
|
||||||
|
hooks:
|
||||||
|
- id: isort
|
||||||
|
name: isort except __init__.py
|
||||||
|
args:
|
||||||
|
- --settings=.
|
||||||
|
exclude: /__init__\.py$
|
||||||
|
- repo: https://github.com/acsone/setuptools-odoo
|
||||||
|
rev: 3.1.8
|
||||||
|
hooks:
|
||||||
|
- id: setuptools-odoo-make-default
|
||||||
|
- repo: https://github.com/PyCQA/flake8
|
||||||
|
rev: 3.9.2
|
||||||
|
hooks:
|
||||||
|
- id: flake8
|
||||||
|
name: flake8
|
||||||
|
additional_dependencies: ["flake8-bugbear==21.9.2"]
|
||||||
|
- repo: https://github.com/OCA/pylint-odoo
|
||||||
|
rev: 7.0.2
|
||||||
|
hooks:
|
||||||
|
- id: pylint_odoo
|
||||||
|
name: pylint with optional checks
|
||||||
|
args:
|
||||||
|
- --rcfile=.pylintrc
|
||||||
|
- --exit-zero
|
||||||
|
verbose: true
|
||||||
|
- id: pylint_odoo
|
||||||
|
args:
|
||||||
|
- --rcfile=.pylintrc-mandatory
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Defaults for all prettier-supported languages.
|
||||||
|
# Prettier will complete this with settings from .editorconfig file.
|
||||||
|
bracketSpacing: false
|
||||||
|
printWidth: 88
|
||||||
|
proseWrap: always
|
||||||
|
semi: true
|
||||||
|
trailingComma: "es5"
|
||||||
|
xmlWhitespaceSensitivity: "strict"
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
|
||||||
|
|
||||||
|
[MASTER]
|
||||||
|
load-plugins=pylint_odoo
|
||||||
|
score=n
|
||||||
|
|
||||||
|
[ODOOLINT]
|
||||||
|
readme_template_url="https://github.com/OCA/maintainer-tools/blob/master/template/module/README.rst"
|
||||||
|
manifest_required_authors=Camptocamp
|
||||||
|
manifest_required_keys=license
|
||||||
|
manifest_deprecated_keys=description,active
|
||||||
|
license_allowed=AGPL-3,GPL-2,GPL-2 or any later version,GPL-3,GPL-3 or any later version,LGPL-3
|
||||||
|
valid_odoo_versions=16.0
|
||||||
|
|
||||||
|
[MESSAGES CONTROL]
|
||||||
|
disable=all
|
||||||
|
|
||||||
|
# This .pylintrc contains optional AND mandatory checks and is meant to be
|
||||||
|
# loaded in an IDE to have it check everything, in the hope this will make
|
||||||
|
# optional checks more visible to contributors who otherwise never look at a
|
||||||
|
# green travis to see optional checks that failed.
|
||||||
|
# .pylintrc-mandatory containing only mandatory checks is used the pre-commit
|
||||||
|
# config as a blocking check.
|
||||||
|
|
||||||
|
enable=anomalous-backslash-in-string,
|
||||||
|
api-one-deprecated,
|
||||||
|
api-one-multi-together,
|
||||||
|
assignment-from-none,
|
||||||
|
attribute-deprecated,
|
||||||
|
class-camelcase,
|
||||||
|
dangerous-default-value,
|
||||||
|
dangerous-view-replace-wo-priority,
|
||||||
|
development-status-allowed,
|
||||||
|
duplicate-id-csv,
|
||||||
|
duplicate-key,
|
||||||
|
duplicate-xml-fields,
|
||||||
|
duplicate-xml-record-id,
|
||||||
|
eval-referenced,
|
||||||
|
eval-used,
|
||||||
|
incoherent-interpreter-exec-perm,
|
||||||
|
license-allowed,
|
||||||
|
manifest-author-string,
|
||||||
|
manifest-deprecated-key,
|
||||||
|
manifest-required-author,
|
||||||
|
manifest-required-key,
|
||||||
|
manifest-version-format,
|
||||||
|
method-compute,
|
||||||
|
method-inverse,
|
||||||
|
method-required-super,
|
||||||
|
method-search,
|
||||||
|
openerp-exception-warning,
|
||||||
|
pointless-statement,
|
||||||
|
pointless-string-statement,
|
||||||
|
print-used,
|
||||||
|
redundant-keyword-arg,
|
||||||
|
redundant-modulename-xml,
|
||||||
|
reimported,
|
||||||
|
relative-import,
|
||||||
|
return-in-init,
|
||||||
|
rst-syntax-error,
|
||||||
|
sql-injection,
|
||||||
|
too-few-format-args,
|
||||||
|
translation-field,
|
||||||
|
translation-required,
|
||||||
|
unreachable,
|
||||||
|
use-vim-comment,
|
||||||
|
wrong-tabs-instead-of-spaces,
|
||||||
|
xml-syntax-error,
|
||||||
|
attribute-string-redundant,
|
||||||
|
character-not-valid-in-resource-link,
|
||||||
|
consider-merging-classes-inherited,
|
||||||
|
context-overridden,
|
||||||
|
create-user-wo-reset-password,
|
||||||
|
dangerous-filter-wo-user,
|
||||||
|
dangerous-qweb-replace-wo-priority,
|
||||||
|
deprecated-data-xml-node,
|
||||||
|
deprecated-openerp-xml-node,
|
||||||
|
duplicate-po-message-definition,
|
||||||
|
except-pass,
|
||||||
|
file-not-used,
|
||||||
|
invalid-commit,
|
||||||
|
manifest-maintainers-list,
|
||||||
|
missing-newline-extrafiles,
|
||||||
|
missing-readme,
|
||||||
|
missing-return,
|
||||||
|
odoo-addons-relative-import,
|
||||||
|
old-api7-method-defined,
|
||||||
|
po-msgstr-variables,
|
||||||
|
po-syntax-error,
|
||||||
|
renamed-field-parameter,
|
||||||
|
resource-not-exist,
|
||||||
|
str-format-used,
|
||||||
|
test-folder-imported,
|
||||||
|
translation-contains-variable,
|
||||||
|
translation-positional-used,
|
||||||
|
unnecessary-utf8-coding-comment,
|
||||||
|
website-manifest-key-not-valid-uri,
|
||||||
|
xml-attribute-translatable,
|
||||||
|
xml-deprecated-qweb-directive,
|
||||||
|
xml-deprecated-tree-attribute,
|
||||||
|
external-request-timeout,
|
||||||
|
# messages that do not cause the lint step to fail
|
||||||
|
consider-merging-classes-inherited,
|
||||||
|
create-user-wo-reset-password,
|
||||||
|
dangerous-filter-wo-user,
|
||||||
|
deprecated-module,
|
||||||
|
file-not-used,
|
||||||
|
invalid-commit,
|
||||||
|
missing-manifest-dependency,
|
||||||
|
missing-newline-extrafiles,
|
||||||
|
missing-readme,
|
||||||
|
no-utf8-coding-comment,
|
||||||
|
odoo-addons-relative-import,
|
||||||
|
old-api7-method-defined,
|
||||||
|
redefined-builtin,
|
||||||
|
too-complex,
|
||||||
|
unnecessary-utf8-coding-comment
|
||||||
|
|
||||||
|
|
||||||
|
[REPORTS]
|
||||||
|
msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg}
|
||||||
|
output-format=colorized
|
||||||
|
reports=no
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
|
||||||
|
[MASTER]
|
||||||
|
load-plugins=pylint_odoo
|
||||||
|
score=n
|
||||||
|
|
||||||
|
[ODOOLINT]
|
||||||
|
readme_template_url="https://github.com/OCA/maintainer-tools/blob/master/template/module/README.rst"
|
||||||
|
manifest_required_authors=Camptocamp
|
||||||
|
manifest_required_keys=license
|
||||||
|
manifest_deprecated_keys=description,active
|
||||||
|
license_allowed=AGPL-3,GPL-2,GPL-2 or any later version,GPL-3,GPL-3 or any later version,LGPL-3
|
||||||
|
valid_odoo_versions=16.0
|
||||||
|
|
||||||
|
[MESSAGES CONTROL]
|
||||||
|
disable=all
|
||||||
|
|
||||||
|
enable=anomalous-backslash-in-string,
|
||||||
|
api-one-deprecated,
|
||||||
|
api-one-multi-together,
|
||||||
|
assignment-from-none,
|
||||||
|
attribute-deprecated,
|
||||||
|
class-camelcase,
|
||||||
|
dangerous-default-value,
|
||||||
|
dangerous-view-replace-wo-priority,
|
||||||
|
development-status-allowed,
|
||||||
|
duplicate-id-csv,
|
||||||
|
duplicate-key,
|
||||||
|
duplicate-xml-fields,
|
||||||
|
duplicate-xml-record-id,
|
||||||
|
eval-referenced,
|
||||||
|
eval-used,
|
||||||
|
incoherent-interpreter-exec-perm,
|
||||||
|
license-allowed,
|
||||||
|
manifest-author-string,
|
||||||
|
manifest-deprecated-key,
|
||||||
|
manifest-required-author,
|
||||||
|
manifest-required-key,
|
||||||
|
manifest-version-format,
|
||||||
|
method-compute,
|
||||||
|
method-inverse,
|
||||||
|
method-required-super,
|
||||||
|
method-search,
|
||||||
|
openerp-exception-warning,
|
||||||
|
pointless-statement,
|
||||||
|
pointless-string-statement,
|
||||||
|
print-used,
|
||||||
|
redundant-keyword-arg,
|
||||||
|
redundant-modulename-xml,
|
||||||
|
reimported,
|
||||||
|
relative-import,
|
||||||
|
return-in-init,
|
||||||
|
rst-syntax-error,
|
||||||
|
sql-injection,
|
||||||
|
too-few-format-args,
|
||||||
|
translation-field,
|
||||||
|
translation-required,
|
||||||
|
unreachable,
|
||||||
|
use-vim-comment,
|
||||||
|
wrong-tabs-instead-of-spaces,
|
||||||
|
xml-syntax-error,
|
||||||
|
attribute-string-redundant,
|
||||||
|
character-not-valid-in-resource-link,
|
||||||
|
consider-merging-classes-inherited,
|
||||||
|
context-overridden,
|
||||||
|
create-user-wo-reset-password,
|
||||||
|
dangerous-filter-wo-user,
|
||||||
|
dangerous-qweb-replace-wo-priority,
|
||||||
|
deprecated-data-xml-node,
|
||||||
|
deprecated-openerp-xml-node,
|
||||||
|
duplicate-po-message-definition,
|
||||||
|
except-pass,
|
||||||
|
file-not-used,
|
||||||
|
invalid-commit,
|
||||||
|
manifest-maintainers-list,
|
||||||
|
missing-newline-extrafiles,
|
||||||
|
missing-readme,
|
||||||
|
missing-return,
|
||||||
|
odoo-addons-relative-import,
|
||||||
|
old-api7-method-defined,
|
||||||
|
po-msgstr-variables,
|
||||||
|
po-syntax-error,
|
||||||
|
renamed-field-parameter,
|
||||||
|
resource-not-exist,
|
||||||
|
str-format-used,
|
||||||
|
test-folder-imported,
|
||||||
|
translation-contains-variable,
|
||||||
|
translation-positional-used,
|
||||||
|
unnecessary-utf8-coding-comment,
|
||||||
|
website-manifest-key-not-valid-uri,
|
||||||
|
xml-attribute-translatable,
|
||||||
|
xml-deprecated-qweb-directive,
|
||||||
|
xml-deprecated-tree-attribute,
|
||||||
|
external-request-timeout
|
||||||
|
|
||||||
|
[REPORTS]
|
||||||
|
msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg}
|
||||||
|
output-format=colorized
|
||||||
|
reports=no
|
||||||
-44
@@ -1,44 +0,0 @@
|
|||||||
language: python
|
|
||||||
sudo: false
|
|
||||||
cache: pip
|
|
||||||
|
|
||||||
branches:
|
|
||||||
only:
|
|
||||||
- "/^[[:digit:]]{1,2}.[[:digit:]]$/"
|
|
||||||
|
|
||||||
python:
|
|
||||||
# Force a newer version than 3.7.1 which break build
|
|
||||||
# due to https://bugs.python.org/issue34921
|
|
||||||
- "3.8"
|
|
||||||
|
|
||||||
addons:
|
|
||||||
postgresql: "9.5"
|
|
||||||
apt:
|
|
||||||
packages:
|
|
||||||
- expect-dev # provides unbuffer utility
|
|
||||||
- python-lxml # because pip installation is slow
|
|
||||||
- python-simplejson
|
|
||||||
- python-serial
|
|
||||||
|
|
||||||
env:
|
|
||||||
matrix:
|
|
||||||
- LINT_CHECK="1"
|
|
||||||
- TESTS="1" ODOO_REPO="odoo/odoo" INCLUDE="cloud_platform_exoscale"
|
|
||||||
- TESTS="1" ODOO_REPO="OCA/OCB" INCLUDE="cloud_platform_exoscale"
|
|
||||||
- TESTS="1" ODOO_REPO="odoo/odoo" INCLUDE="cloud_platform_ovh"
|
|
||||||
- TESTS="1" ODOO_REPO="OCA/OCB" INCLUDE="cloud_platform_ovh"
|
|
||||||
- TESTS="1" ODOO_REPO="odoo/odoo" EXCLUDE="cloud_platform,cloud_platform_ovh,cloud_platform_exoscale"
|
|
||||||
- TESTS="1" ODOO_REPO="OCA/OCB" EXCLUDE="cloud_platform,cloud_platform_ovh,cloud_platform_exoscale"
|
|
||||||
global:
|
|
||||||
- VERSION="16.0" LINT_CHECK="0" TESTS="0"
|
|
||||||
|
|
||||||
install:
|
|
||||||
- git clone --depth=1 https://github.com/OCA/maintainer-quality-tools.git ${HOME}/maintainer-quality-tools
|
|
||||||
- export PATH=${HOME}/maintainer-quality-tools/travis:${PATH}
|
|
||||||
- travis_install_nightly
|
|
||||||
|
|
||||||
script:
|
|
||||||
- travis_run_tests
|
|
||||||
|
|
||||||
after_success:
|
|
||||||
- travis_after_test_success
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
Version 3, 19 November 2007
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
of this license document, but changing it is not allowed.
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
@@ -633,8 +633,8 @@ the "copyright" line and a pointer to where the full notice is found.
|
|||||||
Copyright (C) <year> <name of author>
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU Affero General Public License as published
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
by the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
@@ -643,7 +643,7 @@ the "copyright" line and a pointer to where the full notice is found.
|
|||||||
GNU Affero General Public License for more details.
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
@@ -658,4 +658,4 @@ specific requirements.
|
|||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
<http://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
[](https://travis-ci.com/camptocamp/odoo-cloud-platform)
|
|
||||||
|
<!-- /!\ Non OCA Context : Set here the badge of your runbot / runboat instance. -->
|
||||||
|
[](https://github.com/camptocamp/odoo-cloud-platform/actions/workflows/pre-commit.yml?query=branch%3A16.0)
|
||||||
|
[](https://github.com/camptocamp/odoo-cloud-platform/actions/workflows/test.yml?query=branch%3A16.0)
|
||||||
|
[](https://codecov.io/gh/camptocamp/odoo-cloud-platform)
|
||||||
|
<!-- /!\ Non OCA Context : Set here the badge of your translation instance. -->
|
||||||
|
|
||||||
|
<!-- /!\ do not modify above this line -->
|
||||||
|
|
||||||
# Odoo Cloud Addons
|
# Odoo Cloud Addons
|
||||||
|
|
||||||
@@ -167,3 +174,26 @@ The checks can be bypassed with the environment variable
|
|||||||
|
|
||||||
To prevent object storage to be accessed while failing for any kind of reason
|
To prevent object storage to be accessed while failing for any kind of reason
|
||||||
set this environment variable `DISABLE_ATTACHMENT_STORAGE` set to `1`.
|
set this environment variable `DISABLE_ATTACHMENT_STORAGE` set to `1`.
|
||||||
|
|
||||||
|
<!-- /!\ do not modify below this line -->
|
||||||
|
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
|
||||||
|
[//]: # (addons)
|
||||||
|
|
||||||
|
This part will be replaced when running the oca-gen-addons-table script from OCA/maintainer-tools.
|
||||||
|
|
||||||
|
[//]: # (end addons)
|
||||||
|
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
|
## Licenses
|
||||||
|
|
||||||
|
This repository is licensed under [AGPL-3.0](LICENSE).
|
||||||
|
|
||||||
|
However, each module can have a totally different license, as long as they adhere to Camptocamp
|
||||||
|
policy. Consult each module's `__manifest__.py` file, which contains a `license` key
|
||||||
|
that explains its license.
|
||||||
|
|
||||||
|
----
|
||||||
|
<!-- /!\ Non OCA Context : Set here the full description of your organization. -->
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ from odoo import _, api, exceptions, models
|
|||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
from azure.core.exceptions import HttpResponseError, ResourceExistsError
|
||||||
from azure.storage.blob import (
|
from azure.storage.blob import (
|
||||||
BlobServiceClient,
|
|
||||||
generate_account_sas,
|
|
||||||
ResourceTypes,
|
|
||||||
AccountSasPermissions,
|
AccountSasPermissions,
|
||||||
|
BlobServiceClient,
|
||||||
|
ResourceTypes,
|
||||||
|
generate_account_sas,
|
||||||
)
|
)
|
||||||
from azure.core.exceptions import ResourceExistsError, HttpResponseError
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
_logger.debug("Cannot 'import azure-storage-blob'.")
|
_logger.debug("Cannot 'import azure-storage-blob'.")
|
||||||
|
|
||||||
@@ -32,9 +32,7 @@ class IrAttachment(models.Model):
|
|||||||
_inherit = "ir.attachment"
|
_inherit = "ir.attachment"
|
||||||
|
|
||||||
def _get_stores(self):
|
def _get_stores(self):
|
||||||
l = ["azure"]
|
return ["azure"] + super(IrAttachment, self)._get_stores()
|
||||||
l += super(IrAttachment, self)._get_stores()
|
|
||||||
return l
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _get_blob_service_client(self):
|
def _get_blob_service_client(self):
|
||||||
@@ -88,7 +86,7 @@ class IrAttachment(models.Model):
|
|||||||
"Error during the connection to Azure container using the "
|
"Error during the connection to Azure container using the "
|
||||||
"connection string."
|
"connection string."
|
||||||
)
|
)
|
||||||
raise exceptions.UserError(str(error))
|
raise exceptions.UserError(str(error)) from None
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
sas_token = generate_account_sas(
|
sas_token = generate_account_sas(
|
||||||
@@ -107,15 +105,13 @@ class IrAttachment(models.Model):
|
|||||||
"Error during the connection to Azure container using the Shared "
|
"Error during the connection to Azure container using the Shared "
|
||||||
"Access Signature (SAS)"
|
"Access Signature (SAS)"
|
||||||
)
|
)
|
||||||
raise exceptions.UserError(str(error))
|
raise exceptions.UserError(str(error)) from None
|
||||||
return blob_service_client
|
return blob_service_client
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _get_container_name(self):
|
def _get_container_name(self):
|
||||||
"""
|
# Container naming rules:
|
||||||
Container naming rules:
|
# https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names # noqa: B950
|
||||||
https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names
|
|
||||||
"""
|
|
||||||
running_env = os.environ.get("RUNNING_ENV", "dev")
|
running_env = os.environ.get("RUNNING_ENV", "dev")
|
||||||
storage_name = os.environ.get("AZURE_STORAGE_NAME", r"{env}-{db}")
|
storage_name = os.environ.get("AZURE_STORAGE_NAME", r"{env}-{db}")
|
||||||
storage_name = storage_name.format(env=running_env, db=self.env.cr.dbname)
|
storage_name = storage_name.format(env=running_env, db=self.env.cr.dbname)
|
||||||
@@ -143,7 +139,7 @@ class IrAttachment(models.Model):
|
|||||||
container_client.create_container()
|
container_client.create_container()
|
||||||
except HttpResponseError as error:
|
except HttpResponseError as error:
|
||||||
_logger.exception("Error during the creation of the Azure container")
|
_logger.exception("Error during the creation of the Azure container")
|
||||||
raise exceptions.UserError(str(error))
|
raise exceptions.UserError(str(error)) from None
|
||||||
return container_client
|
return container_client
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
@@ -181,13 +177,17 @@ class IrAttachment(models.Model):
|
|||||||
try:
|
try:
|
||||||
blob_client.upload_blob(file, blob_type="BlockBlob")
|
blob_client.upload_blob(file, blob_type="BlockBlob")
|
||||||
except ResourceExistsError:
|
except ResourceExistsError:
|
||||||
pass
|
_logger.exception(
|
||||||
|
"Trying to re create an existing resource %s" % filename
|
||||||
|
)
|
||||||
except HttpResponseError as error:
|
except HttpResponseError as error:
|
||||||
# log verbose error from azure, return short message for user
|
# log verbose error from azure, return short message for user
|
||||||
_logger.exception("Error during storage of the file %s" % filename)
|
_logger.exception(
|
||||||
|
"HTTP Error during storage of the file %s" % filename
|
||||||
|
)
|
||||||
raise exceptions.UserError(
|
raise exceptions.UserError(
|
||||||
_("The file could not be stored: %s") % str(error)
|
_("The file could not be stored: %s") % str(error)
|
||||||
)
|
) from None
|
||||||
else:
|
else:
|
||||||
_super = super(IrAttachment, self)
|
_super = super(IrAttachment, self)
|
||||||
filename = _super._store_file_write(key, bin_data)
|
filename = _super._store_file_write(key, bin_data)
|
||||||
|
|||||||
@@ -60,6 +60,6 @@ try:
|
|||||||
from odoo.addons import documents
|
from odoo.addons import documents
|
||||||
|
|
||||||
documents.models.ir_binary.IrBinary._record_to_stream = IrBinary._record_to_stream
|
documents.models.ir_binary.IrBinary._record_to_stream = IrBinary._record_to_stream
|
||||||
except ImportError:
|
except ImportError: # pylint: disable=except-pass
|
||||||
# document enterprise module if not installed, we just ignore
|
# document enterprise module if not installed, we just ignore
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"external_dependencies": {
|
"external_dependencies": {
|
||||||
"python": ["boto3"],
|
"python": ["boto3"],
|
||||||
},
|
},
|
||||||
"website": "https://www.camptocamp.com",
|
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
||||||
"data": [],
|
"data": [],
|
||||||
"installable": False,
|
"installable": False,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
|
|
||||||
from . import ir_attachment
|
from . import ir_attachment
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||||
|
|
||||||
|
|
||||||
|
import io
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import io
|
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
from odoo import _, api, exceptions, models
|
from odoo import _, api, exceptions, models
|
||||||
|
|
||||||
from ..s3uri import S3Uri
|
from ..s3uri import S3Uri
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
@@ -26,9 +27,7 @@ class IrAttachment(models.Model):
|
|||||||
_inherit = "ir.attachment"
|
_inherit = "ir.attachment"
|
||||||
|
|
||||||
def _get_stores(self):
|
def _get_stores(self):
|
||||||
l = ['s3']
|
return ["s3"] + super()._get_stores()
|
||||||
l += super()._get_stores()
|
|
||||||
return l
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _get_s3_bucket(self, name=None):
|
def _get_s3_bucket(self, name=None):
|
||||||
@@ -45,42 +44,43 @@ class IrAttachment(models.Model):
|
|||||||
from the environment variable ``AWS_BUCKETNAME`` will be read.
|
from the environment variable ``AWS_BUCKETNAME`` will be read.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
host = os.environ.get('AWS_HOST')
|
host = os.environ.get("AWS_HOST")
|
||||||
|
|
||||||
# Ensure host is prefixed with a scheme (use https as default)
|
# Ensure host is prefixed with a scheme (use https as default)
|
||||||
if host and not urlsplit(host).scheme:
|
if host and not urlsplit(host).scheme:
|
||||||
host = 'https://%s' % host
|
host = "https://%s" % host
|
||||||
|
|
||||||
region_name = os.environ.get('AWS_REGION')
|
region_name = os.environ.get("AWS_REGION")
|
||||||
access_key = os.environ.get('AWS_ACCESS_KEY_ID')
|
access_key = os.environ.get("AWS_ACCESS_KEY_ID")
|
||||||
secret_key = os.environ.get('AWS_SECRET_ACCESS_KEY')
|
secret_key = os.environ.get("AWS_SECRET_ACCESS_KEY")
|
||||||
bucket_name = name or os.environ.get('AWS_BUCKETNAME')
|
bucket_name = name or os.environ.get("AWS_BUCKETNAME")
|
||||||
# replaces {db} by the database name to handle multi-tenancy
|
# replaces {db} by the database name to handle multi-tenancy
|
||||||
bucket_name = bucket_name.format(db=self.env.cr.dbname)
|
bucket_name = bucket_name.format(db=self.env.cr.dbname)
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
'aws_access_key_id': access_key,
|
"aws_access_key_id": access_key,
|
||||||
'aws_secret_access_key': secret_key,
|
"aws_secret_access_key": secret_key,
|
||||||
}
|
}
|
||||||
if host:
|
if host:
|
||||||
params['endpoint_url'] = host
|
params["endpoint_url"] = host
|
||||||
if region_name:
|
if region_name:
|
||||||
params['region_name'] = region_name
|
params["region_name"] = region_name
|
||||||
if not (access_key and secret_key and bucket_name):
|
if not (access_key and secret_key and bucket_name):
|
||||||
msg = _('If you want to read from the %s S3 bucket, the following '
|
msg = _(
|
||||||
'environment variables must be set:\n'
|
"If you want to read from the %(bucket_name)s S3 bucket, the following "
|
||||||
'* AWS_ACCESS_KEY_ID\n'
|
"environment variables must be set:\n"
|
||||||
'* AWS_SECRET_ACCESS_KEY\n'
|
"* AWS_ACCESS_KEY_ID\n"
|
||||||
'If you want to write in the %s S3 bucket, this variable '
|
"* AWS_SECRET_ACCESS_KEY\n"
|
||||||
'must be set as well:\n'
|
"If you want to write in the %(bucket_name)s S3 bucket, this variable "
|
||||||
'* AWS_BUCKETNAME\n'
|
"must be set as well:\n"
|
||||||
'Optionally, the S3 host can be changed with:\n'
|
"* AWS_BUCKETNAME\n"
|
||||||
'* AWS_HOST\n'
|
"Optionally, the S3 host can be changed with:\n"
|
||||||
) % (bucket_name, bucket_name)
|
"* AWS_HOST\n"
|
||||||
|
).format(bucket_name=bucket_name)
|
||||||
|
|
||||||
raise exceptions.UserError(msg)
|
raise exceptions.UserError(msg)
|
||||||
# try:
|
# try:
|
||||||
s3 = boto3.resource('s3', **params)
|
s3 = boto3.resource("s3", **params)
|
||||||
bucket = s3.Bucket(bucket_name)
|
bucket = s3.Bucket(bucket_name)
|
||||||
exists = True
|
exists = True
|
||||||
try:
|
try:
|
||||||
@@ -88,13 +88,13 @@ class IrAttachment(models.Model):
|
|||||||
except ClientError as e:
|
except ClientError as e:
|
||||||
# If a client error is thrown, then check that it was a 404 error.
|
# If a client error is thrown, then check that it was a 404 error.
|
||||||
# If it was a 404 error, then the bucket does not exist.
|
# If it was a 404 error, then the bucket does not exist.
|
||||||
error_code = e.response['Error']['Code']
|
error_code = e.response["Error"]["Code"]
|
||||||
if error_code == '404':
|
if error_code == "404":
|
||||||
exists = False
|
exists = False
|
||||||
except EndpointConnectionError as error:
|
except EndpointConnectionError as error:
|
||||||
# log verbose error from s3, return short message for user
|
# log verbose error from s3, return short message for user
|
||||||
_logger.exception('Error during connection on S3')
|
msg = _logger.exception("Error during connection on S3")
|
||||||
raise exceptions.UserError(str(error))
|
raise exceptions.UserError(str(error)) from None
|
||||||
|
|
||||||
if not exists:
|
if not exists:
|
||||||
if not region_name:
|
if not region_name:
|
||||||
@@ -102,14 +102,13 @@ class IrAttachment(models.Model):
|
|||||||
else:
|
else:
|
||||||
bucket = s3.create_bucket(
|
bucket = s3.create_bucket(
|
||||||
Bucket=bucket_name,
|
Bucket=bucket_name,
|
||||||
CreateBucketConfiguration={
|
CreateBucketConfiguration={"LocationConstraint": region_name},
|
||||||
'LocationConstraint': region_name
|
)
|
||||||
})
|
|
||||||
return bucket
|
return bucket
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _store_file_read(self, fname):
|
def _store_file_read(self, fname):
|
||||||
if fname.startswith('s3://'):
|
if fname.startswith("s3://"):
|
||||||
s3uri = S3Uri(fname)
|
s3uri = S3Uri(fname)
|
||||||
try:
|
try:
|
||||||
bucket = self._get_s3_bucket(name=s3uri.bucket())
|
bucket = self._get_s3_bucket(name=s3uri.bucket())
|
||||||
@@ -117,45 +116,39 @@ class IrAttachment(models.Model):
|
|||||||
_logger.exception(
|
_logger.exception(
|
||||||
"error reading attachment '%s' from object storage", fname
|
"error reading attachment '%s' from object storage", fname
|
||||||
)
|
)
|
||||||
return ''
|
return ""
|
||||||
try:
|
try:
|
||||||
key = s3uri.item()
|
key = s3uri.item()
|
||||||
bucket.meta.client.head_object(
|
bucket.meta.client.head_object(Bucket=bucket.name, Key=key)
|
||||||
Bucket=bucket.name, Key=key
|
|
||||||
)
|
|
||||||
with io.BytesIO() as res:
|
with io.BytesIO() as res:
|
||||||
bucket.download_fileobj(key, res)
|
bucket.download_fileobj(key, res)
|
||||||
res.seek(0)
|
res.seek(0)
|
||||||
read = res.read()
|
read = res.read()
|
||||||
except ClientError:
|
except ClientError:
|
||||||
read = ''
|
read = ""
|
||||||
_logger.info(
|
_logger.info("attachment '%s' missing on object storage", fname)
|
||||||
"attachment '%s' missing on object storage", fname
|
|
||||||
)
|
|
||||||
return read
|
return read
|
||||||
else:
|
else:
|
||||||
return super()._store_file_read(fname)
|
return super()._store_file_read(fname)
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _store_file_write(self, key, bin_data):
|
def _store_file_write(self, key, bin_data):
|
||||||
location = self.env.context.get('storage_location') or self._storage()
|
location = self.env.context.get("storage_location") or self._storage()
|
||||||
if location == 's3':
|
if location == "s3":
|
||||||
bucket = self._get_s3_bucket()
|
bucket = self._get_s3_bucket()
|
||||||
obj = bucket.Object(key=key)
|
obj = bucket.Object(key=key)
|
||||||
with io.BytesIO() as file:
|
with io.BytesIO() as file:
|
||||||
file.write(bin_data)
|
file.write(bin_data)
|
||||||
file.seek(0)
|
file.seek(0)
|
||||||
filename = 's3://%s/%s' % (bucket.name, key)
|
filename = "s3://%s/%s" % (bucket.name, key)
|
||||||
try:
|
try:
|
||||||
obj.upload_fileobj(file)
|
obj.upload_fileobj(file)
|
||||||
except ClientError as error:
|
except ClientError as error:
|
||||||
# log verbose error from s3, return short message for user
|
# log verbose error from s3, return short message for user
|
||||||
_logger.exception(
|
_logger.exception("Error during storage of the file %s" % filename)
|
||||||
'Error during storage of the file %s' % filename
|
|
||||||
)
|
|
||||||
raise exceptions.UserError(
|
raise exceptions.UserError(
|
||||||
_('The file could not be stored: %s') % str(error)
|
_("The file could not be stored: %s") % str(error)
|
||||||
)
|
) from None
|
||||||
else:
|
else:
|
||||||
_super = super()
|
_super = super()
|
||||||
filename = _super._store_file_write(key, bin_data)
|
filename = _super._store_file_write(key, bin_data)
|
||||||
@@ -163,28 +156,22 @@ class IrAttachment(models.Model):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _store_file_delete(self, fname):
|
def _store_file_delete(self, fname):
|
||||||
if fname.startswith('s3://'):
|
if fname.startswith("s3://"):
|
||||||
s3uri = S3Uri(fname)
|
s3uri = S3Uri(fname)
|
||||||
bucket_name = s3uri.bucket()
|
bucket_name = s3uri.bucket()
|
||||||
item_name = s3uri.item()
|
item_name = s3uri.item()
|
||||||
# delete the file only if it is on the current configured bucket
|
# delete the file only if it is on the current configured bucket
|
||||||
# otherwise, we might delete files used on a different environment
|
# otherwise, we might delete files used on a different environment
|
||||||
if bucket_name == os.environ.get('AWS_BUCKETNAME'):
|
if bucket_name == os.environ.get("AWS_BUCKETNAME"):
|
||||||
bucket = self._get_s3_bucket()
|
bucket = self._get_s3_bucket()
|
||||||
obj = bucket.Object(key=item_name)
|
obj = bucket.Object(key=item_name)
|
||||||
try:
|
try:
|
||||||
bucket.meta.client.head_object(
|
bucket.meta.client.head_object(Bucket=bucket.name, Key=item_name)
|
||||||
Bucket=bucket.name, Key=item_name
|
|
||||||
)
|
|
||||||
obj.delete()
|
obj.delete()
|
||||||
_logger.info(
|
_logger.info("file %s deleted on the object storage" % (fname,))
|
||||||
'file %s deleted on the object storage' % (fname,)
|
|
||||||
)
|
|
||||||
except ClientError:
|
except ClientError:
|
||||||
# log verbose error from s3, return short message for
|
# log verbose error from s3, return short message for
|
||||||
# user
|
# user
|
||||||
_logger.exception(
|
_logger.exception("Error during deletion of the file %s" % fname)
|
||||||
'Error during deletion of the file %s' % fname
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
super()._store_file_delete(fname)
|
return super()._store_file_delete(fname)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"keystoneauth1",
|
"keystoneauth1",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"website": "https://www.camptocamp.com",
|
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
||||||
"data": [],
|
"data": [],
|
||||||
"installable": False,
|
"installable": False,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,18 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from ..swift_uri import SwiftUri
|
|
||||||
|
|
||||||
from odoo import api, exceptions, models, _
|
from odoo import _, api, exceptions, models
|
||||||
|
|
||||||
|
from ..swift_uri import SwiftUri
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import swiftclient
|
|
||||||
import keystoneauth1
|
import keystoneauth1
|
||||||
import keystoneauth1.identity
|
import keystoneauth1.identity
|
||||||
import keystoneauth1.session
|
import keystoneauth1.session
|
||||||
|
import swiftclient
|
||||||
from swiftclient.exceptions import ClientException
|
from swiftclient.exceptions import ClientException
|
||||||
except ImportError:
|
except ImportError:
|
||||||
swiftclient = None
|
swiftclient = None
|
||||||
@@ -48,8 +49,9 @@ class SwiftSessionStore(object):
|
|||||||
def _get_key(self, auth_url, username, password, project_name):
|
def _get_key(self, auth_url, username, password, project_name):
|
||||||
return (auth_url, username, password, project_name)
|
return (auth_url, username, password, project_name)
|
||||||
|
|
||||||
def get_session(self, auth_url=None, username=None, password=None,
|
def get_session(
|
||||||
project_name=None):
|
self, auth_url=None, username=None, password=None, project_name=None
|
||||||
|
):
|
||||||
key = self._get_key(auth_url, username, password, project_name)
|
key = self._get_key(auth_url, username, password, project_name)
|
||||||
session = self._sessions.get(key)
|
session = self._sessions.get(key)
|
||||||
if not session:
|
if not session:
|
||||||
@@ -58,8 +60,8 @@ class SwiftSessionStore(object):
|
|||||||
password=password,
|
password=password,
|
||||||
project_name=project_name,
|
project_name=project_name,
|
||||||
auth_url=auth_url,
|
auth_url=auth_url,
|
||||||
project_domain_id='default',
|
project_domain_id="default",
|
||||||
user_domain_id='default',
|
user_domain_id="default",
|
||||||
)
|
)
|
||||||
session = keystoneauth1.session.Session(
|
session = keystoneauth1.session.Session(
|
||||||
auth=auth,
|
auth=auth,
|
||||||
@@ -73,36 +75,36 @@ swift_session_store = SwiftSessionStore()
|
|||||||
|
|
||||||
|
|
||||||
class IrAttachment(models.Model):
|
class IrAttachment(models.Model):
|
||||||
_inherit = 'ir.attachment'
|
_inherit = "ir.attachment"
|
||||||
|
|
||||||
def _get_stores(self):
|
def _get_stores(self):
|
||||||
l = ['swift']
|
return ["swift"] + super()._get_stores()
|
||||||
l += super()._get_stores()
|
|
||||||
return l
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _get_swift_connection(self):
|
def _get_swift_connection(self):
|
||||||
""" Returns a connection object for the Swift object store """
|
"""Returns a connection object for the Swift object store"""
|
||||||
host = os.environ.get('SWIFT_AUTH_URL')
|
host = os.environ.get("SWIFT_AUTH_URL")
|
||||||
account = os.environ.get('SWIFT_ACCOUNT')
|
account = os.environ.get("SWIFT_ACCOUNT")
|
||||||
password = os.environ.get('SWIFT_PASSWORD')
|
password = os.environ.get("SWIFT_PASSWORD")
|
||||||
project_name = os.environ.get('SWIFT_PROJECT_NAME')
|
project_name = os.environ.get("SWIFT_PROJECT_NAME")
|
||||||
if not project_name and os.environ.get('SWIFT_TENANT_NAME'):
|
if not project_name and os.environ.get("SWIFT_TENANT_NAME"):
|
||||||
project_name = os.environ['SWIFT_TENANT_NAME']
|
project_name = os.environ["SWIFT_TENANT_NAME"]
|
||||||
_logger.warning(
|
_logger.warning(
|
||||||
"SWIFT_TENANT_NAME is deprecated and "
|
"SWIFT_TENANT_NAME is deprecated and "
|
||||||
"must be replaced by SWIFT_PROJECT_NAME"
|
"must be replaced by SWIFT_PROJECT_NAME"
|
||||||
)
|
)
|
||||||
region = os.environ.get('SWIFT_REGION_NAME')
|
region = os.environ.get("SWIFT_REGION_NAME")
|
||||||
os_options = {}
|
os_options = {}
|
||||||
if region:
|
if region:
|
||||||
os_options['region_name'] = region
|
os_options["region_name"] = region
|
||||||
if not (host and account and password and project_name):
|
if not (host and account and password and project_name):
|
||||||
raise exceptions.UserError(_(
|
raise exceptions.UserError(
|
||||||
"Problem connecting to Swift store, are the env variables "
|
_(
|
||||||
"(SWIFT_AUTH_URL, SWIFT_ACCOUNT, SWIFT_PASSWORD, "
|
"Problem connecting to Swift store, are the env variables "
|
||||||
"SWIFT_TENANT_NAME) properly set?"
|
"(SWIFT_AUTH_URL, SWIFT_ACCOUNT, SWIFT_PASSWORD, "
|
||||||
))
|
"SWIFT_TENANT_NAME) properly set?"
|
||||||
|
)
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
session = swift_session_store.get_session(
|
session = swift_session_store.get_session(
|
||||||
username=account,
|
username=account,
|
||||||
@@ -115,13 +117,13 @@ class IrAttachment(models.Model):
|
|||||||
os_options=os_options,
|
os_options=os_options,
|
||||||
)
|
)
|
||||||
except ClientException:
|
except ClientException:
|
||||||
_logger.exception('Error connecting to Swift object store')
|
_logger.exception("Error connecting to Swift object store")
|
||||||
raise exceptions.UserError(_('Error on Swift connection'))
|
raise exceptions.UserError(_("Error on Swift connection")) from None
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _store_file_read(self, fname):
|
def _store_file_read(self, fname):
|
||||||
if fname.startswith('swift://'):
|
if fname.startswith("swift://"):
|
||||||
swifturi = SwiftUri(fname)
|
swifturi = SwiftUri(fname)
|
||||||
try:
|
try:
|
||||||
conn = self._get_swift_connection()
|
conn = self._get_swift_connection()
|
||||||
@@ -129,31 +131,27 @@ class IrAttachment(models.Model):
|
|||||||
_logger.exception(
|
_logger.exception(
|
||||||
"error reading attachment '%s' from object storage", fname
|
"error reading attachment '%s' from object storage", fname
|
||||||
)
|
)
|
||||||
return ''
|
return ""
|
||||||
try:
|
try:
|
||||||
resp, read = conn.get_object(
|
resp, read = conn.get_object(swifturi.container(), swifturi.item())
|
||||||
swifturi.container(),
|
|
||||||
swifturi.item()
|
|
||||||
)
|
|
||||||
except ClientException:
|
except ClientException:
|
||||||
read = ''
|
read = ""
|
||||||
_logger.exception(
|
_logger.exception("Error reading object from Swift object store")
|
||||||
'Error reading object from Swift object store')
|
|
||||||
return read
|
return read
|
||||||
else:
|
else:
|
||||||
return super()._store_file_read(fname)
|
return super()._store_file_read(fname)
|
||||||
|
|
||||||
def _store_file_write(self, key, bin_data):
|
def _store_file_write(self, key, bin_data):
|
||||||
if self._storage() == 'swift':
|
if self._storage() == "swift":
|
||||||
container = os.environ.get('SWIFT_WRITE_CONTAINER')
|
container = os.environ.get("SWIFT_WRITE_CONTAINER")
|
||||||
conn = self._get_swift_connection()
|
conn = self._get_swift_connection()
|
||||||
conn.put_container(container)
|
conn.put_container(container)
|
||||||
filename = 'swift://{}/{}'.format(container, key)
|
filename = "swift://{}/{}".format(container, key)
|
||||||
try:
|
try:
|
||||||
conn.put_object(container, key, bin_data)
|
conn.put_object(container, key, bin_data)
|
||||||
except ClientException:
|
except ClientException:
|
||||||
_logger.exception('Error writing to Swift object store')
|
_logger.exception("Error writing to Swift object store")
|
||||||
raise exceptions.UserError(_('Error writing to Swift'))
|
raise exceptions.UserError(_("Error writing to Swift")) from None
|
||||||
else:
|
else:
|
||||||
_super = super()
|
_super = super()
|
||||||
filename = _super._store_file_write(key, bin_data)
|
filename = _super._store_file_write(key, bin_data)
|
||||||
@@ -161,19 +159,18 @@ class IrAttachment(models.Model):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _store_file_delete(self, fname):
|
def _store_file_delete(self, fname):
|
||||||
if fname.startswith('swift://'):
|
if fname.startswith("swift://"):
|
||||||
swifturi = SwiftUri(fname)
|
swifturi = SwiftUri(fname)
|
||||||
container = swifturi.container()
|
container = swifturi.container()
|
||||||
# delete the file only if it is on the current configured bucket
|
# delete the file only if it is on the current configured bucket
|
||||||
# otherwise, we might delete files used on a different environment
|
# otherwise, we might delete files used on a different environment
|
||||||
if container == os.environ.get('SWIFT_WRITE_CONTAINER'):
|
if container == os.environ.get("SWIFT_WRITE_CONTAINER"):
|
||||||
conn = self._get_swift_connection()
|
conn = self._get_swift_connection()
|
||||||
try:
|
try:
|
||||||
conn.delete_object(container, swifturi.item())
|
conn.delete_object(container, swifturi.item())
|
||||||
except ClientException:
|
except ClientException:
|
||||||
_logger.exception(
|
_logger.exception(_("Error deleting an object on the Swift store"))
|
||||||
_('Error deleting an object on the Swift store'))
|
|
||||||
# we ignore the error, file will stay on the object
|
# we ignore the error, file will stay on the object
|
||||||
# storage but won't disrupt the process
|
# storage but won't disrupt the process
|
||||||
else:
|
else:
|
||||||
super()._file_delete_from_store(fname)
|
return super()._file_delete_from_store(fname)
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import re
|
|||||||
|
|
||||||
class SwiftUri(object):
|
class SwiftUri(object):
|
||||||
|
|
||||||
_url_re = re.compile("^swift:///*([^/]*)/?(.*)",
|
_url_re = re.compile("^swift:///*([^/]*)/?(.*)", re.IGNORECASE | re.UNICODE)
|
||||||
re.IGNORECASE | re.UNICODE)
|
|
||||||
|
|
||||||
def __init__(self, uri):
|
def __init__(self, uri):
|
||||||
match = self._url_re.match(uri)
|
match = self._url_re.match(uri)
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
|
|
||||||
from . import test_mock_swift_api
|
from . import test_mock_swift_api
|
||||||
|
|||||||
@@ -2,30 +2,28 @@
|
|||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import mock
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import keystoneauth1
|
||||||
|
import mock
|
||||||
from mock import patch
|
from mock import patch
|
||||||
|
|
||||||
import keystoneauth1
|
|
||||||
|
|
||||||
from odoo.addons.base.tests.test_ir_attachment import TestIrAttachment
|
|
||||||
from odoo.addons.attachment_swift.models.ir_attachment import SwiftSessionStore
|
from odoo.addons.attachment_swift.models.ir_attachment import SwiftSessionStore
|
||||||
from odoo.addons.attachment_swift.swift_uri import SwiftUri
|
from odoo.addons.attachment_swift.swift_uri import SwiftUri
|
||||||
|
from odoo.addons.base.tests.test_ir_attachment import TestIrAttachment
|
||||||
|
|
||||||
|
|
||||||
class TestAttachmentSwift(TestIrAttachment):
|
class TestAttachmentSwift(TestIrAttachment):
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
super().setUp()
|
res = super().setUp()
|
||||||
self.env['ir.config_parameter'].set_param('ir_attachment.location',
|
self.env["ir.config_parameter"].set_param("ir_attachment.location", "swift")
|
||||||
'swift')
|
return res
|
||||||
|
|
||||||
def test_session_store_get_session(self):
|
def test_session_store_get_session(self):
|
||||||
auth_url = 'auth_url'
|
auth_url = "auth_url"
|
||||||
username = 'username'
|
username = "username"
|
||||||
password = 'password'
|
password = "password"
|
||||||
project_name = 'project_name'
|
project_name = "project_name"
|
||||||
store = SwiftSessionStore()
|
store = SwiftSessionStore()
|
||||||
session = store.get_session(
|
session = store.get_session(
|
||||||
auth_url=auth_url,
|
auth_url=auth_url,
|
||||||
@@ -34,10 +32,12 @@ class TestAttachmentSwift(TestIrAttachment):
|
|||||||
project_name=project_name,
|
project_name=project_name,
|
||||||
)
|
)
|
||||||
self.assertEqual(session.auth.auth_url, auth_url)
|
self.assertEqual(session.auth.auth_url, auth_url)
|
||||||
self.assertEqual(session.auth.get_cache_id_elements().get(
|
self.assertEqual(
|
||||||
'password_username'), username)
|
session.auth.get_cache_id_elements().get("password_username"), username
|
||||||
self.assertEqual(session.auth.get_cache_id_elements().get(
|
)
|
||||||
'password_password'), password)
|
self.assertEqual(
|
||||||
|
session.auth.get_cache_id_elements().get("password_password"), password
|
||||||
|
)
|
||||||
self.assertEqual(session.auth.project_name, project_name)
|
self.assertEqual(session.auth.project_name, project_name)
|
||||||
|
|
||||||
# get the same session on a second call
|
# get the same session on a second call
|
||||||
@@ -48,73 +48,73 @@ class TestAttachmentSwift(TestIrAttachment):
|
|||||||
password=password,
|
password=password,
|
||||||
project_name=project_name,
|
project_name=project_name,
|
||||||
),
|
),
|
||||||
session
|
session,
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch('swiftclient.client')
|
@patch("swiftclient.client")
|
||||||
def test_connection(self, mock_swift_client):
|
def test_connection(self, mock_swift_client):
|
||||||
""" Test the connection to the store"""
|
"""Test the connection to the store"""
|
||||||
os.environ['SWIFT_AUTH_URL'] = 'auth_url'
|
os.environ["SWIFT_AUTH_URL"] = "auth_url"
|
||||||
os.environ['SWIFT_ACCOUNT'] = 'account'
|
os.environ["SWIFT_ACCOUNT"] = "account"
|
||||||
os.environ['SWIFT_PASSWORD'] = 'password'
|
os.environ["SWIFT_PASSWORD"] = "password"
|
||||||
os.environ['SWIFT_PROJECT_NAME'] = 'project_name'
|
os.environ["SWIFT_PROJECT_NAME"] = "project_name"
|
||||||
os.environ['SWIFT_REGION_NAME'] = 'NOWHERE'
|
os.environ["SWIFT_REGION_NAME"] = "NOWHERE"
|
||||||
attachment = self.Attachment
|
attachment = self.Attachment
|
||||||
attachment._get_swift_connection()
|
attachment._get_swift_connection()
|
||||||
mock_swift_client.Connection.assert_called_once_with(
|
mock_swift_client.Connection.assert_called_once_with(
|
||||||
session=mock.ANY,
|
session=mock.ANY,
|
||||||
os_options={'region_name': os.environ.get('SWIFT_REGION_NAME')},
|
os_options={"region_name": os.environ.get("SWIFT_REGION_NAME")},
|
||||||
)
|
)
|
||||||
__, kwargs = mock_swift_client.Connection.call_args
|
__, kwargs = mock_swift_client.Connection.call_args
|
||||||
session = kwargs['session']
|
session = kwargs["session"]
|
||||||
self.assertTrue(isinstance(session, keystoneauth1.session.Session))
|
self.assertTrue(isinstance(session, keystoneauth1.session.Session))
|
||||||
self.assertEqual(session.auth.auth_url, os.environ['SWIFT_AUTH_URL'])
|
self.assertEqual(session.auth.auth_url, os.environ["SWIFT_AUTH_URL"])
|
||||||
self.assertEqual(session.auth.get_cache_id_elements().get(
|
self.assertEqual(
|
||||||
'password_username'), os.environ['SWIFT_ACCOUNT'])
|
session.auth.get_cache_id_elements().get("password_username"),
|
||||||
self.assertEqual(session.auth.get_cache_id_elements().get(
|
os.environ["SWIFT_ACCOUNT"],
|
||||||
'password_password'), os.environ['SWIFT_PASSWORD'])
|
)
|
||||||
self.assertEqual(session.auth.project_name,
|
self.assertEqual(
|
||||||
os.environ['SWIFT_PROJECT_NAME'])
|
session.auth.get_cache_id_elements().get("password_password"),
|
||||||
|
os.environ["SWIFT_PASSWORD"],
|
||||||
|
)
|
||||||
|
self.assertEqual(session.auth.project_name, os.environ["SWIFT_PROJECT_NAME"])
|
||||||
|
|
||||||
def test_store_file_on_swift(self):
|
def test_store_file_on_swift(self):
|
||||||
"""
|
"""
|
||||||
Test writing a file
|
Test writing a file
|
||||||
"""
|
"""
|
||||||
(self.env['ir.config_parameter'].
|
(self.env["ir.config_parameter"].set_param("ir_attachment.location", "swift"))
|
||||||
set_param('ir_attachment.location', 'swift'))
|
os.environ["SWIFT_AUTH_URL"] = "auth_url"
|
||||||
os.environ['SWIFT_AUTH_URL'] = 'auth_url'
|
os.environ["SWIFT_ACCOUNT"] = "account"
|
||||||
os.environ['SWIFT_ACCOUNT'] = 'account'
|
os.environ["SWIFT_PASSWORD"] = "password"
|
||||||
os.environ['SWIFT_PASSWORD'] = 'password'
|
os.environ["SWIFT_PROJECT_NAME"] = "project_name"
|
||||||
os.environ['SWIFT_PROJECT_NAME'] = 'project_name'
|
os.environ["SWIFT_WRITE_CONTAINER"] = "my_container"
|
||||||
os.environ['SWIFT_WRITE_CONTAINER'] = 'my_container'
|
container = os.environ.get("SWIFT_WRITE_CONTAINER")
|
||||||
container = os.environ.get('SWIFT_WRITE_CONTAINER')
|
|
||||||
attachment = self.Attachment
|
attachment = self.Attachment
|
||||||
bin_data = base64.b64decode(self.blob1_b64)
|
bin_data = base64.b64decode(self.blob1_b64)
|
||||||
with patch('swiftclient.client.Connection') as MockConnection:
|
with patch("swiftclient.client.Connection") as MockConnection:
|
||||||
conn = MockConnection.return_value
|
conn = MockConnection.return_value
|
||||||
attachment.create({'name': 'a5', 'datas': self.blob1_b64})
|
attachment.create({"name": "a5", "datas": self.blob1_b64})
|
||||||
conn.put_object.assert_called_with(
|
conn.put_object.assert_called_with(
|
||||||
container,
|
container, attachment._compute_checksum(bin_data), bin_data
|
||||||
attachment._compute_checksum(bin_data),
|
)
|
||||||
bin_data)
|
|
||||||
|
|
||||||
def test_delete_file_on_swift(self):
|
def test_delete_file_on_swift(self):
|
||||||
"""
|
"""
|
||||||
Test deleting a file
|
Test deleting a file
|
||||||
"""
|
"""
|
||||||
(self.env['ir.config_parameter'].
|
(self.env["ir.config_parameter"].set_param("ir_attachment.location", "swift"))
|
||||||
set_param('ir_attachment.location', 'swift'))
|
os.environ["SWIFT_AUTH_URL"] = "auth_url"
|
||||||
os.environ['SWIFT_AUTH_URL'] = 'auth_url'
|
os.environ["SWIFT_ACCOUNT"] = "account"
|
||||||
os.environ['SWIFT_ACCOUNT'] = 'account'
|
os.environ["SWIFT_PASSWORD"] = "password"
|
||||||
os.environ['SWIFT_PASSWORD'] = 'password'
|
os.environ["SWIFT_PROJECT_NAME"] = "project_name"
|
||||||
os.environ['SWIFT_PROJECT_NAME'] = 'project_name'
|
os.environ["SWIFT_WRITE_CONTAINER"] = "my_container"
|
||||||
os.environ['SWIFT_WRITE_CONTAINER'] = 'my_container'
|
|
||||||
|
|
||||||
attachment = self.Attachment
|
attachment = self.Attachment
|
||||||
container = os.environ.get('SWIFT_WRITE_CONTAINER')
|
container = os.environ.get("SWIFT_WRITE_CONTAINER")
|
||||||
with patch('swiftclient.client.Connection') as MockConnection:
|
with patch("swiftclient.client.Connection") as MockConnection:
|
||||||
conn = MockConnection.return_value
|
conn = MockConnection.return_value
|
||||||
a5 = attachment.create({'name': 'a5', 'datas': self.blob1_b64})
|
a5 = attachment.create({"name": "a5", "datas": self.blob1_b64})
|
||||||
uri = SwiftUri(a5.store_fname)
|
uri = SwiftUri(a5.store_fname)
|
||||||
a5.unlink()
|
a5.unlink()
|
||||||
conn.delete_object.assert_called_with(container, uri.item())
|
conn.delete_object.assert_called_with(container, uri.item())
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
# Copyright 2017-2019 Camptocamp SA
|
# Copyright 2017-2019 Camptocamp SA
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||||
|
|
||||||
from odoo.addons.base.tests.test_ir_attachment import TestIrAttachment
|
|
||||||
from ..swift_uri import SwiftUri
|
|
||||||
from swiftclient.exceptions import ClientException
|
from swiftclient.exceptions import ClientException
|
||||||
|
|
||||||
|
from odoo.addons.base.tests.test_ir_attachment import TestIrAttachment
|
||||||
|
|
||||||
|
from ..swift_uri import SwiftUri
|
||||||
|
|
||||||
|
|
||||||
class TestAttachmentSwift(TestIrAttachment):
|
class TestAttachmentSwift(TestIrAttachment):
|
||||||
"""
|
"""
|
||||||
@@ -12,28 +14,26 @@ class TestAttachmentSwift(TestIrAttachment):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
super().setUp()
|
res = super().setUp()
|
||||||
self.env['ir.config_parameter'].set_param('ir_attachment.location',
|
self.env["ir.config_parameter"].set_param("ir_attachment.location", "swift")
|
||||||
'swift')
|
return res
|
||||||
|
|
||||||
def test_connection(self):
|
def test_connection(self):
|
||||||
""" Test the connection to the Swift object store """
|
"""Test the connection to the Swift object store"""
|
||||||
conn = self.Attachment._get_swift_connection()
|
conn = self.Attachment._get_swift_connection()
|
||||||
self.assertNotEqual(conn, False)
|
self.assertNotEqual(conn, False)
|
||||||
|
|
||||||
def test_store_file_on_swift(self):
|
def test_store_file_on_swift(self):
|
||||||
""" Test writing a file and then reading it """
|
"""Test writing a file and then reading it"""
|
||||||
(self.env['ir.config_parameter'].
|
(self.env["ir.config_parameter"].set_param("ir_attachment.location", "swift"))
|
||||||
set_param('ir_attachment.location', 'swift'))
|
a5 = self.Attachment.create({"name": "a5", "datas": self.blob1_b64})
|
||||||
a5 = self.Attachment.create({'name': 'a5', 'datas': self.blob1_b64})
|
|
||||||
a5bis = self.Attachment.browse(a5.id)[0]
|
a5bis = self.Attachment.browse(a5.id)[0]
|
||||||
self.assertEqual(a5.datas, a5bis.datas)
|
self.assertEqual(a5.datas, a5bis.datas)
|
||||||
|
|
||||||
def test_delete_file_on_swift(self):
|
def test_delete_file_on_swift(self):
|
||||||
""" Create a file and then test the deletion """
|
"""Create a file and then test the deletion"""
|
||||||
(self.env['ir.config_parameter'].
|
(self.env["ir.config_parameter"].set_param("ir_attachment.location", "swift"))
|
||||||
set_param('ir_attachment.location', 'swift'))
|
a5 = self.Attachment.create({"name": "a5", "datas": self.blob1_b64})
|
||||||
a5 = self.Attachment.create({'name': 'a5', 'datas': self.blob1_b64})
|
|
||||||
uri = SwiftUri(a5.store_fname)
|
uri = SwiftUri(a5.store_fname)
|
||||||
con = self.Attachment._get_swift_connection()
|
con = self.Attachment._get_swift_connection()
|
||||||
con.get_object(uri.container(), uri.item())
|
con.get_object(uri.container(), uri.item())
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"license": "AGPL-3",
|
"license": "AGPL-3",
|
||||||
"category": "Knowledge Management",
|
"category": "Knowledge Management",
|
||||||
"depends": ["base"],
|
"depends": ["base"],
|
||||||
"website": "http://www.camptocamp.com",
|
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
||||||
"data": ["data/res_config_settings_data.xml"],
|
"data": ["data/res_config_settings_data.xml"],
|
||||||
"installable": True,
|
"installable": True,
|
||||||
"auto_install": True,
|
"auto_install": True,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
<?xml version='1.0' encoding='utf-8' ?>
|
||||||
<odoo noupdate="1">
|
<odoo noupdate="1">
|
||||||
|
|
||||||
<record id="ir_attachment_storage_force_database" model="ir.config_parameter">
|
<record id="ir_attachment_storage_force_database" model="ir.config_parameter">
|
||||||
<field name="key">ir_attachment.storage.force.database</field>
|
<field name="key">ir_attachment.storage.force.database</field>
|
||||||
<field name="value">{"image/": 51200, "application/javascript": 0, "text/css": 0}</field>
|
<field
|
||||||
|
name="value"
|
||||||
|
>{"image/": 51200, "application/javascript": 0, "text/css": 0}</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
@@ -5,16 +5,16 @@ import inspect
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from .strtobool import strtobool
|
from contextlib import closing, contextmanager
|
||||||
|
|
||||||
import psycopg2
|
import psycopg2
|
||||||
import odoo
|
|
||||||
|
|
||||||
from contextlib import closing, contextmanager
|
import odoo
|
||||||
from odoo import api, exceptions, models, _
|
from odoo import _, api, exceptions, models
|
||||||
from odoo.osv.expression import AND, OR, normalize_domain
|
from odoo.osv.expression import AND, OR, normalize_domain
|
||||||
from odoo.tools.safe_eval import const_eval
|
from odoo.tools.safe_eval import const_eval
|
||||||
|
|
||||||
|
from .strtobool import strtobool
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -242,7 +242,7 @@ class IrAttachment(models.Model):
|
|||||||
if not count:
|
if not count:
|
||||||
self._store_file_delete(fname)
|
self._store_file_delete(fname)
|
||||||
else:
|
else:
|
||||||
super()._file_delete(fname)
|
return super()._file_delete(fname)
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _is_file_from_a_store(self, fname):
|
def _is_file_from_a_store(self, fname):
|
||||||
@@ -395,7 +395,8 @@ class IrAttachment(models.Model):
|
|||||||
# is required! It's because of an override of _search in ir.attachment
|
# is required! It's because of an override of _search in ir.attachment
|
||||||
# which adds ('res_field', '=', False) when the domain does not
|
# which adds ('res_field', '=', False) when the domain does not
|
||||||
# contain 'res_field'.
|
# contain 'res_field'.
|
||||||
# https://github.com/odoo/odoo/blob/9032617120138848c63b3cfa5d1913c5e5ad76db/odoo/addons/base/ir/ir_attachment.py#L344-L347
|
# https://github.com/odoo/odoo/blob/9032617120138848c63b3cfa5d1913c5e5ad76db/odoo/addons/base/ir/ir_attachment.py#L344-L347 # noqa: B950
|
||||||
|
|
||||||
domain = [
|
domain = [
|
||||||
"!",
|
"!",
|
||||||
("store_fname", "=like", "{}://%".format(storage)),
|
("store_fname", "=like", "{}://%".format(storage)),
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
_MAP = {
|
_MAP = {
|
||||||
'y': True,
|
"y": True,
|
||||||
'yes': True,
|
"yes": True,
|
||||||
't': True,
|
"t": True,
|
||||||
'true': True,
|
"true": True,
|
||||||
'on': True,
|
"on": True,
|
||||||
'1': True,
|
"1": True,
|
||||||
'n': False,
|
"n": False,
|
||||||
'no': False,
|
"no": False,
|
||||||
'f': False,
|
"f": False,
|
||||||
'false': False,
|
"false": False,
|
||||||
'off': False,
|
"off": False,
|
||||||
'0': False
|
"0": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def strtobool(value):
|
def strtobool(value):
|
||||||
try:
|
try:
|
||||||
return _MAP[str(value).lower()]
|
return _MAP[str(value).lower()]
|
||||||
except KeyError:
|
except KeyError as error:
|
||||||
raise ValueError('"{}" is not a valid bool value'.format(value))
|
raise ValueError('"{}" is not a valid bool value'.format(value)) from error
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
from . import fields
|
from . import fields
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"version": "15.0.1.0.0",
|
"version": "15.0.1.0.0",
|
||||||
"category": "Technical Settings",
|
"category": "Technical Settings",
|
||||||
"author": "Camptocamp, Odoo Community Association (OCA)",
|
"author": "Camptocamp, Odoo Community Association (OCA)",
|
||||||
|
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
||||||
"license": "AGPL-3",
|
"license": "AGPL-3",
|
||||||
"depends": [
|
"depends": [
|
||||||
"base_attachment_object_storage",
|
"base_attachment_object_storage",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import unicodedata
|
|||||||
|
|
||||||
from odoo import fields
|
from odoo import fields
|
||||||
|
|
||||||
|
|
||||||
fields.Field.__doc__ += """
|
fields.Field.__doc__ += """
|
||||||
|
|
||||||
.. _field-fileurl:
|
.. _field-fileurl:
|
||||||
@@ -29,10 +28,10 @@ fields.Field.__doc__ += """
|
|||||||
class FileURL(fields.Binary):
|
class FileURL(fields.Binary):
|
||||||
|
|
||||||
_slots = {
|
_slots = {
|
||||||
'attachment': True, # Override default with True
|
"attachment": True, # Override default with True
|
||||||
'storage_location': '', # External storage activated on the system (cf base_attachment_storage) # noqa
|
"storage_location": "", # External storage activated on the system (cf base_attachment_storage) # noqa
|
||||||
'storage_path': '', # Path to be used as storage key (prefix of filename) # noqa
|
"storage_path": "", # Path to be used as storage key (prefix of filename) # noqa
|
||||||
'filename': '', # Field to use to store the filename on ir.attachment
|
"filename": "", # Field to use to store the filename on ir.attachment
|
||||||
}
|
}
|
||||||
|
|
||||||
# pylint: disable=method-required-super
|
# pylint: disable=method-required-super
|
||||||
@@ -47,22 +46,22 @@ class FileURL(fields.Binary):
|
|||||||
if not value:
|
if not value:
|
||||||
continue
|
continue
|
||||||
vals = {
|
vals = {
|
||||||
'name': self.name,
|
"name": self.name,
|
||||||
'res_model': self.model_name,
|
"res_model": self.model_name,
|
||||||
'res_field': self.name,
|
"res_field": self.name,
|
||||||
'res_id': record.id,
|
"res_id": record.id,
|
||||||
'type': 'binary',
|
"type": "binary",
|
||||||
'datas': value,
|
"datas": value,
|
||||||
}
|
}
|
||||||
fname = False
|
fname = False
|
||||||
if self.filename:
|
if self.filename:
|
||||||
fname = record[self.filename]
|
fname = record[self.filename]
|
||||||
vals['datas_fname'] = fname
|
vals["datas_fname"] = fname
|
||||||
if fname and self.storage_path:
|
if fname and self.storage_path:
|
||||||
storage_key = self._build_storage_key(fname)
|
storage_key = self._build_storage_key(fname)
|
||||||
if not fname:
|
if not fname:
|
||||||
storage_key = False
|
storage_key = False
|
||||||
env['ir.attachment'].sudo().with_context(
|
env["ir.attachment"].sudo().with_context(
|
||||||
binary_field_real_user=env.user,
|
binary_field_real_user=env.user,
|
||||||
storage_location=self.storage_location,
|
storage_location=self.storage_location,
|
||||||
force_storage_key=storage_key,
|
force_storage_key=storage_key,
|
||||||
@@ -80,21 +79,22 @@ class FileURL(fields.Binary):
|
|||||||
storage_location=self.storage_location,
|
storage_location=self.storage_location,
|
||||||
force_storage_key=storage_key,
|
force_storage_key=storage_key,
|
||||||
),
|
),
|
||||||
value
|
value,
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _setup_regular_base(self, model):
|
def _setup_regular_base(self, model):
|
||||||
super()._setup_regular_base(model)
|
res = super()._setup_regular_base(model)
|
||||||
if self.storage_path:
|
if self.storage_path:
|
||||||
assert self.filename is not None, \
|
assert self.filename is not None, (
|
||||||
"Field %s defines storage_path without filename" % self
|
"Field %s defines storage_path without filename" % self
|
||||||
|
)
|
||||||
|
return res
|
||||||
|
|
||||||
def _build_storage_key(self, filename):
|
def _build_storage_key(self, filename):
|
||||||
return '/'.join([
|
return "/".join(
|
||||||
self.storage_path.rstrip('/'),
|
[self.storage_path.rstrip("/"), unicodedata.normalize("NFKC", filename)]
|
||||||
unicodedata.normalize('NFKC', filename)
|
)
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
fields.FileURL = FileURL
|
fields.FileURL = FileURL
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"logging_json",
|
"logging_json",
|
||||||
"server_environment", # OCA/server-tools
|
"server_environment", # OCA/server-tools
|
||||||
],
|
],
|
||||||
"website": "https://www.camptocamp.com",
|
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
||||||
"data": [],
|
"data": [],
|
||||||
"installable": True,
|
"installable": True,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
|
|
||||||
from . import cloud_platform
|
from . import cloud_platform
|
||||||
|
|||||||
@@ -4,46 +4,39 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from .strtobool import strtobool
|
|
||||||
|
|
||||||
from odoo import api, models
|
from odoo import api, models
|
||||||
from odoo.tools.config import config
|
from odoo.tools.config import config
|
||||||
|
|
||||||
|
from .strtobool import strtobool
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def is_true(strval):
|
def is_true(strval):
|
||||||
return bool(strtobool(strval or '0'))
|
return bool(strtobool(strval or "0"))
|
||||||
|
|
||||||
|
|
||||||
PlatformConfig = namedtuple(
|
PlatformConfig = namedtuple("PlatformConfig", "filestore")
|
||||||
'PlatformConfig',
|
|
||||||
'filestore'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
FilestoreKind = namedtuple(
|
FilestoreKind = namedtuple("FilestoreKind", ["name", "location"])
|
||||||
'FilestoreKind',
|
|
||||||
['name', 'location']
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CloudPlatform(models.AbstractModel):
|
class CloudPlatform(models.AbstractModel):
|
||||||
_name = 'cloud.platform'
|
_name = "cloud.platform"
|
||||||
_description = 'cloud.platform'
|
_description = "cloud.platform"
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _default_config(self):
|
def _default_config(self):
|
||||||
return PlatformConfig(self._filestore_kinds()['db'])
|
return PlatformConfig(self._filestore_kinds()["db"])
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _filestore_kinds(self):
|
def _filestore_kinds(self):
|
||||||
return {
|
return {
|
||||||
'db': FilestoreKind('db', 'local'),
|
"db": FilestoreKind("db", "local"),
|
||||||
'file': FilestoreKind('file', 'local'),
|
"file": FilestoreKind("file", "local"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
@@ -53,33 +46,31 @@ class CloudPlatform(models.AbstractModel):
|
|||||||
@api.model
|
@api.model
|
||||||
def _config_by_server_env(self, platform_kind, environment):
|
def _config_by_server_env(self, platform_kind, environment):
|
||||||
configs_getter = getattr(
|
configs_getter = getattr(
|
||||||
self,
|
self, "_config_by_server_env_for_%s" % platform_kind, None
|
||||||
'_config_by_server_env_for_%s' % platform_kind,
|
|
||||||
None
|
|
||||||
)
|
)
|
||||||
configs = configs_getter() if configs_getter else {}
|
configs = configs_getter() if configs_getter else {}
|
||||||
return configs.get(environment) or self._default_config()
|
return configs.get(environment) or self._default_config()
|
||||||
|
|
||||||
def _get_running_env(self):
|
def _get_running_env(self):
|
||||||
environment_name = config['running_env']
|
environment_name = config["running_env"]
|
||||||
if environment_name.startswith('labs'):
|
if environment_name.startswith("labs"):
|
||||||
# We allow to have environments such as 'labs-logistics'
|
# We allow to have environments such as 'labs-logistics'
|
||||||
# or 'labs-finance', in order to have the matching ribbon.
|
# or 'labs-finance', in order to have the matching ribbon.
|
||||||
environment_name = 'labs'
|
environment_name = "labs"
|
||||||
return environment_name
|
return environment_name
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _install(self, platform_kind):
|
def _install(self, platform_kind):
|
||||||
assert platform_kind in self._platform_kinds()
|
assert platform_kind in self._platform_kinds()
|
||||||
params = self.env['ir.config_parameter'].sudo()
|
params = self.env["ir.config_parameter"].sudo()
|
||||||
params.set_param('cloud.platform.kind', platform_kind)
|
params.set_param("cloud.platform.kind", platform_kind)
|
||||||
environment_name = self._get_running_env()
|
environment_name = self._get_running_env()
|
||||||
configs = self._config_by_server_env(platform_kind, environment_name)
|
configs = self._config_by_server_env(platform_kind, environment_name)
|
||||||
params.set_param('ir_attachment.location', configs.filestore.name)
|
params.set_param("ir_attachment.location", configs.filestore.name)
|
||||||
self.check()
|
self.check()
|
||||||
if configs.filestore.location == 'remote':
|
if configs.filestore.location == "remote":
|
||||||
self.env['ir.attachment'].sudo().force_storage()
|
self.env["ir.attachment"].sudo().force_storage()
|
||||||
_logger.info('cloud platform configured for {}'.format(platform_kind))
|
_logger.info("cloud platform configured for {}".format(platform_kind))
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def install(self):
|
def install(self):
|
||||||
@@ -91,39 +82,39 @@ class CloudPlatform(models.AbstractModel):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _check_redis(self, environment_name):
|
def _check_redis(self, environment_name):
|
||||||
if environment_name in ('prod', 'integration', 'labs', 'test'):
|
if environment_name in ("prod", "integration", "labs", "test"):
|
||||||
assert is_true(os.environ.get('ODOO_SESSION_REDIS')), (
|
assert is_true(os.environ.get("ODOO_SESSION_REDIS")), (
|
||||||
"Redis must be activated on prod, integration, labs,"
|
"Redis must be activated on prod, integration, labs,"
|
||||||
" test instances. This is done by setting ODOO_SESSION_REDIS=1."
|
" test instances. This is done by setting ODOO_SESSION_REDIS=1."
|
||||||
)
|
)
|
||||||
assert (os.environ.get('ODOO_SESSION_REDIS_HOST') or
|
assert (
|
||||||
os.environ.get('ODOO_SESSION_REDIS_SENTINEL_HOST') or
|
os.environ.get("ODOO_SESSION_REDIS_HOST")
|
||||||
os.environ.get('ODOO_SESSION_REDIS_URL')), (
|
or os.environ.get("ODOO_SESSION_REDIS_SENTINEL_HOST")
|
||||||
|
or os.environ.get("ODOO_SESSION_REDIS_URL")
|
||||||
|
), (
|
||||||
"ODOO_SESSION_REDIS_HOST or "
|
"ODOO_SESSION_REDIS_HOST or "
|
||||||
"ODOO_SESSION_REDIS_SENTINEL_HOST or "
|
"ODOO_SESSION_REDIS_SENTINEL_HOST or "
|
||||||
"ODOO_SESSION_REDIS_URL "
|
"ODOO_SESSION_REDIS_URL "
|
||||||
"environment variable is required to connect on Redis"
|
"environment variable is required to connect on Redis"
|
||||||
)
|
)
|
||||||
assert os.environ.get('ODOO_SESSION_REDIS_PREFIX'), (
|
assert os.environ.get("ODOO_SESSION_REDIS_PREFIX"), (
|
||||||
"ODOO_SESSION_REDIS_PREFIX environment variable is required "
|
"ODOO_SESSION_REDIS_PREFIX environment variable is required "
|
||||||
"to store sessions on Redis"
|
"to store sessions on Redis"
|
||||||
)
|
)
|
||||||
|
|
||||||
prefix = os.environ['ODOO_SESSION_REDIS_PREFIX']
|
prefix = os.environ["ODOO_SESSION_REDIS_PREFIX"]
|
||||||
assert re.match(r'^[a-z-0-9]+-odoo-[a-z-0-9]+$', prefix), (
|
assert re.match(r"^[a-z-0-9]+-odoo-[a-z-0-9]+$", prefix), (
|
||||||
"ODOO_SESSION_REDIS_PREFIX must match '<client>-odoo-<env>'"
|
"ODOO_SESSION_REDIS_PREFIX must match '<client>-odoo-<env>'"
|
||||||
", we got: '%s'" % (prefix,)
|
", we got: '%s'" % (prefix,)
|
||||||
)
|
)
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def check(self):
|
def check(self):
|
||||||
if is_true(os.environ.get('ODOO_CLOUD_PLATFORM_UNSAFE')):
|
if is_true(os.environ.get("ODOO_CLOUD_PLATFORM_UNSAFE")):
|
||||||
_logger.warning(
|
_logger.warning("cloud platform checks disabled, this is not safe")
|
||||||
"cloud platform checks disabled, this is not safe"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
params = self.env['ir.config_parameter'].sudo()
|
params = self.env["ir.config_parameter"].sudo()
|
||||||
kind = params.get_param('cloud.platform.kind')
|
kind = params.get_param("cloud.platform.kind")
|
||||||
if not kind:
|
if not kind:
|
||||||
_logger.warning(
|
_logger.warning(
|
||||||
"cloud platform not configured, you should "
|
"cloud platform not configured, you should "
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
_MAP = {
|
_MAP = {
|
||||||
'y': True,
|
"y": True,
|
||||||
'yes': True,
|
"yes": True,
|
||||||
't': True,
|
"t": True,
|
||||||
'true': True,
|
"true": True,
|
||||||
'on': True,
|
"on": True,
|
||||||
'1': True,
|
"1": True,
|
||||||
'n': False,
|
"n": False,
|
||||||
'no': False,
|
"no": False,
|
||||||
'f': False,
|
"f": False,
|
||||||
'false': False,
|
"false": False,
|
||||||
'off': False,
|
"off": False,
|
||||||
'0': False
|
"0": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def strtobool(value):
|
def strtobool(value):
|
||||||
try:
|
try:
|
||||||
return _MAP[str(value).lower()]
|
return _MAP[str(value).lower()]
|
||||||
except KeyError:
|
except KeyError as error:
|
||||||
raise ValueError('"{}" is not a valid bool value'.format(value))
|
raise ValueError('"{}" is not a valid bool value'.format(value)) from error
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
|
|
||||||
def install(ctx):
|
def install(ctx):
|
||||||
ctx.env['cloud.platform'].install()
|
ctx.env["cloud.platform"].install()
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
Cloud Platform Azure
|
# Cloud Platform Azure
|
||||||
====================
|
|
||||||
|
|
||||||
Install addons specific to the Azure setup.
|
Install addons specific to the Azure setup.
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"cloud_platform_ovh",
|
"cloud_platform_ovh",
|
||||||
"cloud_platform_exoscale",
|
"cloud_platform_exoscale",
|
||||||
],
|
],
|
||||||
"website": "https://www.camptocamp.com",
|
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
||||||
"data": [],
|
"data": [],
|
||||||
"installable": True,
|
"installable": True,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
# Copyright 2016-2021 Camptocamp SA
|
# Copyright 2016-2021 Camptocamp SA
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||||
|
|
||||||
import re
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
from odoo import models, api
|
from odoo import api, models
|
||||||
from odoo.addons.cloud_platform.models.cloud_platform import FilestoreKind
|
|
||||||
from odoo.addons.cloud_platform.models.cloud_platform import PlatformConfig
|
|
||||||
|
|
||||||
|
from odoo.addons.cloud_platform.models.cloud_platform import (
|
||||||
|
FilestoreKind,
|
||||||
|
PlatformConfig,
|
||||||
|
)
|
||||||
|
|
||||||
AZURE_STORE_KIND = FilestoreKind("azure", "remote")
|
AZURE_STORE_KIND = FilestoreKind("azure", "remote")
|
||||||
|
|
||||||
@@ -42,8 +44,7 @@ class CloudPlatform(models.AbstractModel):
|
|||||||
@api.model
|
@api.model
|
||||||
def _check_filestore(self, environment_name):
|
def _check_filestore(self, environment_name):
|
||||||
params = self.env["ir.config_parameter"].sudo()
|
params = self.env["ir.config_parameter"].sudo()
|
||||||
use_azure = (params.get_param("ir_attachment.location") ==
|
use_azure = params.get_param("ir_attachment.location") == AZURE_STORE_KIND.name
|
||||||
AZURE_STORE_KIND.name)
|
|
||||||
if environment_name in ("prod", "integration"):
|
if environment_name in ("prod", "integration"):
|
||||||
# Labs instances use azure by default, but we don't want
|
# Labs instances use azure by default, but we don't want
|
||||||
# to enforce it in case we want to test something with a different
|
# to enforce it in case we want to test something with a different
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
Cloud Platform Exoscale
|
# Cloud Platform Exoscale
|
||||||
=======================
|
|
||||||
|
|
||||||
Install addons specific to the Exoscale setup.
|
Install addons specific to the Exoscale setup.
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"excludes": [
|
"excludes": [
|
||||||
"cloud_platform_ovh",
|
"cloud_platform_ovh",
|
||||||
],
|
],
|
||||||
"website": "https://www.camptocamp.com",
|
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
||||||
"data": [],
|
"data": [],
|
||||||
"installable": False,
|
"installable": False,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,51 @@
|
|||||||
# Copyright 2016-2019 Camptocamp SA
|
# Copyright 2016-2019 Camptocamp SA
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||||
|
|
||||||
import re
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
from odoo import models, api
|
from odoo import api, models
|
||||||
from odoo.addons.cloud_platform.models.cloud_platform import FilestoreKind
|
|
||||||
from odoo.addons.cloud_platform.models.cloud_platform import PlatformConfig
|
|
||||||
|
|
||||||
|
from odoo.addons.cloud_platform.models.cloud_platform import (
|
||||||
|
FilestoreKind,
|
||||||
|
PlatformConfig,
|
||||||
|
)
|
||||||
|
|
||||||
S3_STORE_KIND = FilestoreKind('s3', 'remote')
|
S3_STORE_KIND = FilestoreKind("s3", "remote")
|
||||||
|
|
||||||
|
|
||||||
class CloudPlatform(models.AbstractModel):
|
class CloudPlatform(models.AbstractModel):
|
||||||
_inherit = 'cloud.platform'
|
_inherit = "cloud.platform"
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _filestore_kinds(self):
|
def _filestore_kinds(self):
|
||||||
kinds = super(CloudPlatform, self)._filestore_kinds()
|
kinds = super(CloudPlatform, self)._filestore_kinds()
|
||||||
kinds['s3'] = S3_STORE_KIND
|
kinds["s3"] = S3_STORE_KIND
|
||||||
return kinds
|
return kinds
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _platform_kinds(self):
|
def _platform_kinds(self):
|
||||||
kinds = super(CloudPlatform, self)._platform_kinds()
|
kinds = super(CloudPlatform, self)._platform_kinds()
|
||||||
kinds.append('exoscale')
|
kinds.append("exoscale")
|
||||||
return kinds
|
return kinds
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _config_by_server_env_for_exoscale(self):
|
def _config_by_server_env_for_exoscale(self):
|
||||||
fs_kinds = self._filestore_kinds()
|
fs_kinds = self._filestore_kinds()
|
||||||
configs = {
|
configs = {
|
||||||
'prod': PlatformConfig(filestore=fs_kinds['s3']),
|
"prod": PlatformConfig(filestore=fs_kinds["s3"]),
|
||||||
'integration': PlatformConfig(filestore=fs_kinds['s3']),
|
"integration": PlatformConfig(filestore=fs_kinds["s3"]),
|
||||||
'labs': PlatformConfig(filestore=fs_kinds['s3']),
|
"labs": PlatformConfig(filestore=fs_kinds["s3"]),
|
||||||
'test': PlatformConfig(filestore=fs_kinds['db']),
|
"test": PlatformConfig(filestore=fs_kinds["db"]),
|
||||||
'dev': PlatformConfig(filestore=fs_kinds['db']),
|
"dev": PlatformConfig(filestore=fs_kinds["db"]),
|
||||||
}
|
}
|
||||||
return configs
|
return configs
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _check_filestore(self, environment_name):
|
def _check_filestore(self, environment_name):
|
||||||
params = self.env['ir.config_parameter'].sudo()
|
params = self.env["ir.config_parameter"].sudo()
|
||||||
use_s3 = (params.get_param('ir_attachment.location') ==
|
use_s3 = params.get_param("ir_attachment.location") == S3_STORE_KIND.name
|
||||||
S3_STORE_KIND.name)
|
if environment_name in ("prod", "integration"):
|
||||||
if environment_name in ('prod', 'integration'):
|
|
||||||
# Labs instances use s3 by default, but we don't want
|
# Labs instances use s3 by default, but we don't want
|
||||||
# to enforce it in case we want to test something with a different
|
# to enforce it in case we want to test something with a different
|
||||||
# storage. At your own risks!
|
# storage. At your own risks!
|
||||||
@@ -55,16 +56,16 @@ class CloudPlatform(models.AbstractModel):
|
|||||||
"automatically."
|
"automatically."
|
||||||
)
|
)
|
||||||
if use_s3:
|
if use_s3:
|
||||||
assert os.environ.get('AWS_ACCESS_KEY_ID'), (
|
assert os.environ.get("AWS_ACCESS_KEY_ID"), (
|
||||||
"AWS_ACCESS_KEY_ID environment variable is required when "
|
"AWS_ACCESS_KEY_ID environment variable is required when "
|
||||||
"ir_attachment.location is 's3'."
|
"ir_attachment.location is 's3'."
|
||||||
)
|
)
|
||||||
assert os.environ.get('AWS_SECRET_ACCESS_KEY'), (
|
assert os.environ.get("AWS_SECRET_ACCESS_KEY"), (
|
||||||
"AWS_SECRET_ACCESS_KEY environment variable is required when "
|
"AWS_SECRET_ACCESS_KEY environment variable is required when "
|
||||||
"ir_attachment.location is 's3'."
|
"ir_attachment.location is 's3'."
|
||||||
)
|
)
|
||||||
bucket_name = os.environ.get('AWS_BUCKETNAME', '')
|
bucket_name = os.environ.get("AWS_BUCKETNAME", "")
|
||||||
if environment_name in ('prod', 'integration', 'labs'):
|
if environment_name in ("prod", "integration", "labs"):
|
||||||
assert bucket_name, (
|
assert bucket_name, (
|
||||||
"AWS_BUCKETNAME environment variable is required when "
|
"AWS_BUCKETNAME environment variable is required when "
|
||||||
"ir_attachment.location is 's3'.\n"
|
"ir_attachment.location is 's3'.\n"
|
||||||
@@ -80,10 +81,10 @@ class CloudPlatform(models.AbstractModel):
|
|||||||
#
|
#
|
||||||
# Use AWS_BUCKETNAME_UNSTRUCTURED to by-pass check on bucket name
|
# Use AWS_BUCKETNAME_UNSTRUCTURED to by-pass check on bucket name
|
||||||
# structure
|
# structure
|
||||||
if os.environ.get('AWS_BUCKETNAME_UNSTRUCTURED'):
|
if os.environ.get("AWS_BUCKETNAME_UNSTRUCTURED"):
|
||||||
return
|
return
|
||||||
prod_bucket = bool(re.match(r'[a-z-0-9]+-odoo-prod', bucket_name))
|
prod_bucket = bool(re.match(r"[a-z-0-9]+-odoo-prod", bucket_name))
|
||||||
if environment_name == 'prod':
|
if environment_name == "prod":
|
||||||
assert prod_bucket, (
|
assert prod_bucket, (
|
||||||
"AWS_BUCKETNAME should match '<client>-odoo-prod', "
|
"AWS_BUCKETNAME should match '<client>-odoo-prod', "
|
||||||
"we got: '%s'" % (bucket_name,)
|
"we got: '%s'" % (bucket_name,)
|
||||||
@@ -96,9 +97,9 @@ class CloudPlatform(models.AbstractModel):
|
|||||||
"we got: '%s'" % (bucket_name,)
|
"we got: '%s'" % (bucket_name,)
|
||||||
)
|
)
|
||||||
|
|
||||||
elif environment_name == 'test':
|
elif environment_name == "test":
|
||||||
# store in DB so we don't have files local to the host
|
# store in DB so we don't have files local to the host
|
||||||
assert params.get_param('ir_attachment.location') == 'db', (
|
assert params.get_param("ir_attachment.location") == "db", (
|
||||||
"In test instances, files must be stored in the database with "
|
"In test instances, files must be stored in the database with "
|
||||||
"'ir_attachment.location' set to 'db'. This is "
|
"'ir_attachment.location' set to 'db'. This is "
|
||||||
"automatically set by the function 'install()'."
|
"automatically set by the function 'install()'."
|
||||||
@@ -106,4 +107,4 @@ class CloudPlatform(models.AbstractModel):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def install(self):
|
def install(self):
|
||||||
self._install('exoscale')
|
self._install("exoscale")
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
Cloud Platform OVH
|
# Cloud Platform OVH
|
||||||
==================
|
|
||||||
|
|
||||||
Install addons specific to the OVH setup.
|
Install addons specific to the OVH setup.
|
||||||
|
|
||||||
* The object storage is Swift
|
* The object storage is Swift
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"excludes": [
|
"excludes": [
|
||||||
"cloud_platform_exoscale",
|
"cloud_platform_exoscale",
|
||||||
],
|
],
|
||||||
"website": "https://www.camptocamp.com",
|
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
||||||
"data": [],
|
"data": [],
|
||||||
"installable": False,
|
"installable": False,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,51 @@
|
|||||||
# Copyright 2017-2019 Camptocamp SA
|
# Copyright 2017-2019 Camptocamp SA
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||||
|
|
||||||
import re
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
from odoo import api, models
|
from odoo import api, models
|
||||||
|
|
||||||
from odoo.addons.cloud_platform.models.cloud_platform import FilestoreKind
|
from odoo.addons.cloud_platform.models.cloud_platform import (
|
||||||
from odoo.addons.cloud_platform.models.cloud_platform import PlatformConfig
|
FilestoreKind,
|
||||||
|
PlatformConfig,
|
||||||
|
)
|
||||||
|
|
||||||
|
SWIFT_STORE_KIND = FilestoreKind("swift", "remote")
|
||||||
SWIFT_STORE_KIND = FilestoreKind('swift', 'remote')
|
|
||||||
|
|
||||||
|
|
||||||
class CloudPlatform(models.AbstractModel):
|
class CloudPlatform(models.AbstractModel):
|
||||||
_inherit = 'cloud.platform'
|
_inherit = "cloud.platform"
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _filestore_kinds(self):
|
def _filestore_kinds(self):
|
||||||
kinds = super(CloudPlatform, self)._filestore_kinds()
|
kinds = super(CloudPlatform, self)._filestore_kinds()
|
||||||
kinds['swift'] = SWIFT_STORE_KIND
|
kinds["swift"] = SWIFT_STORE_KIND
|
||||||
return kinds
|
return kinds
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _platform_kinds(self):
|
def _platform_kinds(self):
|
||||||
kinds = super()._platform_kinds()
|
kinds = super()._platform_kinds()
|
||||||
kinds.append('ovh')
|
kinds.append("ovh")
|
||||||
return kinds
|
return kinds
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _config_by_server_env_for_ovh(self):
|
def _config_by_server_env_for_ovh(self):
|
||||||
fs_kinds = self._filestore_kinds()
|
fs_kinds = self._filestore_kinds()
|
||||||
configs = {
|
configs = {
|
||||||
'prod': PlatformConfig(filestore=fs_kinds['swift']),
|
"prod": PlatformConfig(filestore=fs_kinds["swift"]),
|
||||||
'integration': PlatformConfig(filestore=fs_kinds['swift']),
|
"integration": PlatformConfig(filestore=fs_kinds["swift"]),
|
||||||
'labs': PlatformConfig(filestore=fs_kinds['swift']),
|
"labs": PlatformConfig(filestore=fs_kinds["swift"]),
|
||||||
'test': PlatformConfig(filestore=fs_kinds['db']),
|
"test": PlatformConfig(filestore=fs_kinds["db"]),
|
||||||
'dev': PlatformConfig(filestore=fs_kinds['db']),
|
"dev": PlatformConfig(filestore=fs_kinds["db"]),
|
||||||
}
|
}
|
||||||
return configs
|
return configs
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _check_filestore(self, environment_name):
|
def _check_filestore(self, environment_name):
|
||||||
params = self.env['ir.config_parameter'].sudo()
|
params = self.env["ir.config_parameter"].sudo()
|
||||||
use_swift = (params.get_param('ir_attachment.location') ==
|
use_swift = params.get_param("ir_attachment.location") == SWIFT_STORE_KIND.name
|
||||||
SWIFT_STORE_KIND.name)
|
if environment_name in ("prod", "integration"):
|
||||||
if environment_name in ('prod', 'integration'):
|
|
||||||
# Labs instances use swift by default, but we don't want
|
# Labs instances use swift by default, but we don't want
|
||||||
# to enforce it in case we want to test something with a different
|
# to enforce it in case we want to test something with a different
|
||||||
# storage. At your own risks!
|
# storage. At your own risks!
|
||||||
@@ -56,20 +56,20 @@ class CloudPlatform(models.AbstractModel):
|
|||||||
"automatically."
|
"automatically."
|
||||||
)
|
)
|
||||||
if use_swift:
|
if use_swift:
|
||||||
assert os.environ.get('SWIFT_AUTH_URL'), (
|
assert os.environ.get("SWIFT_AUTH_URL"), (
|
||||||
"SWIFT_AUTH_URL environment variable is required when "
|
"SWIFT_AUTH_URL environment variable is required when "
|
||||||
"ir_attachment.location is 'swift'."
|
"ir_attachment.location is 'swift'."
|
||||||
)
|
)
|
||||||
assert os.environ.get('SWIFT_ACCOUNT'), (
|
assert os.environ.get("SWIFT_ACCOUNT"), (
|
||||||
"SWIFT_ACCOUNT environment variable is required when "
|
"SWIFT_ACCOUNT environment variable is required when "
|
||||||
"ir_attachment.location is 'swift'."
|
"ir_attachment.location is 'swift'."
|
||||||
)
|
)
|
||||||
assert os.environ.get('SWIFT_PASSWORD'), (
|
assert os.environ.get("SWIFT_PASSWORD"), (
|
||||||
"SWIFT_PASSWORD environment variable is required when "
|
"SWIFT_PASSWORD environment variable is required when "
|
||||||
"ir_attachment.location is 'swift'."
|
"ir_attachment.location is 'swift'."
|
||||||
)
|
)
|
||||||
container_name = os.environ.get('SWIFT_WRITE_CONTAINER', '')
|
container_name = os.environ.get("SWIFT_WRITE_CONTAINER", "")
|
||||||
if environment_name in ('prod', 'integration', 'labs'):
|
if environment_name in ("prod", "integration", "labs"):
|
||||||
assert container_name, (
|
assert container_name, (
|
||||||
"SWIFT_WRITE_CONTAINER environment variable is required when "
|
"SWIFT_WRITE_CONTAINER environment variable is required when "
|
||||||
"ir_attachment.location is 'swift'.\n"
|
"ir_attachment.location is 'swift'.\n"
|
||||||
@@ -80,16 +80,15 @@ class CloudPlatform(models.AbstractModel):
|
|||||||
"If you don't actually need a bucket, change the"
|
"If you don't actually need a bucket, change the"
|
||||||
" 'ir_attachment.location' parameter."
|
" 'ir_attachment.location' parameter."
|
||||||
)
|
)
|
||||||
prod_container = bool(re.match(r'[a-z0-9-]+-odoo-prod',
|
prod_container = bool(re.match(r"[a-z0-9-]+-odoo-prod", container_name))
|
||||||
container_name))
|
|
||||||
# A bucket name is defined under the following format
|
# A bucket name is defined under the following format
|
||||||
# <client>-odoo-<env>
|
# <client>-odoo-<env>
|
||||||
#
|
#
|
||||||
# Use SWIFT_WRITE_CONTAINER_UNSTRUCTURED to by-pass check on bucket name
|
# Use SWIFT_WRITE_CONTAINER_UNSTRUCTURED to by-pass check on bucket name
|
||||||
# structure
|
# structure
|
||||||
if os.environ.get('SWIFT_WRITE_CONTAINER_UNSTRUCTURED'):
|
if os.environ.get("SWIFT_WRITE_CONTAINER_UNSTRUCTURED"):
|
||||||
return
|
return
|
||||||
if environment_name == 'prod':
|
if environment_name == "prod":
|
||||||
assert prod_container, (
|
assert prod_container, (
|
||||||
"SWIFT_WRITE_CONTAINER should match '<client>-odoo-prod', "
|
"SWIFT_WRITE_CONTAINER should match '<client>-odoo-prod', "
|
||||||
"we got: '%s'" % (container_name,)
|
"we got: '%s'" % (container_name,)
|
||||||
@@ -101,9 +100,9 @@ class CloudPlatform(models.AbstractModel):
|
|||||||
"SWIFT_WRITE_CONTAINER should not match "
|
"SWIFT_WRITE_CONTAINER should not match "
|
||||||
"'<client>-odoo-prod', we got: '%s'" % (container_name,)
|
"'<client>-odoo-prod', we got: '%s'" % (container_name,)
|
||||||
)
|
)
|
||||||
elif environment_name == 'test':
|
elif environment_name == "test":
|
||||||
# store in DB so we don't have files local to the host
|
# store in DB so we don't have files local to the host
|
||||||
assert params.get_param('ir_attachment.location') == 'db', (
|
assert params.get_param("ir_attachment.location") == "db", (
|
||||||
"In test instances, files must be stored in the database with "
|
"In test instances, files must be stored in the database with "
|
||||||
"'ir_attachment.location' set to 'db'. This is "
|
"'ir_attachment.location' set to 'db'. This is "
|
||||||
"automatically set by the function 'install()'."
|
"automatically set by the function 'install()'."
|
||||||
@@ -111,4 +110,4 @@ class CloudPlatform(models.AbstractModel):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def install(self):
|
def install(self):
|
||||||
self._install('ovh')
|
self._install("ovh")
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"depends": [
|
"depends": [
|
||||||
"base",
|
"base",
|
||||||
],
|
],
|
||||||
"website": "http://www.camptocamp.com",
|
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
||||||
"data": [],
|
"data": [],
|
||||||
"installable": True,
|
"installable": True,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
|
|
||||||
from . import ir_qweb
|
from . import ir_qweb
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Copyright 2016-2019 Camptocamp SA
|
# Copyright 2016-2019 Camptocamp SA
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||||
|
|
||||||
from odoo.tools import config
|
|
||||||
from odoo import models
|
from odoo import models
|
||||||
|
from odoo.tools import config
|
||||||
|
|
||||||
|
|
||||||
class IrQweb(models.AbstractModel):
|
class IrQweb(models.AbstractModel):
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
|
|
||||||
from . import json_log
|
from . import json_log
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"external_dependencies": {
|
"external_dependencies": {
|
||||||
"python": ["python-json-logger"],
|
"python": ["python-json-logger"],
|
||||||
},
|
},
|
||||||
"website": "http://www.camptocamp.com",
|
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
||||||
"data": [],
|
"data": [],
|
||||||
"installable": True,
|
"installable": True,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import os
|
|||||||
import threading
|
import threading
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from .strtobool import strtobool
|
|
||||||
|
|
||||||
from odoo import http
|
from odoo import http
|
||||||
|
|
||||||
|
from .strtobool import strtobool
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
+14
-14
@@ -1,21 +1,21 @@
|
|||||||
_MAP = {
|
_MAP = {
|
||||||
'y': True,
|
"y": True,
|
||||||
'yes': True,
|
"yes": True,
|
||||||
't': True,
|
"t": True,
|
||||||
'true': True,
|
"true": True,
|
||||||
'on': True,
|
"on": True,
|
||||||
'1': True,
|
"1": True,
|
||||||
'n': False,
|
"n": False,
|
||||||
'no': False,
|
"no": False,
|
||||||
'f': False,
|
"f": False,
|
||||||
'false': False,
|
"false": False,
|
||||||
'off': False,
|
"off": False,
|
||||||
'0': False
|
"0": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def strtobool(value):
|
def strtobool(value):
|
||||||
try:
|
try:
|
||||||
return _MAP[str(value).lower()]
|
return _MAP[str(value).lower()]
|
||||||
except KeyError:
|
except KeyError as error:
|
||||||
raise ValueError('"{}" is not a valid bool value'.format(value))
|
raise ValueError('"{}" is not a valid bool value'.format(value)) from error
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"license": "AGPL-3",
|
"license": "AGPL-3",
|
||||||
"category": "category",
|
"category": "category",
|
||||||
"depends": ["base", "web"],
|
"depends": ["base", "web"],
|
||||||
"website": "http://www.camptocamp.com",
|
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
||||||
"data": [],
|
"data": [],
|
||||||
"installable": False,
|
"installable": False,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
|
|
||||||
from . import ir_http
|
from . import ir_http
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from odoo import models
|
|||||||
from odoo.http import request as http_request
|
from odoo.http import request as http_request
|
||||||
from odoo.tools.config import config
|
from odoo.tools.config import config
|
||||||
|
|
||||||
|
|
||||||
_logger = logging.getLogger("monitoring.http.requests")
|
_logger = logging.getLogger("monitoring.http.requests")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"web",
|
"web",
|
||||||
"server_environment",
|
"server_environment",
|
||||||
],
|
],
|
||||||
"website": "http://www.camptocamp.com",
|
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
||||||
"data": [],
|
"data": [],
|
||||||
"external_dependencies": {
|
"external_dependencies": {
|
||||||
"python": ["prometheus_client"],
|
"python": ["prometheus_client"],
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
# Copyright 2016-2021 Camptocamp SA
|
# Copyright 2016-2021 Camptocamp SA
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||||
|
|
||||||
from odoo.http import Controller, route
|
|
||||||
from prometheus_client import generate_latest
|
from prometheus_client import generate_latest
|
||||||
|
|
||||||
|
from odoo.http import Controller, route
|
||||||
|
|
||||||
|
|
||||||
class PrometheusController(Controller):
|
class PrometheusController(Controller):
|
||||||
@route('/metrics', auth='public')
|
@route("/metrics", auth="public")
|
||||||
def metrics(self):
|
def metrics(self):
|
||||||
return generate_latest()
|
return generate_latest()
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# Copyright 2016-2021 Camptocamp SA
|
# Copyright 2016-2021 Camptocamp SA
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||||
|
|
||||||
|
from prometheus_client import Counter, Summary
|
||||||
|
|
||||||
from odoo import models
|
from odoo import models
|
||||||
from odoo.http import request
|
from odoo.http import request
|
||||||
from prometheus_client import Summary, Counter
|
|
||||||
|
|
||||||
|
|
||||||
REQUEST_TIME = Summary(
|
REQUEST_TIME = Summary(
|
||||||
"request_latency_sec", "Request response time in sec", ["query_type"]
|
"request_latency_sec", "Request response time in sec", ["query_type"]
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"web",
|
"web",
|
||||||
"server_environment",
|
"server_environment",
|
||||||
],
|
],
|
||||||
"website": "http://www.camptocamp.com",
|
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
||||||
"data": [],
|
"data": [],
|
||||||
"external_dependencies": {
|
"external_dependencies": {
|
||||||
"python": ["statsd"],
|
"python": ["statsd"],
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
|
|
||||||
from . import ir_http
|
from . import ir_http
|
||||||
|
|||||||
@@ -4,38 +4,46 @@
|
|||||||
from odoo import models
|
from odoo import models
|
||||||
from odoo.http import request
|
from odoo.http import request
|
||||||
|
|
||||||
from ..statsd_client import statsd, customer, environment
|
from ..statsd_client import customer, environment, statsd
|
||||||
|
|
||||||
|
|
||||||
class IrHttp(models.AbstractModel):
|
class IrHttp(models.AbstractModel):
|
||||||
_inherit = 'ir.http'
|
_inherit = "ir.http"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _dispatch(cls, endpoint):
|
def _dispatch(cls, endpoint):
|
||||||
if not statsd:
|
if not statsd:
|
||||||
return super()._dispatch(endpoint)
|
return super()._dispatch(endpoint)
|
||||||
|
|
||||||
path_info = request.httprequest.environ.get('PATH_INFO')
|
path_info = request.httprequest.environ.get("PATH_INFO")
|
||||||
if path_info.startswith('/longpolling/'):
|
if path_info.startswith("/longpolling/"):
|
||||||
return super()._dispatch(endpoint)
|
return super()._dispatch(endpoint)
|
||||||
|
|
||||||
parts = ['http', ]
|
parts = [
|
||||||
if path_info.startswith('/web/dataset/call_button'):
|
"http",
|
||||||
parts += ['button',
|
]
|
||||||
customer, environment,
|
if path_info.startswith("/web/dataset/call_button"):
|
||||||
request.params['model'].replace('.', '_'),
|
parts += [
|
||||||
request.params['method'],
|
"button",
|
||||||
]
|
customer,
|
||||||
elif path_info.startswith('/web/dataset/exec_workflow'):
|
environment,
|
||||||
parts += ['workflow',
|
request.params["model"].replace(".", "_"),
|
||||||
customer, environment,
|
request.params["method"],
|
||||||
request.params['model'].replace('.', '_'),
|
]
|
||||||
request.params['signal'],
|
elif path_info.startswith("/web/dataset/exec_workflow"):
|
||||||
]
|
parts += [
|
||||||
|
"workflow",
|
||||||
|
customer,
|
||||||
|
environment,
|
||||||
|
request.params["model"].replace(".", "_"),
|
||||||
|
request.params["signal"],
|
||||||
|
]
|
||||||
else:
|
else:
|
||||||
parts += ['request',
|
parts += [
|
||||||
customer, environment,
|
"request",
|
||||||
]
|
customer,
|
||||||
|
environment,
|
||||||
|
]
|
||||||
|
|
||||||
with statsd.timer('.'.join(parts)):
|
with statsd.timer(".".join(parts)):
|
||||||
return super()._dispatch(endpoint)
|
return super()._dispatch(endpoint)
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
_MAP = {
|
_MAP = {
|
||||||
'y': True,
|
"y": True,
|
||||||
'yes': True,
|
"yes": True,
|
||||||
't': True,
|
"t": True,
|
||||||
'true': True,
|
"true": True,
|
||||||
'on': True,
|
"on": True,
|
||||||
'1': True,
|
"1": True,
|
||||||
'n': False,
|
"n": False,
|
||||||
'no': False,
|
"no": False,
|
||||||
'f': False,
|
"f": False,
|
||||||
'false': False,
|
"false": False,
|
||||||
'off': False,
|
"off": False,
|
||||||
'0': False
|
"0": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"license": "AGPL-3",
|
"license": "AGPL-3",
|
||||||
"category": "category",
|
"category": "category",
|
||||||
"depends": ["base", "web"],
|
"depends": ["base", "web"],
|
||||||
"website": "http://www.camptocamp.com",
|
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
||||||
"data": [],
|
"data": [],
|
||||||
"installable": True,
|
"installable": True,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
# Copyright 2016-2019 Camptocamp SA
|
# Copyright 2016-2019 Camptocamp SA
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||||
|
|
||||||
import logging
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
import werkzeug
|
import werkzeug
|
||||||
|
|
||||||
from odoo import http
|
from odoo import http
|
||||||
|
|
||||||
from odoo.addons.web.controllers.main import ensure_db
|
from odoo.addons.web.controllers.main import ensure_db
|
||||||
|
|
||||||
|
|
||||||
class HealthCheckFilter(logging.Filter):
|
class HealthCheckFilter(logging.Filter):
|
||||||
|
def __init__(self, path, name=""):
|
||||||
def __init__(self, path, name=''):
|
|
||||||
super().__init__(name)
|
super().__init__(name)
|
||||||
self.path = path
|
self.path = path
|
||||||
|
|
||||||
@@ -20,20 +20,19 @@ class HealthCheckFilter(logging.Filter):
|
|||||||
return self.path not in record.getMessage()
|
return self.path not in record.getMessage()
|
||||||
|
|
||||||
|
|
||||||
logging.getLogger('werkzeug').addFilter(
|
logging.getLogger("werkzeug").addFilter(
|
||||||
HealthCheckFilter('GET /monitoring/status HTTP')
|
HealthCheckFilter("GET /monitoring/status HTTP")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Monitoring(http.Controller):
|
class Monitoring(http.Controller):
|
||||||
|
@http.route("/monitoring/status", type="http", auth="none")
|
||||||
@http.route('/monitoring/status', type='http', auth='none')
|
|
||||||
def status(self):
|
def status(self):
|
||||||
ensure_db()
|
ensure_db()
|
||||||
# TODO: add 'sub-systems' status and infos:
|
# TODO: add 'sub-systems' status and infos:
|
||||||
# queue job, cron, database, ...
|
# queue job, cron, database, ...
|
||||||
headers = {'Content-Type': 'application/json'}
|
headers = {"Content-Type": "application/json"}
|
||||||
info = {'status': 1}
|
info = {"status": 1}
|
||||||
session = http.request.session
|
session = http.request.session
|
||||||
# We set a custom expiration of 1 second for this request, as we do a
|
# We set a custom expiration of 1 second for this request, as we do a
|
||||||
# lot of health checks, we don't want those anonymous sessions to be
|
# lot of health checks, we don't want those anonymous sessions to be
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
|
|
||||||
from . import http
|
from . import http
|
||||||
from . import session
|
from . import session
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"external_dependencies": {
|
"external_dependencies": {
|
||||||
"python": ["redis"],
|
"python": ["redis"],
|
||||||
},
|
},
|
||||||
"website": "http://www.camptocamp.com",
|
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
||||||
"data": [],
|
"data": [],
|
||||||
"installable": True,
|
"installable": True,
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-9
@@ -4,13 +4,12 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from .strtobool import strtobool
|
|
||||||
|
|
||||||
from odoo import http
|
from odoo import http
|
||||||
from odoo.tools import config
|
from odoo.tools import config
|
||||||
from odoo.tools.func import lazy_property
|
from odoo.tools.func import lazy_property
|
||||||
|
|
||||||
from .session import RedisSessionStore
|
from .session import RedisSessionStore
|
||||||
|
from .strtobool import strtobool
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -52,10 +51,13 @@ def session_store(self):
|
|||||||
redis_client = redis.from_url(url)
|
redis_client = redis.from_url(url)
|
||||||
else:
|
else:
|
||||||
redis_client = redis.Redis(host=host, port=port, password=password)
|
redis_client = redis.Redis(host=host, port=port, password=password)
|
||||||
return RedisSessionStore(redis=redis_client, prefix=prefix,
|
return RedisSessionStore(
|
||||||
expiration=expiration,
|
redis=redis_client,
|
||||||
anon_expiration=anon_expiration,
|
prefix=prefix,
|
||||||
session_class=http.Session)
|
expiration=expiration,
|
||||||
|
anon_expiration=anon_expiration,
|
||||||
|
session_class=http.Session,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def purge_fs_sessions(path):
|
def purge_fs_sessions(path):
|
||||||
@@ -64,7 +66,7 @@ def purge_fs_sessions(path):
|
|||||||
try:
|
try:
|
||||||
os.unlink(path)
|
os.unlink(path)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
_logger.warning("OS Error during purge of redis sessions.")
|
||||||
|
|
||||||
|
|
||||||
if is_true(os.environ.get("ODOO_SESSION_REDIS")):
|
if is_true(os.environ.get("ODOO_SESSION_REDIS")):
|
||||||
@@ -77,8 +79,12 @@ if is_true(os.environ.get("ODOO_SESSION_REDIS")):
|
|||||||
sentinel_port,
|
sentinel_port,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
_logger.debug("HTTP sessions stored in Redis with prefix '%s' on "
|
_logger.debug(
|
||||||
"%s:%s", prefix or '', host, port)
|
"HTTP sessions stored in Redis with prefix '%s' on " "%s:%s",
|
||||||
|
prefix or "",
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
)
|
||||||
http.Application.session_store = session_store
|
http.Application.session_store = session_store
|
||||||
# clean the existing sessions on the file system
|
# clean the existing sessions on the file system
|
||||||
purge_fs_sessions(config.session_dir)
|
purge_fs_sessions(config.session_dir)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
|
|
||||||
import dateutil
|
import dateutil
|
||||||
|
|||||||
+41
-29
@@ -18,10 +18,16 @@ _logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class RedisSessionStore(SessionStore):
|
class RedisSessionStore(SessionStore):
|
||||||
""" SessionStore that saves session to redis """
|
"""SessionStore that saves session to redis"""
|
||||||
|
|
||||||
def __init__(self, redis, session_class=None,
|
def __init__(
|
||||||
prefix='', expiration=None, anon_expiration=None):
|
self,
|
||||||
|
redis,
|
||||||
|
session_class=None,
|
||||||
|
prefix="",
|
||||||
|
expiration=None,
|
||||||
|
anon_expiration=None,
|
||||||
|
):
|
||||||
super().__init__(session_class=session_class)
|
super().__init__(session_class=session_class)
|
||||||
self.redis = redis
|
self.redis = redis
|
||||||
if expiration is None:
|
if expiration is None:
|
||||||
@@ -32,14 +38,12 @@ class RedisSessionStore(SessionStore):
|
|||||||
self.anon_expiration = DEFAULT_SESSION_TIMEOUT_ANONYMOUS
|
self.anon_expiration = DEFAULT_SESSION_TIMEOUT_ANONYMOUS
|
||||||
else:
|
else:
|
||||||
self.anon_expiration = anon_expiration
|
self.anon_expiration = anon_expiration
|
||||||
self.prefix = 'session:'
|
self.prefix = "session:"
|
||||||
if prefix:
|
if prefix:
|
||||||
self.prefix = '%s:%s:' % (
|
self.prefix = "%s:%s:" % (self.prefix, prefix)
|
||||||
self.prefix, prefix
|
|
||||||
)
|
|
||||||
|
|
||||||
def build_key(self, sid):
|
def build_key(self, sid):
|
||||||
return '%s%s' % (self.prefix, sid)
|
return "%s%s" % (self.prefix, sid)
|
||||||
|
|
||||||
def save(self, session):
|
def save(self, session):
|
||||||
key = self.build_key(session.sid)
|
key = self.build_key(session.sid)
|
||||||
@@ -52,51 +56,59 @@ class RedisSessionStore(SessionStore):
|
|||||||
expiration = session.expiration or self.anon_expiration
|
expiration = session.expiration or self.anon_expiration
|
||||||
if _logger.isEnabledFor(logging.DEBUG):
|
if _logger.isEnabledFor(logging.DEBUG):
|
||||||
if session.uid:
|
if session.uid:
|
||||||
user_msg = "user '%s' (id: %s)" % (
|
user_msg = "user '%s' (id: %s)" % (session.login, session.uid)
|
||||||
session.login, session.uid)
|
|
||||||
else:
|
else:
|
||||||
user_msg = "anonymous user"
|
user_msg = "anonymous user"
|
||||||
_logger.debug("saving session with key '%s' and "
|
_logger.debug(
|
||||||
"expiration of %s seconds for %s",
|
"saving session with key '%s' and " "expiration of %s seconds for %s",
|
||||||
key, expiration, user_msg)
|
key,
|
||||||
|
expiration,
|
||||||
|
user_msg,
|
||||||
|
)
|
||||||
|
|
||||||
data = json.dumps(
|
data = json.dumps(dict(session), cls=json_encoding.SessionEncoder).encode(
|
||||||
dict(session), cls=json_encoding.SessionEncoder
|
"utf-8"
|
||||||
).encode('utf-8')
|
)
|
||||||
if self.redis.set(key, data):
|
if self.redis.set(key, data):
|
||||||
return self.redis.expire(key, expiration)
|
return self.redis.expire(key, expiration)
|
||||||
|
|
||||||
def delete(self, session):
|
def delete(self, session):
|
||||||
key = self.build_key(session.sid)
|
key = self.build_key(session.sid)
|
||||||
_logger.debug('deleting session with key %s', key)
|
_logger.debug("deleting session with key %s", key)
|
||||||
return self.redis.delete(key)
|
return self.redis.delete(key)
|
||||||
|
|
||||||
def get(self, sid):
|
def get(self, sid):
|
||||||
if not self.is_valid_key(sid):
|
if not self.is_valid_key(sid):
|
||||||
_logger.debug("session with invalid sid '%s' has been asked, "
|
_logger.debug(
|
||||||
"returning a new one", sid)
|
"session with invalid sid '%s' has been asked, " "returning a new one",
|
||||||
|
sid,
|
||||||
|
)
|
||||||
return self.new()
|
return self.new()
|
||||||
|
|
||||||
key = self.build_key(sid)
|
key = self.build_key(sid)
|
||||||
saved = self.redis.get(key)
|
saved = self.redis.get(key)
|
||||||
if not saved:
|
if not saved:
|
||||||
_logger.debug("session with non-existent key '%s' has been asked, "
|
_logger.debug(
|
||||||
"returning a new one", key)
|
"session with non-existent key '%s' has been asked, "
|
||||||
|
"returning a new one",
|
||||||
|
key,
|
||||||
|
)
|
||||||
return self.new()
|
return self.new()
|
||||||
try:
|
try:
|
||||||
data = json.loads(
|
data = json.loads(saved.decode("utf-8"), cls=json_encoding.SessionDecoder)
|
||||||
saved.decode('utf-8'), cls=json_encoding.SessionDecoder
|
|
||||||
)
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
_logger.debug("session for key '%s' has been asked but its json "
|
_logger.debug(
|
||||||
"content could not be read, it has been reset", key)
|
"session for key '%s' has been asked but its json "
|
||||||
|
"content could not be read, it has been reset",
|
||||||
|
key,
|
||||||
|
)
|
||||||
data = {}
|
data = {}
|
||||||
return self.session_class(data, sid, False)
|
return self.session_class(data, sid, False)
|
||||||
|
|
||||||
def list(self):
|
def list(self):
|
||||||
keys = self.redis.keys('%s*' % self.prefix)
|
keys = self.redis.keys("%s*" % self.prefix)
|
||||||
_logger.debug("a listing redis keys has been called")
|
_logger.debug("a listing redis keys has been called")
|
||||||
return [key[len(self.prefix):] for key in keys]
|
return [key[len(self.prefix) :] for key in keys]
|
||||||
|
|
||||||
def rotate(self, session, env):
|
def rotate(self, session, env):
|
||||||
self.delete(session)
|
self.delete(session)
|
||||||
@@ -106,7 +118,7 @@ class RedisSessionStore(SessionStore):
|
|||||||
self.save(session)
|
self.save(session)
|
||||||
|
|
||||||
def vacuum(self):
|
def vacuum(self):
|
||||||
""" Do not garbage collect the sessions
|
"""Do not garbage collect the sessions
|
||||||
|
|
||||||
Redis keys are automatically cleaned at the end of their
|
Redis keys are automatically cleaned at the end of their
|
||||||
expiration.
|
expiration.
|
||||||
|
|||||||
+14
-14
@@ -1,21 +1,21 @@
|
|||||||
_MAP = {
|
_MAP = {
|
||||||
'y': True,
|
"y": True,
|
||||||
'yes': True,
|
"yes": True,
|
||||||
't': True,
|
"t": True,
|
||||||
'true': True,
|
"true": True,
|
||||||
'on': True,
|
"on": True,
|
||||||
'1': True,
|
"1": True,
|
||||||
'n': False,
|
"n": False,
|
||||||
'no': False,
|
"no": False,
|
||||||
'f': False,
|
"f": False,
|
||||||
'false': False,
|
"false": False,
|
||||||
'off': False,
|
"off": False,
|
||||||
'0': False
|
"0": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def strtobool(value):
|
def strtobool(value):
|
||||||
try:
|
try:
|
||||||
return _MAP[str(value).lower()]
|
return _MAP[str(value).lower()]
|
||||||
except KeyError:
|
except KeyError as error:
|
||||||
raise ValueError('"{}" is not a valid bool value'.format(value))
|
raise ValueError('"{}" is not a valid bool value'.format(value)) from error
|
||||||
|
|||||||
@@ -6,10 +6,9 @@
|
|||||||
"version": "12.0.1.0.0",
|
"version": "12.0.1.0.0",
|
||||||
"category": "Tests",
|
"category": "Tests",
|
||||||
"author": "Camptocamp,Odoo Community Association (OCA)",
|
"author": "Camptocamp,Odoo Community Association (OCA)",
|
||||||
|
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
||||||
"license": "AGPL-3",
|
"license": "AGPL-3",
|
||||||
"depends": [
|
"depends": ["base_fileurl_field"],
|
||||||
"base_fileurl_field"
|
|
||||||
],
|
|
||||||
"data": [
|
"data": [
|
||||||
"views/res_partner.xml",
|
"views/res_partner.xml",
|
||||||
"views/res_users.xml",
|
"views/res_users.xml",
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
This is a simple text file.
|
This is a simple text file.
|
||||||
|
|||||||
@@ -1,44 +1,46 @@
|
|||||||
# Copyright 2019 Camptocamp SA
|
# Copyright 2019 Camptocamp SA
|
||||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||||
from odoo import models, fields, api, _
|
from odoo import _, api, fields, models
|
||||||
from odoo.exceptions import ValidationError
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
class ResPartner(models.Model):
|
class ResPartner(models.Model):
|
||||||
|
|
||||||
_inherit = 'res.partner'
|
_inherit = "res.partner"
|
||||||
|
|
||||||
name = fields.Char()
|
name = fields.Char()
|
||||||
url_file = fields.FileURL(
|
url_file = fields.FileURL(
|
||||||
storage_location='s3',
|
storage_location="s3", filename="url_file_fname", storage_path="partner"
|
||||||
filename='url_file_fname',
|
|
||||||
storage_path='partner'
|
|
||||||
)
|
)
|
||||||
url_file_fname = fields.Char()
|
url_file_fname = fields.Char()
|
||||||
|
|
||||||
url_image = fields.FileURL(
|
url_image = fields.FileURL(
|
||||||
storage_location='s3',
|
storage_location="s3",
|
||||||
filename='url_image_fname',
|
filename="url_image_fname",
|
||||||
storage_path='partner_image',
|
storage_path="partner_image",
|
||||||
)
|
)
|
||||||
url_image_fname = fields.Char()
|
url_image_fname = fields.Char()
|
||||||
|
|
||||||
@api.constrains('url_file', 'url_file_fname')
|
@api.constrains("url_file", "url_file_fname")
|
||||||
def _check_url_file_fname(self):
|
def _check_url_file_fname(self):
|
||||||
rec = self.search([('url_file_fname', '=', self.url_file_fname)])
|
rec = self.search([("url_file_fname", "=", self.url_file_fname)])
|
||||||
if len(rec) > 1:
|
if len(rec) > 1:
|
||||||
raise ValidationError(_(
|
raise ValidationError(
|
||||||
"This file name is already used on an existing record. "
|
_(
|
||||||
"Please use another file name or delete the url_file on :\n"
|
"This file name is already used on an existing record. "
|
||||||
"Model: %s Id: %s" % (self._name, rec.id)
|
"Please use another file name or delete the url_file on :\n"
|
||||||
))
|
"Model: %s Id: %s" % (self._name, rec.id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@api.constrains('url_image', 'url_image_fname')
|
@api.constrains("url_image", "url_image_fname")
|
||||||
def _check_url_image_fname(self):
|
def _check_url_image_fname(self):
|
||||||
rec = self.search([('url_image_fname', '=', self.url_image_fname)])
|
rec = self.search([("url_image_fname", "=", self.url_image_fname)])
|
||||||
if len(rec) > 1:
|
if len(rec) > 1:
|
||||||
raise ValidationError(_(
|
raise ValidationError(
|
||||||
"This file name is already used on an existing record. "
|
_(
|
||||||
"Please use another file name or delete the url_image on :\n"
|
"This file name is already used on an existing record. "
|
||||||
"Model: %s Id: %s" % (self._name, rec.id)
|
"Please use another file name or delete the url_image on :\n"
|
||||||
))
|
"Model: %s Id: %s" % (self._name, rec.id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# Copyright 2019 Camptocamp SA
|
# Copyright 2019 Camptocamp SA
|
||||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||||
from odoo import models, fields
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
class ResUsers(models.Model):
|
class ResUsers(models.Model):
|
||||||
|
|
||||||
_inherit = 'res.users'
|
_inherit = "res.users"
|
||||||
|
|
||||||
partner_url_file = fields.FileURL(related='partner_id.url_file')
|
partner_url_file = fields.FileURL(related="partner_id.url_file")
|
||||||
partner_url_file_fname = fields.Char(related='partner_id.url_file_fname')
|
partner_url_file_fname = fields.Char(related="partner_id.url_file_fname")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from odoo import models, api
|
from odoo import api, models
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -14,23 +14,23 @@ class IrAttachment(models.Model):
|
|||||||
_inherit = "ir.attachment"
|
_inherit = "ir.attachment"
|
||||||
|
|
||||||
def _get_stores(self):
|
def _get_stores(self):
|
||||||
l = ['s3']
|
l = ["s3"]
|
||||||
l += super(IrAttachment, self)._get_stores()
|
l += super(IrAttachment, self)._get_stores()
|
||||||
return l
|
return l
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _store_file_read(self, fname, bin_size=False):
|
def _store_file_read(self, fname, bin_size=False):
|
||||||
if fname.startswith('s3://'):
|
if fname.startswith("s3://"):
|
||||||
return FAKE_S3_BUCKET.get(fname)
|
return FAKE_S3_BUCKET.get(fname)
|
||||||
else:
|
else:
|
||||||
return super(IrAttachment, self)._store_file_read(fname, bin_size)
|
return super(IrAttachment, self)._store_file_read(fname, bin_size)
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _store_file_write(self, key, bin_data):
|
def _store_file_write(self, key, bin_data):
|
||||||
location = self.env.context.get('storage_location') or self._storage()
|
location = self.env.context.get("storage_location") or self._storage()
|
||||||
if location == 's3':
|
if location == "s3":
|
||||||
FAKE_S3_BUCKET[key] = bin_data
|
FAKE_S3_BUCKET[key] = bin_data
|
||||||
filename = 's3://fake_bucket/%s' % key
|
filename = "s3://fake_bucket/%s" % key
|
||||||
else:
|
else:
|
||||||
_super = super(IrAttachment, self)
|
_super = super(IrAttachment, self)
|
||||||
filename = _super._store_file_write(key, bin_data)
|
filename = _super._store_file_write(key, bin_data)
|
||||||
@@ -38,7 +38,7 @@ class IrAttachment(models.Model):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _store_file_delete(self, fname):
|
def _store_file_delete(self, fname):
|
||||||
if fname.startswith('s3://'):
|
if fname.startswith("s3://"):
|
||||||
FAKE_S3_BUCKET.pop(fname)
|
FAKE_S3_BUCKET.pop(fname)
|
||||||
else:
|
else:
|
||||||
super(IrAttachment, self)._store_file_delete(fname)
|
super(IrAttachment, self)._store_file_delete(fname)
|
||||||
|
|||||||
@@ -2,38 +2,41 @@
|
|||||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
from odoo.tests import TransactionCase
|
|
||||||
from odoo.modules.module import get_module_resource
|
|
||||||
from odoo.exceptions import ValidationError
|
from odoo.exceptions import ValidationError
|
||||||
|
from odoo.modules.module import get_module_resource
|
||||||
|
from odoo.tests import TransactionCase
|
||||||
|
|
||||||
|
|
||||||
class TestFileUrlFields(TransactionCase):
|
class TestFileUrlFields(TransactionCase):
|
||||||
|
|
||||||
def test_fileurl_fields(self):
|
def test_fileurl_fields(self):
|
||||||
file_path = get_module_resource('test_base_fileurl_field', 'data',
|
file_path = get_module_resource("test_base_fileurl_field", "data", "sample.txt")
|
||||||
'sample.txt')
|
image_path = get_module_resource(
|
||||||
image_path = get_module_resource('test_base_fileurl_field', 'data',
|
"test_base_fileurl_field", "data", "pattern.png"
|
||||||
'pattern.png')
|
)
|
||||||
partner = self.env.ref('base.main_partner')
|
partner = self.env.ref("base.main_partner")
|
||||||
with open(file_path, 'rb') as f:
|
with open(file_path, "rb") as f:
|
||||||
with open(image_path, 'rb') as i:
|
with open(image_path, "rb") as i:
|
||||||
partner.write({
|
partner.write(
|
||||||
'url_file': base64.b64encode(f.read()),
|
{
|
||||||
'url_file_fname': 'sample.txt',
|
"url_file": base64.b64encode(f.read()),
|
||||||
'url_image': base64.b64encode(i.read()),
|
"url_file_fname": "sample.txt",
|
||||||
'url_image_fname': 'pattern.png',
|
"url_image": base64.b64encode(i.read()),
|
||||||
})
|
"url_image_fname": "pattern.png",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
with open(file_path, 'rb') as f:
|
with open(file_path, "rb") as f:
|
||||||
self.assertEqual(base64.decodebytes(partner.url_file), f.read())
|
self.assertEqual(base64.decodebytes(partner.url_file), f.read())
|
||||||
|
|
||||||
with open(image_path, 'rb') as i:
|
with open(image_path, "rb") as i:
|
||||||
self.assertEqual(base64.decodebytes(partner.url_image), i.read())
|
self.assertEqual(base64.decodebytes(partner.url_image), i.read())
|
||||||
|
|
||||||
partner2 = self.env.ref('base.partner_admin')
|
partner2 = self.env.ref("base.partner_admin")
|
||||||
with open(file_path, 'rb') as f:
|
with open(file_path, "rb") as f:
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
partner2.write({
|
partner2.write(
|
||||||
'url_file': base64.b64encode(f.read()),
|
{
|
||||||
'url_file_fname': 'sample.txt',
|
"url_file": base64.b64encode(f.read()),
|
||||||
})
|
"url_file_fname": "sample.txt",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<record id="view_partner_form_inherit" model="ir.ui.view">
|
<record id="view_partner_form_inherit" model="ir.ui.view">
|
||||||
<field name="name">res.partner.form.inherit</field>
|
<field name="name">res.partner.form.inherit</field>
|
||||||
@@ -9,11 +9,15 @@
|
|||||||
<page name="fileurl_test" string="FileURL Test fields">
|
<page name="fileurl_test" string="FileURL Test fields">
|
||||||
<group string="Default widget">
|
<group string="Default widget">
|
||||||
<field name="url_file" filename="url_file_fname" />
|
<field name="url_file" filename="url_file_fname" />
|
||||||
<field name="url_file_fname" invisible="1"/>
|
<field name="url_file_fname" invisible="1" />
|
||||||
</group>
|
</group>
|
||||||
<group string="Image widget">
|
<group string="Image widget">
|
||||||
<field name="url_image" widget="image" filename="url_image_fname" />
|
<field
|
||||||
<field name="url_image_fname" invisible="1"/>
|
name="url_image"
|
||||||
|
widget="image"
|
||||||
|
filename="url_image_fname"
|
||||||
|
/>
|
||||||
|
<field name="url_image_fname" invisible="1" />
|
||||||
</group>
|
</group>
|
||||||
</page>
|
</page>
|
||||||
</xpath>
|
</xpath>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<record id="view_users_form_inherit" model="ir.ui.view">
|
<record id="view_users_form_inherit" model="ir.ui.view">
|
||||||
<field name="name">res.users.form.inherit</field>
|
<field name="name">res.users.form.inherit</field>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<page name="fileurl_test" string="FileURL Test fields">
|
<page name="fileurl_test" string="FileURL Test fields">
|
||||||
<group string="Default widget">
|
<group string="Default widget">
|
||||||
<field name="url_file" filename="url_file_fname" />
|
<field name="url_file" filename="url_file_fname" />
|
||||||
<field name="url_file_fname" invisible="1"/>
|
<field name="url_file_fname" invisible="1" />
|
||||||
</group>
|
</group>
|
||||||
</page>
|
</page>
|
||||||
</xpath>
|
</xpath>
|
||||||
|
|||||||
Reference in New Issue
Block a user