Change CI to GitHub actions

Use copier template from oca/oca-addons-repo-template

Apply linting
This commit is contained in:
Yannick Payot
2023-05-24 15:05:39 +02:00
parent 05d111f7c1
commit d17d229b13
86 changed files with 1441 additions and 634 deletions
+25
View File
@@ -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: []
+20
View File
@@ -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
View File
@@ -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
+12
View File
@@ -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
+35
View File
@@ -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
+69
View File
@@ -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.
+82
View File
@@ -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
View File
@@ -1,6 +1,8 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
/.venv
/.pytest_cache
# C extensions
*.so
@@ -13,8 +15,6 @@ build/
develop-eggs/
dist/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
@@ -22,6 +22,7 @@ var/
*.egg-info/
.installed.cfg
*.egg
*.eggs
# Installer logs
pip-log.txt
@@ -41,6 +42,19 @@ coverage.xml
# Pycharm
.idea
# Eclipse
.settings
# Visual Studio cache/options directory
.vs/
.vscode
# OSX Files
.DS_Store
# Django stuff:
*.log
# Mr Developer
.mr.developer.cfg
.project
@@ -50,8 +64,11 @@ coverage.xml
.ropeproject
# Sphinx documentation
connector/doc/_build/
docs/_build/
# Backup files
*~
*.swp
# OCA rules
!static/lib/
+13
View File
@@ -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
+140
View File
@@ -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
+8
View File
@@ -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"
+123
View File
@@ -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
+98
View File
@@ -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
View File
@@ -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
+6 -6
View File
@@ -1,7 +1,7 @@
GNU AFFERO GENERAL PUBLIC LICENSE
GNU AFFERO GENERAL PUBLIC LICENSE
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
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>
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
by the Free Software Foundation, either version 3 of the License, or
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
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.
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.
@@ -658,4 +658,4 @@ specific requirements.
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.
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/>.
+31 -1
View File
@@ -1,4 +1,11 @@
[![Build Status](https://travis-ci.com/camptocamp/odoo-cloud-platform.svg?token=Lpp9PcS5on9AGbp76WKB&branch=12.0)](https://travis-ci.com/camptocamp/odoo-cloud-platform)
<!-- /!\ Non OCA Context : Set here the badge of your runbot / runboat instance. -->
[![Pre-commit Status](https://github.com/camptocamp/odoo-cloud-platform/actions/workflows/pre-commit.yml/badge.svg?branch=16.0)](https://github.com/camptocamp/odoo-cloud-platform/actions/workflows/pre-commit.yml?query=branch%3A16.0)
[![Build Status](https://github.com/camptocamp/odoo-cloud-platform/actions/workflows/test.yml/badge.svg?branch=16.0)](https://github.com/camptocamp/odoo-cloud-platform/actions/workflows/test.yml?query=branch%3A16.0)
[![codecov](https://codecov.io/gh/camptocamp/odoo-cloud-platform/branch/16.0/graph/badge.svg)](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
@@ -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
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. -->
+17 -17
View File
@@ -12,13 +12,13 @@ from odoo import _, api, exceptions, models
_logger = logging.getLogger(__name__)
try:
from azure.core.exceptions import HttpResponseError, ResourceExistsError
from azure.storage.blob import (
BlobServiceClient,
generate_account_sas,
ResourceTypes,
AccountSasPermissions,
BlobServiceClient,
ResourceTypes,
generate_account_sas,
)
from azure.core.exceptions import ResourceExistsError, HttpResponseError
except ImportError:
_logger.debug("Cannot 'import azure-storage-blob'.")
@@ -32,9 +32,7 @@ class IrAttachment(models.Model):
_inherit = "ir.attachment"
def _get_stores(self):
l = ["azure"]
l += super(IrAttachment, self)._get_stores()
return l
return ["azure"] + super(IrAttachment, self)._get_stores()
@api.model
def _get_blob_service_client(self):
@@ -88,7 +86,7 @@ class IrAttachment(models.Model):
"Error during the connection to Azure container using the "
"connection string."
)
raise exceptions.UserError(str(error))
raise exceptions.UserError(str(error)) from None
else:
try:
sas_token = generate_account_sas(
@@ -107,15 +105,13 @@ class IrAttachment(models.Model):
"Error during the connection to Azure container using the Shared "
"Access Signature (SAS)"
)
raise exceptions.UserError(str(error))
raise exceptions.UserError(str(error)) from None
return blob_service_client
@api.model
def _get_container_name(self):
"""
Container naming rules:
https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names
"""
# Container naming rules:
# https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names # noqa: B950
running_env = os.environ.get("RUNNING_ENV", "dev")
storage_name = os.environ.get("AZURE_STORAGE_NAME", r"{env}-{db}")
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()
except HttpResponseError as error:
_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
@api.model
@@ -181,13 +177,17 @@ class IrAttachment(models.Model):
try:
blob_client.upload_blob(file, blob_type="BlockBlob")
except ResourceExistsError:
pass
_logger.exception(
"Trying to re create an existing resource %s" % filename
)
except HttpResponseError as error:
# 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(
_("The file could not be stored: %s") % str(error)
)
) from None
else:
_super = super(IrAttachment, self)
filename = _super._store_file_write(key, bin_data)
+1 -1
View File
@@ -60,6 +60,6 @@ try:
from odoo.addons import documents
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
pass
-1
View File
@@ -1,2 +1 @@
from . import models
+1 -1
View File
@@ -13,7 +13,7 @@
"external_dependencies": {
"python": ["boto3"],
},
"website": "https://www.camptocamp.com",
"website": "https://github.com/camptocamp/odoo-cloud-platform",
"data": [],
"installable": False,
}
-1
View File
@@ -1,2 +1 @@
from . import ir_attachment
+48 -61
View File
@@ -2,12 +2,13 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
import io
import logging
import os
import io
from urllib.parse import urlsplit
from odoo import _, api, exceptions, models
from ..s3uri import S3Uri
_logger = logging.getLogger(__name__)
@@ -26,9 +27,7 @@ class IrAttachment(models.Model):
_inherit = "ir.attachment"
def _get_stores(self):
l = ['s3']
l += super()._get_stores()
return l
return ["s3"] + super()._get_stores()
@api.model
def _get_s3_bucket(self, name=None):
@@ -45,42 +44,43 @@ class IrAttachment(models.Model):
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)
if host and not urlsplit(host).scheme:
host = 'https://%s' % host
host = "https://%s" % host
region_name = os.environ.get('AWS_REGION')
access_key = os.environ.get('AWS_ACCESS_KEY_ID')
secret_key = os.environ.get('AWS_SECRET_ACCESS_KEY')
bucket_name = name or os.environ.get('AWS_BUCKETNAME')
region_name = os.environ.get("AWS_REGION")
access_key = os.environ.get("AWS_ACCESS_KEY_ID")
secret_key = os.environ.get("AWS_SECRET_ACCESS_KEY")
bucket_name = name or os.environ.get("AWS_BUCKETNAME")
# replaces {db} by the database name to handle multi-tenancy
bucket_name = bucket_name.format(db=self.env.cr.dbname)
params = {
'aws_access_key_id': access_key,
'aws_secret_access_key': secret_key,
"aws_access_key_id": access_key,
"aws_secret_access_key": secret_key,
}
if host:
params['endpoint_url'] = host
params["endpoint_url"] = host
if region_name:
params['region_name'] = region_name
params["region_name"] = region_name
if not (access_key and secret_key and bucket_name):
msg = _('If you want to read from the %s S3 bucket, the following '
'environment variables must be set:\n'
'* AWS_ACCESS_KEY_ID\n'
'* AWS_SECRET_ACCESS_KEY\n'
'If you want to write in the %s S3 bucket, this variable '
'must be set as well:\n'
'* AWS_BUCKETNAME\n'
'Optionally, the S3 host can be changed with:\n'
'* AWS_HOST\n'
) % (bucket_name, bucket_name)
msg = _(
"If you want to read from the %(bucket_name)s S3 bucket, the following "
"environment variables must be set:\n"
"* AWS_ACCESS_KEY_ID\n"
"* AWS_SECRET_ACCESS_KEY\n"
"If you want to write in the %(bucket_name)s S3 bucket, this variable "
"must be set as well:\n"
"* AWS_BUCKETNAME\n"
"Optionally, the S3 host can be changed with:\n"
"* AWS_HOST\n"
).format(bucket_name=bucket_name)
raise exceptions.UserError(msg)
# try:
s3 = boto3.resource('s3', **params)
s3 = boto3.resource("s3", **params)
bucket = s3.Bucket(bucket_name)
exists = True
try:
@@ -88,13 +88,13 @@ class IrAttachment(models.Model):
except ClientError as e:
# 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.
error_code = e.response['Error']['Code']
if error_code == '404':
error_code = e.response["Error"]["Code"]
if error_code == "404":
exists = False
except EndpointConnectionError as error:
# log verbose error from s3, return short message for user
_logger.exception('Error during connection on S3')
raise exceptions.UserError(str(error))
msg = _logger.exception("Error during connection on S3")
raise exceptions.UserError(str(error)) from None
if not exists:
if not region_name:
@@ -102,14 +102,13 @@ class IrAttachment(models.Model):
else:
bucket = s3.create_bucket(
Bucket=bucket_name,
CreateBucketConfiguration={
'LocationConstraint': region_name
})
CreateBucketConfiguration={"LocationConstraint": region_name},
)
return bucket
@api.model
def _store_file_read(self, fname):
if fname.startswith('s3://'):
if fname.startswith("s3://"):
s3uri = S3Uri(fname)
try:
bucket = self._get_s3_bucket(name=s3uri.bucket())
@@ -117,45 +116,39 @@ class IrAttachment(models.Model):
_logger.exception(
"error reading attachment '%s' from object storage", fname
)
return ''
return ""
try:
key = s3uri.item()
bucket.meta.client.head_object(
Bucket=bucket.name, Key=key
)
bucket.meta.client.head_object(Bucket=bucket.name, Key=key)
with io.BytesIO() as res:
bucket.download_fileobj(key, res)
res.seek(0)
read = res.read()
except ClientError:
read = ''
_logger.info(
"attachment '%s' missing on object storage", fname
)
read = ""
_logger.info("attachment '%s' missing on object storage", fname)
return read
else:
return super()._store_file_read(fname)
@api.model
def _store_file_write(self, key, bin_data):
location = self.env.context.get('storage_location') or self._storage()
if location == 's3':
location = self.env.context.get("storage_location") or self._storage()
if location == "s3":
bucket = self._get_s3_bucket()
obj = bucket.Object(key=key)
with io.BytesIO() as file:
file.write(bin_data)
file.seek(0)
filename = 's3://%s/%s' % (bucket.name, key)
filename = "s3://%s/%s" % (bucket.name, key)
try:
obj.upload_fileobj(file)
except ClientError as error:
# log verbose error from s3, return short message for user
_logger.exception(
'Error during storage of the file %s' % filename
)
_logger.exception("Error during storage of the file %s" % filename)
raise exceptions.UserError(
_('The file could not be stored: %s') % str(error)
)
_("The file could not be stored: %s") % str(error)
) from None
else:
_super = super()
filename = _super._store_file_write(key, bin_data)
@@ -163,28 +156,22 @@ class IrAttachment(models.Model):
@api.model
def _store_file_delete(self, fname):
if fname.startswith('s3://'):
if fname.startswith("s3://"):
s3uri = S3Uri(fname)
bucket_name = s3uri.bucket()
item_name = s3uri.item()
# delete the file only if it is on the current configured bucket
# 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()
obj = bucket.Object(key=item_name)
try:
bucket.meta.client.head_object(
Bucket=bucket.name, Key=item_name
)
bucket.meta.client.head_object(Bucket=bucket.name, Key=item_name)
obj.delete()
_logger.info(
'file %s deleted on the object storage' % (fname,)
)
_logger.info("file %s deleted on the object storage" % (fname,))
except ClientError:
# log verbose error from s3, return short message for
# user
_logger.exception(
'Error during deletion of the file %s' % fname
)
_logger.exception("Error during deletion of the file %s" % fname)
else:
super()._store_file_delete(fname)
return super()._store_file_delete(fname)
+1 -1
View File
@@ -17,7 +17,7 @@
"keystoneauth1",
],
},
"website": "https://www.camptocamp.com",
"website": "https://github.com/camptocamp/odoo-cloud-platform",
"data": [],
"installable": False,
}
+40 -43
View File
@@ -4,17 +4,18 @@
import logging
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__)
try:
import swiftclient
import keystoneauth1
import keystoneauth1.identity
import keystoneauth1.session
import swiftclient
from swiftclient.exceptions import ClientException
except ImportError:
swiftclient = None
@@ -48,8 +49,9 @@ class SwiftSessionStore(object):
def _get_key(self, auth_url, username, password, project_name):
return (auth_url, username, password, project_name)
def get_session(self, auth_url=None, username=None, password=None,
project_name=None):
def get_session(
self, auth_url=None, username=None, password=None, project_name=None
):
key = self._get_key(auth_url, username, password, project_name)
session = self._sessions.get(key)
if not session:
@@ -58,8 +60,8 @@ class SwiftSessionStore(object):
password=password,
project_name=project_name,
auth_url=auth_url,
project_domain_id='default',
user_domain_id='default',
project_domain_id="default",
user_domain_id="default",
)
session = keystoneauth1.session.Session(
auth=auth,
@@ -73,36 +75,36 @@ swift_session_store = SwiftSessionStore()
class IrAttachment(models.Model):
_inherit = 'ir.attachment'
_inherit = "ir.attachment"
def _get_stores(self):
l = ['swift']
l += super()._get_stores()
return l
return ["swift"] + super()._get_stores()
@api.model
def _get_swift_connection(self):
""" Returns a connection object for the Swift object store """
host = os.environ.get('SWIFT_AUTH_URL')
account = os.environ.get('SWIFT_ACCOUNT')
password = os.environ.get('SWIFT_PASSWORD')
project_name = os.environ.get('SWIFT_PROJECT_NAME')
if not project_name and os.environ.get('SWIFT_TENANT_NAME'):
project_name = os.environ['SWIFT_TENANT_NAME']
"""Returns a connection object for the Swift object store"""
host = os.environ.get("SWIFT_AUTH_URL")
account = os.environ.get("SWIFT_ACCOUNT")
password = os.environ.get("SWIFT_PASSWORD")
project_name = os.environ.get("SWIFT_PROJECT_NAME")
if not project_name and os.environ.get("SWIFT_TENANT_NAME"):
project_name = os.environ["SWIFT_TENANT_NAME"]
_logger.warning(
"SWIFT_TENANT_NAME is deprecated and "
"must be replaced by SWIFT_PROJECT_NAME"
)
region = os.environ.get('SWIFT_REGION_NAME')
region = os.environ.get("SWIFT_REGION_NAME")
os_options = {}
if region:
os_options['region_name'] = region
os_options["region_name"] = region
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, "
"SWIFT_TENANT_NAME) properly set?"
))
)
)
try:
session = swift_session_store.get_session(
username=account,
@@ -115,13 +117,13 @@ class IrAttachment(models.Model):
os_options=os_options,
)
except ClientException:
_logger.exception('Error connecting to Swift object store')
raise exceptions.UserError(_('Error on Swift connection'))
_logger.exception("Error connecting to Swift object store")
raise exceptions.UserError(_("Error on Swift connection")) from None
return conn
@api.model
def _store_file_read(self, fname):
if fname.startswith('swift://'):
if fname.startswith("swift://"):
swifturi = SwiftUri(fname)
try:
conn = self._get_swift_connection()
@@ -129,31 +131,27 @@ class IrAttachment(models.Model):
_logger.exception(
"error reading attachment '%s' from object storage", fname
)
return ''
return ""
try:
resp, read = conn.get_object(
swifturi.container(),
swifturi.item()
)
resp, read = conn.get_object(swifturi.container(), swifturi.item())
except ClientException:
read = ''
_logger.exception(
'Error reading object from Swift object store')
read = ""
_logger.exception("Error reading object from Swift object store")
return read
else:
return super()._store_file_read(fname)
def _store_file_write(self, key, bin_data):
if self._storage() == 'swift':
container = os.environ.get('SWIFT_WRITE_CONTAINER')
if self._storage() == "swift":
container = os.environ.get("SWIFT_WRITE_CONTAINER")
conn = self._get_swift_connection()
conn.put_container(container)
filename = 'swift://{}/{}'.format(container, key)
filename = "swift://{}/{}".format(container, key)
try:
conn.put_object(container, key, bin_data)
except ClientException:
_logger.exception('Error writing to Swift object store')
raise exceptions.UserError(_('Error writing to Swift'))
_logger.exception("Error writing to Swift object store")
raise exceptions.UserError(_("Error writing to Swift")) from None
else:
_super = super()
filename = _super._store_file_write(key, bin_data)
@@ -161,19 +159,18 @@ class IrAttachment(models.Model):
@api.model
def _store_file_delete(self, fname):
if fname.startswith('swift://'):
if fname.startswith("swift://"):
swifturi = SwiftUri(fname)
container = swifturi.container()
# delete the file only if it is on the current configured bucket
# 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()
try:
conn.delete_object(container, swifturi.item())
except ClientException:
_logger.exception(
_('Error deleting an object on the Swift store'))
_logger.exception(_("Error deleting an object on the Swift store"))
# we ignore the error, file will stay on the object
# storage but won't disrupt the process
else:
super()._file_delete_from_store(fname)
return super()._file_delete_from_store(fname)
+1 -2
View File
@@ -6,8 +6,7 @@ import re
class SwiftUri(object):
_url_re = re.compile("^swift:///*([^/]*)/?(.*)",
re.IGNORECASE | re.UNICODE)
_url_re = re.compile("^swift:///*([^/]*)/?(.*)", re.IGNORECASE | re.UNICODE)
def __init__(self, uri):
match = self._url_re.match(uri)
-1
View File
@@ -1,2 +1 @@
from . import test_mock_swift_api
+56 -56
View File
@@ -2,30 +2,28 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
import base64
import mock
import os
import keystoneauth1
import mock
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.swift_uri import SwiftUri
from odoo.addons.base.tests.test_ir_attachment import TestIrAttachment
class TestAttachmentSwift(TestIrAttachment):
def setup(self):
super().setUp()
self.env['ir.config_parameter'].set_param('ir_attachment.location',
'swift')
res = super().setUp()
self.env["ir.config_parameter"].set_param("ir_attachment.location", "swift")
return res
def test_session_store_get_session(self):
auth_url = 'auth_url'
username = 'username'
password = 'password'
project_name = 'project_name'
auth_url = "auth_url"
username = "username"
password = "password"
project_name = "project_name"
store = SwiftSessionStore()
session = store.get_session(
auth_url=auth_url,
@@ -34,10 +32,12 @@ class TestAttachmentSwift(TestIrAttachment):
project_name=project_name,
)
self.assertEqual(session.auth.auth_url, auth_url)
self.assertEqual(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_username"), username
)
self.assertEqual(
session.auth.get_cache_id_elements().get("password_password"), password
)
self.assertEqual(session.auth.project_name, project_name)
# get the same session on a second call
@@ -48,73 +48,73 @@ class TestAttachmentSwift(TestIrAttachment):
password=password,
project_name=project_name,
),
session
session,
)
@patch('swiftclient.client')
@patch("swiftclient.client")
def test_connection(self, mock_swift_client):
""" Test the connection to the store"""
os.environ['SWIFT_AUTH_URL'] = 'auth_url'
os.environ['SWIFT_ACCOUNT'] = 'account'
os.environ['SWIFT_PASSWORD'] = 'password'
os.environ['SWIFT_PROJECT_NAME'] = 'project_name'
os.environ['SWIFT_REGION_NAME'] = 'NOWHERE'
"""Test the connection to the store"""
os.environ["SWIFT_AUTH_URL"] = "auth_url"
os.environ["SWIFT_ACCOUNT"] = "account"
os.environ["SWIFT_PASSWORD"] = "password"
os.environ["SWIFT_PROJECT_NAME"] = "project_name"
os.environ["SWIFT_REGION_NAME"] = "NOWHERE"
attachment = self.Attachment
attachment._get_swift_connection()
mock_swift_client.Connection.assert_called_once_with(
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
session = kwargs['session']
session = kwargs["session"]
self.assertTrue(isinstance(session, keystoneauth1.session.Session))
self.assertEqual(session.auth.auth_url, os.environ['SWIFT_AUTH_URL'])
self.assertEqual(session.auth.get_cache_id_elements().get(
'password_username'), os.environ['SWIFT_ACCOUNT'])
self.assertEqual(session.auth.get_cache_id_elements().get(
'password_password'), os.environ['SWIFT_PASSWORD'])
self.assertEqual(session.auth.project_name,
os.environ['SWIFT_PROJECT_NAME'])
self.assertEqual(session.auth.auth_url, os.environ["SWIFT_AUTH_URL"])
self.assertEqual(
session.auth.get_cache_id_elements().get("password_username"),
os.environ["SWIFT_ACCOUNT"],
)
self.assertEqual(
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):
"""
Test writing a file
"""
(self.env['ir.config_parameter'].
set_param('ir_attachment.location', 'swift'))
os.environ['SWIFT_AUTH_URL'] = 'auth_url'
os.environ['SWIFT_ACCOUNT'] = 'account'
os.environ['SWIFT_PASSWORD'] = 'password'
os.environ['SWIFT_PROJECT_NAME'] = 'project_name'
os.environ['SWIFT_WRITE_CONTAINER'] = 'my_container'
container = os.environ.get('SWIFT_WRITE_CONTAINER')
(self.env["ir.config_parameter"].set_param("ir_attachment.location", "swift"))
os.environ["SWIFT_AUTH_URL"] = "auth_url"
os.environ["SWIFT_ACCOUNT"] = "account"
os.environ["SWIFT_PASSWORD"] = "password"
os.environ["SWIFT_PROJECT_NAME"] = "project_name"
os.environ["SWIFT_WRITE_CONTAINER"] = "my_container"
container = os.environ.get("SWIFT_WRITE_CONTAINER")
attachment = self.Attachment
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
attachment.create({'name': 'a5', 'datas': self.blob1_b64})
attachment.create({"name": "a5", "datas": self.blob1_b64})
conn.put_object.assert_called_with(
container,
attachment._compute_checksum(bin_data),
bin_data)
container, attachment._compute_checksum(bin_data), bin_data
)
def test_delete_file_on_swift(self):
"""
Test deleting a file
"""
(self.env['ir.config_parameter'].
set_param('ir_attachment.location', 'swift'))
os.environ['SWIFT_AUTH_URL'] = 'auth_url'
os.environ['SWIFT_ACCOUNT'] = 'account'
os.environ['SWIFT_PASSWORD'] = 'password'
os.environ['SWIFT_PROJECT_NAME'] = 'project_name'
os.environ['SWIFT_WRITE_CONTAINER'] = 'my_container'
(self.env["ir.config_parameter"].set_param("ir_attachment.location", "swift"))
os.environ["SWIFT_AUTH_URL"] = "auth_url"
os.environ["SWIFT_ACCOUNT"] = "account"
os.environ["SWIFT_PASSWORD"] = "password"
os.environ["SWIFT_PROJECT_NAME"] = "project_name"
os.environ["SWIFT_WRITE_CONTAINER"] = "my_container"
attachment = self.Attachment
container = os.environ.get('SWIFT_WRITE_CONTAINER')
with patch('swiftclient.client.Connection') as MockConnection:
container = os.environ.get("SWIFT_WRITE_CONTAINER")
with patch("swiftclient.client.Connection") as MockConnection:
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)
a5.unlink()
conn.delete_object.assert_called_with(container, uri.item())
+14 -14
View File
@@ -1,10 +1,12 @@
# Copyright 2017-2019 Camptocamp SA
# 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 odoo.addons.base.tests.test_ir_attachment import TestIrAttachment
from ..swift_uri import SwiftUri
class TestAttachmentSwift(TestIrAttachment):
"""
@@ -12,28 +14,26 @@ class TestAttachmentSwift(TestIrAttachment):
"""
def setup(self):
super().setUp()
self.env['ir.config_parameter'].set_param('ir_attachment.location',
'swift')
res = super().setUp()
self.env["ir.config_parameter"].set_param("ir_attachment.location", "swift")
return res
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()
self.assertNotEqual(conn, False)
def test_store_file_on_swift(self):
""" Test writing a file and then reading it """
(self.env['ir.config_parameter'].
set_param('ir_attachment.location', 'swift'))
a5 = self.Attachment.create({'name': 'a5', 'datas': self.blob1_b64})
"""Test writing a file and then reading it"""
(self.env["ir.config_parameter"].set_param("ir_attachment.location", "swift"))
a5 = self.Attachment.create({"name": "a5", "datas": self.blob1_b64})
a5bis = self.Attachment.browse(a5.id)[0]
self.assertEqual(a5.datas, a5bis.datas)
def test_delete_file_on_swift(self):
""" Create a file and then test the deletion """
(self.env['ir.config_parameter'].
set_param('ir_attachment.location', 'swift'))
a5 = self.Attachment.create({'name': 'a5', 'datas': self.blob1_b64})
"""Create a file and then test the deletion"""
(self.env["ir.config_parameter"].set_param("ir_attachment.location", "swift"))
a5 = self.Attachment.create({"name": "a5", "datas": self.blob1_b64})
uri = SwiftUri(a5.store_fname)
con = self.Attachment._get_swift_connection()
con.get_object(uri.container(), uri.item())
@@ -10,7 +10,7 @@
"license": "AGPL-3",
"category": "Knowledge Management",
"depends": ["base"],
"website": "http://www.camptocamp.com",
"website": "https://github.com/camptocamp/odoo-cloud-platform",
"data": ["data/res_config_settings_data.xml"],
"installable": 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">
<record id="ir_attachment_storage_force_database" model="ir.config_parameter">
<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>
</odoo>
@@ -5,16 +5,16 @@ import inspect
import logging
import os
import time
from .strtobool import strtobool
from contextlib import closing, contextmanager
import psycopg2
import odoo
from contextlib import closing, contextmanager
from odoo import api, exceptions, models, _
import odoo
from odoo import _, api, exceptions, models
from odoo.osv.expression import AND, OR, normalize_domain
from odoo.tools.safe_eval import const_eval
from .strtobool import strtobool
_logger = logging.getLogger(__name__)
@@ -242,7 +242,7 @@ class IrAttachment(models.Model):
if not count:
self._store_file_delete(fname)
else:
super()._file_delete(fname)
return super()._file_delete(fname)
@api.model
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
# which adds ('res_field', '=', False) when the domain does not
# 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 = [
"!",
("store_fname", "=like", "{}://%".format(storage)),
@@ -1,21 +1,21 @@
_MAP = {
'y': True,
'yes': True,
't': True,
'true': True,
'on': True,
'1': True,
'n': False,
'no': False,
'f': False,
'false': False,
'off': False,
'0': False
"y": True,
"yes": True,
"t": True,
"true": True,
"on": True,
"1": True,
"n": False,
"no": False,
"f": False,
"false": False,
"off": False,
"0": False,
}
def strtobool(value):
try:
return _MAP[str(value).lower()]
except KeyError:
raise ValueError('"{}" is not a valid bool value'.format(value))
except KeyError as error:
raise ValueError('"{}" is not a valid bool value'.format(value)) from error
-1
View File
@@ -1,2 +1 @@
from . import fields
+1
View File
@@ -6,6 +6,7 @@
"version": "15.0.1.0.0",
"category": "Technical Settings",
"author": "Camptocamp, Odoo Community Association (OCA)",
"website": "https://github.com/camptocamp/odoo-cloud-platform",
"license": "AGPL-3",
"depends": [
"base_attachment_object_storage",
+20 -20
View File
@@ -4,7 +4,6 @@ import unicodedata
from odoo import fields
fields.Field.__doc__ += """
.. _field-fileurl:
@@ -29,10 +28,10 @@ fields.Field.__doc__ += """
class FileURL(fields.Binary):
_slots = {
'attachment': True, # Override default with True
'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
'filename': '', # Field to use to store the filename on ir.attachment
"attachment": True, # Override default with True
"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
"filename": "", # Field to use to store the filename on ir.attachment
}
# pylint: disable=method-required-super
@@ -47,22 +46,22 @@ class FileURL(fields.Binary):
if not value:
continue
vals = {
'name': self.name,
'res_model': self.model_name,
'res_field': self.name,
'res_id': record.id,
'type': 'binary',
'datas': value,
"name": self.name,
"res_model": self.model_name,
"res_field": self.name,
"res_id": record.id,
"type": "binary",
"datas": value,
}
fname = False
if self.filename:
fname = record[self.filename]
vals['datas_fname'] = fname
vals["datas_fname"] = fname
if fname and self.storage_path:
storage_key = self._build_storage_key(fname)
if not fname:
storage_key = False
env['ir.attachment'].sudo().with_context(
env["ir.attachment"].sudo().with_context(
binary_field_real_user=env.user,
storage_location=self.storage_location,
force_storage_key=storage_key,
@@ -80,21 +79,22 @@ class FileURL(fields.Binary):
storage_location=self.storage_location,
force_storage_key=storage_key,
),
value
value,
)
return True
def _setup_regular_base(self, model):
super()._setup_regular_base(model)
res = super()._setup_regular_base(model)
if self.storage_path:
assert self.filename is not None, \
assert self.filename is not None, (
"Field %s defines storage_path without filename" % self
)
return res
def _build_storage_key(self, filename):
return '/'.join([
self.storage_path.rstrip('/'),
unicodedata.normalize('NFKC', filename)
])
return "/".join(
[self.storage_path.rstrip("/"), unicodedata.normalize("NFKC", filename)]
)
fields.FileURL = FileURL
-1
View File
@@ -1,2 +1 @@
from . import models
+1 -1
View File
@@ -15,7 +15,7 @@
"logging_json",
"server_environment", # OCA/server-tools
],
"website": "https://www.camptocamp.com",
"website": "https://github.com/camptocamp/odoo-cloud-platform",
"data": [],
"installable": True,
}
-1
View File
@@ -1,2 +1 @@
from . import cloud_platform
+33 -42
View File
@@ -4,46 +4,39 @@
import logging
import os
import re
from collections import namedtuple
from .strtobool import strtobool
from odoo import api, models
from odoo.tools.config import config
from .strtobool import strtobool
_logger = logging.getLogger(__name__)
def is_true(strval):
return bool(strtobool(strval or '0'))
return bool(strtobool(strval or "0"))
PlatformConfig = namedtuple(
'PlatformConfig',
'filestore'
)
PlatformConfig = namedtuple("PlatformConfig", "filestore")
FilestoreKind = namedtuple(
'FilestoreKind',
['name', 'location']
)
FilestoreKind = namedtuple("FilestoreKind", ["name", "location"])
class CloudPlatform(models.AbstractModel):
_name = 'cloud.platform'
_description = 'cloud.platform'
_name = "cloud.platform"
_description = "cloud.platform"
@api.model
def _default_config(self):
return PlatformConfig(self._filestore_kinds()['db'])
return PlatformConfig(self._filestore_kinds()["db"])
@api.model
def _filestore_kinds(self):
return {
'db': FilestoreKind('db', 'local'),
'file': FilestoreKind('file', 'local'),
"db": FilestoreKind("db", "local"),
"file": FilestoreKind("file", "local"),
}
@api.model
@@ -53,33 +46,31 @@ class CloudPlatform(models.AbstractModel):
@api.model
def _config_by_server_env(self, platform_kind, environment):
configs_getter = getattr(
self,
'_config_by_server_env_for_%s' % platform_kind,
None
self, "_config_by_server_env_for_%s" % platform_kind, None
)
configs = configs_getter() if configs_getter else {}
return configs.get(environment) or self._default_config()
def _get_running_env(self):
environment_name = config['running_env']
if environment_name.startswith('labs'):
environment_name = config["running_env"]
if environment_name.startswith("labs"):
# We allow to have environments such as 'labs-logistics'
# or 'labs-finance', in order to have the matching ribbon.
environment_name = 'labs'
environment_name = "labs"
return environment_name
@api.model
def _install(self, platform_kind):
assert platform_kind in self._platform_kinds()
params = self.env['ir.config_parameter'].sudo()
params.set_param('cloud.platform.kind', platform_kind)
params = self.env["ir.config_parameter"].sudo()
params.set_param("cloud.platform.kind", platform_kind)
environment_name = self._get_running_env()
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()
if configs.filestore.location == 'remote':
self.env['ir.attachment'].sudo().force_storage()
_logger.info('cloud platform configured for {}'.format(platform_kind))
if configs.filestore.location == "remote":
self.env["ir.attachment"].sudo().force_storage()
_logger.info("cloud platform configured for {}".format(platform_kind))
@api.model
def install(self):
@@ -91,39 +82,39 @@ class CloudPlatform(models.AbstractModel):
@api.model
def _check_redis(self, environment_name):
if environment_name in ('prod', 'integration', 'labs', 'test'):
assert is_true(os.environ.get('ODOO_SESSION_REDIS')), (
if environment_name in ("prod", "integration", "labs", "test"):
assert is_true(os.environ.get("ODOO_SESSION_REDIS")), (
"Redis must be activated on prod, integration, labs,"
" test instances. This is done by setting ODOO_SESSION_REDIS=1."
)
assert (os.environ.get('ODOO_SESSION_REDIS_HOST') or
os.environ.get('ODOO_SESSION_REDIS_SENTINEL_HOST') or
os.environ.get('ODOO_SESSION_REDIS_URL')), (
assert (
os.environ.get("ODOO_SESSION_REDIS_HOST")
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_SENTINEL_HOST or "
"ODOO_SESSION_REDIS_URL "
"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 "
"to store sessions on Redis"
)
prefix = os.environ['ODOO_SESSION_REDIS_PREFIX']
assert re.match(r'^[a-z-0-9]+-odoo-[a-z-0-9]+$', prefix), (
prefix = os.environ["ODOO_SESSION_REDIS_PREFIX"]
assert re.match(r"^[a-z-0-9]+-odoo-[a-z-0-9]+$", prefix), (
"ODOO_SESSION_REDIS_PREFIX must match '<client>-odoo-<env>'"
", we got: '%s'" % (prefix,)
)
@api.model
def check(self):
if is_true(os.environ.get('ODOO_CLOUD_PLATFORM_UNSAFE')):
_logger.warning(
"cloud platform checks disabled, this is not safe"
)
if is_true(os.environ.get("ODOO_CLOUD_PLATFORM_UNSAFE")):
_logger.warning("cloud platform checks disabled, this is not safe")
return
params = self.env['ir.config_parameter'].sudo()
kind = params.get_param('cloud.platform.kind')
params = self.env["ir.config_parameter"].sudo()
kind = params.get_param("cloud.platform.kind")
if not kind:
_logger.warning(
"cloud platform not configured, you should "
+14 -14
View File
@@ -1,21 +1,21 @@
_MAP = {
'y': True,
'yes': True,
't': True,
'true': True,
'on': True,
'1': True,
'n': False,
'no': False,
'f': False,
'false': False,
'off': False,
'0': False
"y": True,
"yes": True,
"t": True,
"true": True,
"on": True,
"1": True,
"n": False,
"no": False,
"f": False,
"false": False,
"off": False,
"0": False,
}
def strtobool(value):
try:
return _MAP[str(value).lower()]
except KeyError:
raise ValueError('"{}" is not a valid bool value'.format(value))
except KeyError as error:
raise ValueError('"{}" is not a valid bool value'.format(value)) from error
+1 -2
View File
@@ -1,3 +1,2 @@
def install(ctx):
ctx.env['cloud.platform'].install()
ctx.env["cloud.platform"].install()
+1 -2
View File
@@ -1,5 +1,4 @@
Cloud Platform Azure
====================
# Cloud Platform Azure
Install addons specific to the Azure setup.
+1 -1
View File
@@ -18,7 +18,7 @@
"cloud_platform_ovh",
"cloud_platform_exoscale",
],
"website": "https://www.camptocamp.com",
"website": "https://github.com/camptocamp/odoo-cloud-platform",
"data": [],
"installable": True,
}
@@ -1,13 +1,15 @@
# Copyright 2016-2021 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
import re
import os
import re
from odoo import models, api
from odoo.addons.cloud_platform.models.cloud_platform import FilestoreKind
from odoo.addons.cloud_platform.models.cloud_platform import PlatformConfig
from odoo import api, models
from odoo.addons.cloud_platform.models.cloud_platform import (
FilestoreKind,
PlatformConfig,
)
AZURE_STORE_KIND = FilestoreKind("azure", "remote")
@@ -42,8 +44,7 @@ class CloudPlatform(models.AbstractModel):
@api.model
def _check_filestore(self, environment_name):
params = self.env["ir.config_parameter"].sudo()
use_azure = (params.get_param("ir_attachment.location") ==
AZURE_STORE_KIND.name)
use_azure = params.get_param("ir_attachment.location") == AZURE_STORE_KIND.name
if environment_name in ("prod", "integration"):
# Labs instances use azure by default, but we don't want
# to enforce it in case we want to test something with a different
+1 -2
View File
@@ -1,5 +1,4 @@
Cloud Platform Exoscale
=======================
# Cloud Platform Exoscale
Install addons specific to the Exoscale setup.
+1 -1
View File
@@ -17,7 +17,7 @@
"excludes": [
"cloud_platform_ovh",
],
"website": "https://www.camptocamp.com",
"website": "https://github.com/camptocamp/odoo-cloud-platform",
"data": [],
"installable": False,
}
@@ -1,50 +1,51 @@
# Copyright 2016-2019 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
import re
import os
import re
from odoo import models, api
from odoo.addons.cloud_platform.models.cloud_platform import FilestoreKind
from odoo.addons.cloud_platform.models.cloud_platform import PlatformConfig
from odoo import api, models
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):
_inherit = 'cloud.platform'
_inherit = "cloud.platform"
@api.model
def _filestore_kinds(self):
kinds = super(CloudPlatform, self)._filestore_kinds()
kinds['s3'] = S3_STORE_KIND
kinds["s3"] = S3_STORE_KIND
return kinds
@api.model
def _platform_kinds(self):
kinds = super(CloudPlatform, self)._platform_kinds()
kinds.append('exoscale')
kinds.append("exoscale")
return kinds
@api.model
def _config_by_server_env_for_exoscale(self):
fs_kinds = self._filestore_kinds()
configs = {
'prod': PlatformConfig(filestore=fs_kinds['s3']),
'integration': PlatformConfig(filestore=fs_kinds['s3']),
'labs': PlatformConfig(filestore=fs_kinds['s3']),
'test': PlatformConfig(filestore=fs_kinds['db']),
'dev': PlatformConfig(filestore=fs_kinds['db']),
"prod": PlatformConfig(filestore=fs_kinds["s3"]),
"integration": PlatformConfig(filestore=fs_kinds["s3"]),
"labs": PlatformConfig(filestore=fs_kinds["s3"]),
"test": PlatformConfig(filestore=fs_kinds["db"]),
"dev": PlatformConfig(filestore=fs_kinds["db"]),
}
return configs
@api.model
def _check_filestore(self, environment_name):
params = self.env['ir.config_parameter'].sudo()
use_s3 = (params.get_param('ir_attachment.location') ==
S3_STORE_KIND.name)
if environment_name in ('prod', 'integration'):
params = self.env["ir.config_parameter"].sudo()
use_s3 = params.get_param("ir_attachment.location") == S3_STORE_KIND.name
if environment_name in ("prod", "integration"):
# Labs instances use s3 by default, but we don't want
# to enforce it in case we want to test something with a different
# storage. At your own risks!
@@ -55,16 +56,16 @@ class CloudPlatform(models.AbstractModel):
"automatically."
)
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 "
"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 "
"ir_attachment.location is 's3'."
)
bucket_name = os.environ.get('AWS_BUCKETNAME', '')
if environment_name in ('prod', 'integration', 'labs'):
bucket_name = os.environ.get("AWS_BUCKETNAME", "")
if environment_name in ("prod", "integration", "labs"):
assert bucket_name, (
"AWS_BUCKETNAME environment variable is required when "
"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
# structure
if os.environ.get('AWS_BUCKETNAME_UNSTRUCTURED'):
if os.environ.get("AWS_BUCKETNAME_UNSTRUCTURED"):
return
prod_bucket = bool(re.match(r'[a-z-0-9]+-odoo-prod', bucket_name))
if environment_name == 'prod':
prod_bucket = bool(re.match(r"[a-z-0-9]+-odoo-prod", bucket_name))
if environment_name == "prod":
assert prod_bucket, (
"AWS_BUCKETNAME should match '<client>-odoo-prod', "
"we got: '%s'" % (bucket_name,)
@@ -96,9 +97,9 @@ class CloudPlatform(models.AbstractModel):
"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
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 "
"'ir_attachment.location' set to 'db'. This is "
"automatically set by the function 'install()'."
@@ -106,4 +107,4 @@ class CloudPlatform(models.AbstractModel):
@api.model
def install(self):
self._install('exoscale')
self._install("exoscale")
+1 -3
View File
@@ -1,7 +1,5 @@
Cloud Platform OVH
==================
# Cloud Platform OVH
Install addons specific to the OVH setup.
* The object storage is Swift
+1 -1
View File
@@ -17,7 +17,7 @@
"excludes": [
"cloud_platform_exoscale",
],
"website": "https://www.camptocamp.com",
"website": "https://github.com/camptocamp/odoo-cloud-platform",
"data": [],
"installable": False,
}
+28 -29
View File
@@ -1,51 +1,51 @@
# Copyright 2017-2019 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
import re
import os
import re
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,
)
SWIFT_STORE_KIND = FilestoreKind('swift', 'remote')
SWIFT_STORE_KIND = FilestoreKind("swift", "remote")
class CloudPlatform(models.AbstractModel):
_inherit = 'cloud.platform'
_inherit = "cloud.platform"
@api.model
def _filestore_kinds(self):
kinds = super(CloudPlatform, self)._filestore_kinds()
kinds['swift'] = SWIFT_STORE_KIND
kinds["swift"] = SWIFT_STORE_KIND
return kinds
@api.model
def _platform_kinds(self):
kinds = super()._platform_kinds()
kinds.append('ovh')
kinds.append("ovh")
return kinds
@api.model
def _config_by_server_env_for_ovh(self):
fs_kinds = self._filestore_kinds()
configs = {
'prod': PlatformConfig(filestore=fs_kinds['swift']),
'integration': PlatformConfig(filestore=fs_kinds['swift']),
'labs': PlatformConfig(filestore=fs_kinds['swift']),
'test': PlatformConfig(filestore=fs_kinds['db']),
'dev': PlatformConfig(filestore=fs_kinds['db']),
"prod": PlatformConfig(filestore=fs_kinds["swift"]),
"integration": PlatformConfig(filestore=fs_kinds["swift"]),
"labs": PlatformConfig(filestore=fs_kinds["swift"]),
"test": PlatformConfig(filestore=fs_kinds["db"]),
"dev": PlatformConfig(filestore=fs_kinds["db"]),
}
return configs
@api.model
def _check_filestore(self, environment_name):
params = self.env['ir.config_parameter'].sudo()
use_swift = (params.get_param('ir_attachment.location') ==
SWIFT_STORE_KIND.name)
if environment_name in ('prod', 'integration'):
params = self.env["ir.config_parameter"].sudo()
use_swift = params.get_param("ir_attachment.location") == SWIFT_STORE_KIND.name
if environment_name in ("prod", "integration"):
# Labs instances use swift by default, but we don't want
# to enforce it in case we want to test something with a different
# storage. At your own risks!
@@ -56,20 +56,20 @@ class CloudPlatform(models.AbstractModel):
"automatically."
)
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 "
"ir_attachment.location is 'swift'."
)
assert os.environ.get('SWIFT_ACCOUNT'), (
assert os.environ.get("SWIFT_ACCOUNT"), (
"SWIFT_ACCOUNT environment variable is required when "
"ir_attachment.location is 'swift'."
)
assert os.environ.get('SWIFT_PASSWORD'), (
assert os.environ.get("SWIFT_PASSWORD"), (
"SWIFT_PASSWORD environment variable is required when "
"ir_attachment.location is 'swift'."
)
container_name = os.environ.get('SWIFT_WRITE_CONTAINER', '')
if environment_name in ('prod', 'integration', 'labs'):
container_name = os.environ.get("SWIFT_WRITE_CONTAINER", "")
if environment_name in ("prod", "integration", "labs"):
assert container_name, (
"SWIFT_WRITE_CONTAINER environment variable is required when "
"ir_attachment.location is 'swift'.\n"
@@ -80,16 +80,15 @@ class CloudPlatform(models.AbstractModel):
"If you don't actually need a bucket, change the"
" 'ir_attachment.location' parameter."
)
prod_container = bool(re.match(r'[a-z0-9-]+-odoo-prod',
container_name))
prod_container = bool(re.match(r"[a-z0-9-]+-odoo-prod", container_name))
# A bucket name is defined under the following format
# <client>-odoo-<env>
#
# Use SWIFT_WRITE_CONTAINER_UNSTRUCTURED to by-pass check on bucket name
# structure
if os.environ.get('SWIFT_WRITE_CONTAINER_UNSTRUCTURED'):
if os.environ.get("SWIFT_WRITE_CONTAINER_UNSTRUCTURED"):
return
if environment_name == 'prod':
if environment_name == "prod":
assert prod_container, (
"SWIFT_WRITE_CONTAINER should match '<client>-odoo-prod', "
"we got: '%s'" % (container_name,)
@@ -101,9 +100,9 @@ class CloudPlatform(models.AbstractModel):
"SWIFT_WRITE_CONTAINER should not match "
"'<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
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 "
"'ir_attachment.location' set to 'db'. This is "
"automatically set by the function 'install()'."
@@ -111,4 +110,4 @@ class CloudPlatform(models.AbstractModel):
@api.model
def install(self):
self._install('ovh')
self._install("ovh")
-1
View File
@@ -1,2 +1 @@
from . import models
+1 -1
View File
@@ -11,7 +11,7 @@
"depends": [
"base",
],
"website": "http://www.camptocamp.com",
"website": "https://github.com/camptocamp/odoo-cloud-platform",
"data": [],
"installable": True,
}
-1
View File
@@ -1,2 +1 @@
from . import ir_qweb
+1 -1
View File
@@ -1,8 +1,8 @@
# Copyright 2016-2019 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
from odoo.tools import config
from odoo import models
from odoo.tools import config
class IrQweb(models.AbstractModel):
-1
View File
@@ -1,2 +1 @@
from . import json_log
+1 -1
View File
@@ -13,7 +13,7 @@
"external_dependencies": {
"python": ["python-json-logger"],
},
"website": "http://www.camptocamp.com",
"website": "https://github.com/camptocamp/odoo-cloud-platform",
"data": [],
"installable": True,
}
+2 -2
View File
@@ -6,10 +6,10 @@ import os
import threading
import uuid
from .strtobool import strtobool
from odoo import http
from .strtobool import strtobool
_logger = logging.getLogger(__name__)
try:
+14 -14
View File
@@ -1,21 +1,21 @@
_MAP = {
'y': True,
'yes': True,
't': True,
'true': True,
'on': True,
'1': True,
'n': False,
'no': False,
'f': False,
'false': False,
'off': False,
'0': False
"y": True,
"yes": True,
"t": True,
"true": True,
"on": True,
"1": True,
"n": False,
"no": False,
"f": False,
"false": False,
"off": False,
"0": False,
}
def strtobool(value):
try:
return _MAP[str(value).lower()]
except KeyError:
raise ValueError('"{}" is not a valid bool value'.format(value))
except KeyError as error:
raise ValueError('"{}" is not a valid bool value'.format(value)) from error
-1
View File
@@ -1,2 +1 @@
from . import models
+1 -1
View File
@@ -9,7 +9,7 @@
"license": "AGPL-3",
"category": "category",
"depends": ["base", "web"],
"website": "http://www.camptocamp.com",
"website": "https://github.com/camptocamp/odoo-cloud-platform",
"data": [],
"installable": False,
}
@@ -1,2 +1 @@
from . import ir_http
@@ -9,7 +9,6 @@ from odoo import models
from odoo.http import request as http_request
from odoo.tools.config import config
_logger = logging.getLogger("monitoring.http.requests")
+1 -1
View File
@@ -13,7 +13,7 @@
"web",
"server_environment",
],
"website": "http://www.camptocamp.com",
"website": "https://github.com/camptocamp/odoo-cloud-platform",
"data": [],
"external_dependencies": {
"python": ["prometheus_client"],
@@ -1,11 +1,12 @@
# Copyright 2016-2021 Camptocamp SA
# 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 odoo.http import Controller, route
class PrometheusController(Controller):
@route('/metrics', auth='public')
@route("/metrics", auth="public")
def metrics(self):
return generate_latest()
+2 -2
View File
@@ -1,10 +1,10 @@
# Copyright 2016-2021 Camptocamp SA
# 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.http import request
from prometheus_client import Summary, Counter
REQUEST_TIME = Summary(
"request_latency_sec", "Request response time in sec", ["query_type"]
-1
View File
@@ -1,2 +1 @@
from . import models
+1 -1
View File
@@ -13,7 +13,7 @@
"web",
"server_environment",
],
"website": "http://www.camptocamp.com",
"website": "https://github.com/camptocamp/odoo-cloud-platform",
"data": [],
"external_dependencies": {
"python": ["statsd"],
-1
View File
@@ -1,2 +1 @@
from . import ir_http
+26 -18
View File
@@ -4,38 +4,46 @@
from odoo import models
from odoo.http import request
from ..statsd_client import statsd, customer, environment
from ..statsd_client import customer, environment, statsd
class IrHttp(models.AbstractModel):
_inherit = 'ir.http'
_inherit = "ir.http"
@classmethod
def _dispatch(cls, endpoint):
if not statsd:
return super()._dispatch(endpoint)
path_info = request.httprequest.environ.get('PATH_INFO')
if path_info.startswith('/longpolling/'):
path_info = request.httprequest.environ.get("PATH_INFO")
if path_info.startswith("/longpolling/"):
return super()._dispatch(endpoint)
parts = ['http', ]
if path_info.startswith('/web/dataset/call_button'):
parts += ['button',
customer, environment,
request.params['model'].replace('.', '_'),
request.params['method'],
parts = [
"http",
]
elif path_info.startswith('/web/dataset/exec_workflow'):
parts += ['workflow',
customer, environment,
request.params['model'].replace('.', '_'),
request.params['signal'],
if path_info.startswith("/web/dataset/call_button"):
parts += [
"button",
customer,
environment,
request.params["model"].replace(".", "_"),
request.params["method"],
]
elif path_info.startswith("/web/dataset/exec_workflow"):
parts += [
"workflow",
customer,
environment,
request.params["model"].replace(".", "_"),
request.params["signal"],
]
else:
parts += ['request',
customer, environment,
parts += [
"request",
customer,
environment,
]
with statsd.timer('.'.join(parts)):
with statsd.timer(".".join(parts)):
return super()._dispatch(endpoint)
+12 -12
View File
@@ -1,16 +1,16 @@
_MAP = {
'y': True,
'yes': True,
't': True,
'true': True,
'on': True,
'1': True,
'n': False,
'no': False,
'f': False,
'false': False,
'off': False,
'0': False
"y": True,
"yes": True,
"t": True,
"true": True,
"on": True,
"1": True,
"n": False,
"no": False,
"f": False,
"false": False,
"off": False,
"0": False,
}
+1 -1
View File
@@ -9,7 +9,7 @@
"license": "AGPL-3",
"category": "category",
"depends": ["base", "web"],
"website": "http://www.camptocamp.com",
"website": "https://github.com/camptocamp/odoo-cloud-platform",
"data": [],
"installable": True,
}
+8 -9
View File
@@ -1,18 +1,18 @@
# Copyright 2016-2019 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
import logging
import json
import logging
import werkzeug
from odoo import http
from odoo.addons.web.controllers.main import ensure_db
class HealthCheckFilter(logging.Filter):
def __init__(self, path, name=''):
def __init__(self, path, name=""):
super().__init__(name)
self.path = path
@@ -20,20 +20,19 @@ class HealthCheckFilter(logging.Filter):
return self.path not in record.getMessage()
logging.getLogger('werkzeug').addFilter(
HealthCheckFilter('GET /monitoring/status HTTP')
logging.getLogger("werkzeug").addFilter(
HealthCheckFilter("GET /monitoring/status HTTP")
)
class Monitoring(http.Controller):
@http.route('/monitoring/status', type='http', auth='none')
@http.route("/monitoring/status", type="http", auth="none")
def status(self):
ensure_db()
# TODO: add 'sub-systems' status and infos:
# queue job, cron, database, ...
headers = {'Content-Type': 'application/json'}
info = {'status': 1}
headers = {"Content-Type": "application/json"}
info = {"status": 1}
session = http.request.session
# 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
-1
View File
@@ -1,3 +1,2 @@
from . import http
from . import session
+1 -1
View File
@@ -13,7 +13,7 @@
"external_dependencies": {
"python": ["redis"],
},
"website": "http://www.camptocamp.com",
"website": "https://github.com/camptocamp/odoo-cloud-platform",
"data": [],
"installable": True,
}
+13 -7
View File
@@ -4,13 +4,12 @@
import logging
import os
from .strtobool import strtobool
from odoo import http
from odoo.tools import config
from odoo.tools.func import lazy_property
from .session import RedisSessionStore
from .strtobool import strtobool
_logger = logging.getLogger(__name__)
@@ -52,10 +51,13 @@ def session_store(self):
redis_client = redis.from_url(url)
else:
redis_client = redis.Redis(host=host, port=port, password=password)
return RedisSessionStore(redis=redis_client, prefix=prefix,
return RedisSessionStore(
redis=redis_client,
prefix=prefix,
expiration=expiration,
anon_expiration=anon_expiration,
session_class=http.Session)
session_class=http.Session,
)
def purge_fs_sessions(path):
@@ -64,7 +66,7 @@ def purge_fs_sessions(path):
try:
os.unlink(path)
except OSError:
pass
_logger.warning("OS Error during purge of redis sessions.")
if is_true(os.environ.get("ODOO_SESSION_REDIS")):
@@ -77,8 +79,12 @@ if is_true(os.environ.get("ODOO_SESSION_REDIS")):
sentinel_port,
)
else:
_logger.debug("HTTP sessions stored in Redis with prefix '%s' on "
"%s:%s", prefix or '', host, port)
_logger.debug(
"HTTP sessions stored in Redis with prefix '%s' on " "%s:%s",
prefix or "",
host,
port,
)
http.Application.session_store = session_store
# clean the existing sessions on the file system
purge_fs_sessions(config.session_dir)
-1
View File
@@ -2,7 +2,6 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
import json
from datetime import date, datetime
import dateutil
+41 -29
View File
@@ -18,10 +18,16 @@ _logger = logging.getLogger(__name__)
class RedisSessionStore(SessionStore):
""" SessionStore that saves session to redis """
"""SessionStore that saves session to redis"""
def __init__(self, redis, session_class=None,
prefix='', expiration=None, anon_expiration=None):
def __init__(
self,
redis,
session_class=None,
prefix="",
expiration=None,
anon_expiration=None,
):
super().__init__(session_class=session_class)
self.redis = redis
if expiration is None:
@@ -32,14 +38,12 @@ class RedisSessionStore(SessionStore):
self.anon_expiration = DEFAULT_SESSION_TIMEOUT_ANONYMOUS
else:
self.anon_expiration = anon_expiration
self.prefix = 'session:'
self.prefix = "session:"
if prefix:
self.prefix = '%s:%s:' % (
self.prefix, prefix
)
self.prefix = "%s:%s:" % (self.prefix, prefix)
def build_key(self, sid):
return '%s%s' % (self.prefix, sid)
return "%s%s" % (self.prefix, sid)
def save(self, session):
key = self.build_key(session.sid)
@@ -52,51 +56,59 @@ class RedisSessionStore(SessionStore):
expiration = session.expiration or self.anon_expiration
if _logger.isEnabledFor(logging.DEBUG):
if session.uid:
user_msg = "user '%s' (id: %s)" % (
session.login, session.uid)
user_msg = "user '%s' (id: %s)" % (session.login, session.uid)
else:
user_msg = "anonymous user"
_logger.debug("saving session with key '%s' and "
"expiration of %s seconds for %s",
key, expiration, user_msg)
_logger.debug(
"saving session with key '%s' and " "expiration of %s seconds for %s",
key,
expiration,
user_msg,
)
data = json.dumps(
dict(session), cls=json_encoding.SessionEncoder
).encode('utf-8')
data = json.dumps(dict(session), cls=json_encoding.SessionEncoder).encode(
"utf-8"
)
if self.redis.set(key, data):
return self.redis.expire(key, expiration)
def delete(self, session):
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)
def get(self, sid):
if not self.is_valid_key(sid):
_logger.debug("session with invalid sid '%s' has been asked, "
"returning a new one", sid)
_logger.debug(
"session with invalid sid '%s' has been asked, " "returning a new one",
sid,
)
return self.new()
key = self.build_key(sid)
saved = self.redis.get(key)
if not saved:
_logger.debug("session with non-existent key '%s' has been asked, "
"returning a new one", key)
_logger.debug(
"session with non-existent key '%s' has been asked, "
"returning a new one",
key,
)
return self.new()
try:
data = json.loads(
saved.decode('utf-8'), cls=json_encoding.SessionDecoder
)
data = json.loads(saved.decode("utf-8"), cls=json_encoding.SessionDecoder)
except ValueError:
_logger.debug("session for key '%s' has been asked but its json "
"content could not be read, it has been reset", key)
_logger.debug(
"session for key '%s' has been asked but its json "
"content could not be read, it has been reset",
key,
)
data = {}
return self.session_class(data, sid, False)
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")
return [key[len(self.prefix):] for key in keys]
return [key[len(self.prefix) :] for key in keys]
def rotate(self, session, env):
self.delete(session)
@@ -106,7 +118,7 @@ class RedisSessionStore(SessionStore):
self.save(session)
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
expiration.
+14 -14
View File
@@ -1,21 +1,21 @@
_MAP = {
'y': True,
'yes': True,
't': True,
'true': True,
'on': True,
'1': True,
'n': False,
'no': False,
'f': False,
'false': False,
'off': False,
'0': False
"y": True,
"yes": True,
"t": True,
"true": True,
"on": True,
"1": True,
"n": False,
"no": False,
"f": False,
"false": False,
"off": False,
"0": False,
}
def strtobool(value):
try:
return _MAP[str(value).lower()]
except KeyError:
raise ValueError('"{}" is not a valid bool value'.format(value))
except KeyError as error:
raise ValueError('"{}" is not a valid bool value'.format(value)) from error
+2 -3
View File
@@ -6,10 +6,9 @@
"version": "12.0.1.0.0",
"category": "Tests",
"author": "Camptocamp,Odoo Community Association (OCA)",
"website": "https://github.com/camptocamp/odoo-cloud-platform",
"license": "AGPL-3",
"depends": [
"base_fileurl_field"
],
"depends": ["base_fileurl_field"],
"data": [
"views/res_partner.xml",
"views/res_users.xml",
+18 -16
View File
@@ -1,44 +1,46 @@
# Copyright 2019 Camptocamp SA
# 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
class ResPartner(models.Model):
_inherit = 'res.partner'
_inherit = "res.partner"
name = fields.Char()
url_file = fields.FileURL(
storage_location='s3',
filename='url_file_fname',
storage_path='partner'
storage_location="s3", filename="url_file_fname", storage_path="partner"
)
url_file_fname = fields.Char()
url_image = fields.FileURL(
storage_location='s3',
filename='url_image_fname',
storage_path='partner_image',
storage_location="s3",
filename="url_image_fname",
storage_path="partner_image",
)
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):
rec = self.search([('url_file_fname', '=', self.url_file_fname)])
rec = self.search([("url_file_fname", "=", self.url_file_fname)])
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"
"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):
rec = self.search([('url_image_fname', '=', self.url_image_fname)])
rec = self.search([("url_image_fname", "=", self.url_image_fname)])
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"
"Model: %s Id: %s" % (self._name, rec.id)
))
)
)
+4 -4
View File
@@ -1,11 +1,11 @@
# Copyright 2019 Camptocamp SA
# 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):
_inherit = 'res.users'
_inherit = "res.users"
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 = fields.FileURL(related="partner_id.url_file")
partner_url_file_fname = fields.Char(related="partner_id.url_file_fname")
@@ -3,7 +3,7 @@
import logging
from odoo import models, api
from odoo import api, models
_logger = logging.getLogger(__name__)
@@ -14,23 +14,23 @@ class IrAttachment(models.Model):
_inherit = "ir.attachment"
def _get_stores(self):
l = ['s3']
l = ["s3"]
l += super(IrAttachment, self)._get_stores()
return l
@api.model
def _store_file_read(self, fname, bin_size=False):
if fname.startswith('s3://'):
if fname.startswith("s3://"):
return FAKE_S3_BUCKET.get(fname)
else:
return super(IrAttachment, self)._store_file_read(fname, bin_size)
@api.model
def _store_file_write(self, key, bin_data):
location = self.env.context.get('storage_location') or self._storage()
if location == 's3':
location = self.env.context.get("storage_location") or self._storage()
if location == "s3":
FAKE_S3_BUCKET[key] = bin_data
filename = 's3://fake_bucket/%s' % key
filename = "s3://fake_bucket/%s" % key
else:
_super = super(IrAttachment, self)
filename = _super._store_file_write(key, bin_data)
@@ -38,7 +38,7 @@ class IrAttachment(models.Model):
@api.model
def _store_file_delete(self, fname):
if fname.startswith('s3://'):
if fname.startswith("s3://"):
FAKE_S3_BUCKET.pop(fname)
else:
super(IrAttachment, self)._store_file_delete(fname)
@@ -2,38 +2,41 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
import base64
from odoo.tests import TransactionCase
from odoo.modules.module import get_module_resource
from odoo.exceptions import ValidationError
from odoo.modules.module import get_module_resource
from odoo.tests import TransactionCase
class TestFileUrlFields(TransactionCase):
def test_fileurl_fields(self):
file_path = get_module_resource('test_base_fileurl_field', 'data',
'sample.txt')
image_path = get_module_resource('test_base_fileurl_field', 'data',
'pattern.png')
partner = self.env.ref('base.main_partner')
with open(file_path, 'rb') as f:
with open(image_path, 'rb') as i:
partner.write({
'url_file': base64.b64encode(f.read()),
'url_file_fname': 'sample.txt',
'url_image': base64.b64encode(i.read()),
'url_image_fname': 'pattern.png',
})
file_path = get_module_resource("test_base_fileurl_field", "data", "sample.txt")
image_path = get_module_resource(
"test_base_fileurl_field", "data", "pattern.png"
)
partner = self.env.ref("base.main_partner")
with open(file_path, "rb") as f:
with open(image_path, "rb") as i:
partner.write(
{
"url_file": base64.b64encode(f.read()),
"url_file_fname": "sample.txt",
"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())
with open(image_path, 'rb') as i:
with open(image_path, "rb") as i:
self.assertEqual(base64.decodebytes(partner.url_image), i.read())
partner2 = self.env.ref('base.partner_admin')
with open(file_path, 'rb') as f:
partner2 = self.env.ref("base.partner_admin")
with open(file_path, "rb") as f:
with self.assertRaises(ValidationError):
partner2.write({
'url_file': base64.b64encode(f.read()),
'url_file_fname': 'sample.txt',
})
partner2.write(
{
"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>
<record id="view_partner_form_inherit" model="ir.ui.view">
<field name="name">res.partner.form.inherit</field>
@@ -9,11 +9,15 @@
<page name="fileurl_test" string="FileURL Test fields">
<group string="Default widget">
<field name="url_file" filename="url_file_fname" />
<field name="url_file_fname" invisible="1"/>
<field name="url_file_fname" invisible="1" />
</group>
<group string="Image widget">
<field name="url_image" widget="image" filename="url_image_fname" />
<field name="url_image_fname" invisible="1"/>
<field
name="url_image"
widget="image"
filename="url_image_fname"
/>
<field name="url_image_fname" invisible="1" />
</group>
</page>
</xpath>
+2 -2
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_users_form_inherit" model="ir.ui.view">
<field name="name">res.users.form.inherit</field>
@@ -9,7 +9,7 @@
<page name="fileurl_test" string="FileURL Test fields">
<group string="Default widget">
<field name="url_file" filename="url_file_fname" />
<field name="url_file_fname" invisible="1"/>
<field name="url_file_fname" invisible="1" />
</group>
</page>
</xpath>