merge trunk
bzr revid: nicolas.vanhoren@openerp.com-20130114102410-m60y4kh8o69u00hc
This commit is contained in:
commit
d211ac7798
|
@ -0,0 +1,34 @@
|
|||
.. _adding-command:
|
||||
|
||||
Adding a new command
|
||||
====================
|
||||
|
||||
``oe`` uses the argparse_ library to implement commands. Each
|
||||
command lives in its own ``openerpcommand/<command>.py`` file.
|
||||
|
||||
.. _argparse: http://docs.python.org/2.7/library/argparse.html
|
||||
|
||||
To create a new command, probably the most simple way to get started is to
|
||||
copy/paste an existing command, say ``openerpcommand/initialize.py`` to
|
||||
``openerpcommand/foo.py``. In the newly created file, the important bits
|
||||
are the ``run(args)`` and ``add_parser(subparsers)`` functions.
|
||||
|
||||
``add_parser``'s responsability is to create a (sub-)parser for the command,
|
||||
i.e. describe the different options and flags. The last thing it does is to set
|
||||
``run`` as the function to call when the command is invoked.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
> def add_parser(subparsers):
|
||||
> parser = subparsers.add_parser('<command-name>',
|
||||
> description='...')
|
||||
> parser.add_argument(...)
|
||||
> ...
|
||||
> parser.set_defaults(run=run)
|
||||
|
||||
``run(args)`` actually implements the command. It should be kept as simple as
|
||||
possible and delegate most of its work to small functions (probably placed at
|
||||
the top of the new file). In other words, its responsability is mainly to
|
||||
deal with the presence/absence/pre-processing of ``argparse``'s arguments.
|
||||
|
||||
Finally, the module must be added to ``openerpcommand/__init__.py``.
|
|
@ -0,0 +1,256 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# OpenERP Technical Documentation configuration file, created by
|
||||
# sphinx-quickstart on Fri Feb 17 16:14:06 2012.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys, os
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#sys.path.insert(0, os.path.abspath('.'))
|
||||
sys.path.append(os.path.abspath('_themes'))
|
||||
sys.path.append(os.path.abspath('..'))
|
||||
sys.path.append(os.path.abspath('../openerp'))
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.viewcode']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'OpenERP Server Developers Documentation'
|
||||
copyright = u'2012, OpenERP s.a.'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '7.0'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '7.0b'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
|
||||
# -- Options for HTML output ---------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'flask'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
html_theme_path = ['_themes']
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
html_sidebars = {
|
||||
'index': ['sidebarintro.html', 'sourcelink.html', 'searchbox.html'],
|
||||
'**': ['sidebarlogo.html', 'localtoc.html', 'relations.html',
|
||||
'sourcelink.html', 'searchbox.html']
|
||||
}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'openerp-server-doc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output --------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'openerp-server-doc.tex', u'OpenERP Server Developers Documentation',
|
||||
u'OpenERP s.a.', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output --------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'openerp-server-doc', u'OpenERP Server Developers Documentation',
|
||||
[u'OpenERP s.a.'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output ------------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'OpenERPServerDocumentation', u'OpenERP Server Developers Documentation',
|
||||
u'OpenERP s.a.', 'OpenERPServerDocumentation', 'Developers documentation for the openobject-server project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
||||
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {
|
||||
'python': ('http://docs.python.org/', None),
|
||||
'openerpweb': ('http://doc.openerp.com/trunk/developers/web', None),
|
||||
}
|
|
@ -18,13 +18,21 @@ OpenERP Server
|
|||
06_misc
|
||||
09_deployment
|
||||
|
||||
OpenERP Command
|
||||
'''''''''''''''
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
openerp-command.rst
|
||||
adding-command.rst
|
||||
|
||||
OpenERP Server API
|
||||
''''''''''''''''''
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
api_core.rst
|
||||
api_models.rst
|
||||
|
||||
Concepts
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
.. _openerp-command:
|
||||
|
||||
OpenERP Command
|
||||
===============
|
||||
|
||||
The ``oe`` script provides a set of command-line tools around the OpenERP
|
||||
framework.
|
||||
|
||||
Using OpenERP Command
|
||||
---------------------
|
||||
|
||||
In contrast to the previous ``openerp-server`` script, ``oe`` defines a few
|
||||
sub-commands, each with its own set of flags and options. You can get some
|
||||
information for any of them with
|
||||
|
||||
::
|
||||
|
||||
> oe <sub-command> --help
|
||||
|
||||
For instance::
|
||||
|
||||
> oe run-tests --help
|
||||
|
||||
Some ``oe`` options can be provided via environment variables. For instance::
|
||||
|
||||
> export OPENERP_DATABASE=trunk
|
||||
> export OPENERP_HOST=127.0.0.1
|
||||
> export OPENERP_PORT=8069
|
||||
|
||||
Depending on your needs, you can group all of the above in one single script;
|
||||
for instance here is a, say, ``test-trunk-view-validation.sh`` file::
|
||||
|
||||
COMMAND_REPO=/home/thu/repos/command/trunk/
|
||||
SERVER_REPO=/home/thu/repos/server/trunk
|
||||
|
||||
export PYTHONPATH=$SERVER_REPO:$COMMAND_REPO
|
||||
export PATH=$SERVER_REPO:$COMMAND_REPO:$PATH
|
||||
export OPENERP_DATABASE=trunk
|
||||
export OPENERP_HOST=127.0.0.1
|
||||
export OPENERP_PORT=8069
|
||||
|
||||
# The -d ignored is actually needed by `oe` even though `test_view_validation`
|
||||
# itself does not need it.
|
||||
oe run-tests -d ignored -m openerp.test_view_validation
|
||||
|
||||
Adding new commands
|
||||
-------------------
|
||||
|
||||
See the :doc:`adding-command` page.
|
||||
|
||||
Bash completion
|
||||
---------------
|
||||
|
||||
A preliminary ``oe-bash-completion`` file is provided. After sourcing it,
|
||||
|
||||
::
|
||||
|
||||
> . oe-bash-completion
|
||||
|
||||
completion (using the TAB character) in Bash should be working.
|
|
@ -0,0 +1,5 @@
|
|||
#! /usr/bin/env python2
|
||||
|
||||
if __name__ == '__main__':
|
||||
import openerpcommand.main
|
||||
openerpcommand.main.run()
|
|
@ -0,0 +1,89 @@
|
|||
_oe()
|
||||
{
|
||||
local cur prev opts
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
|
||||
cmd="${COMP_WORDS[0]}"
|
||||
subcmd=""
|
||||
if [[ ${COMP_CWORD} > 0 ]] ; then
|
||||
subcmd="${COMP_WORDS[1]}"
|
||||
fi
|
||||
|
||||
# oe
|
||||
|
||||
opts="initialize model read run-tests scaffold update \
|
||||
call open show consume-nothing consume-memory leak-memory \
|
||||
consume-cpu bench-read bench-fields-view-get bench-dummy bench-login \
|
||||
bench-sale-mrp --help"
|
||||
|
||||
if [[ ${prev} == oe && ${cur} != -* ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
||||
return 0
|
||||
fi
|
||||
|
||||
# oe call
|
||||
|
||||
opts="--database --user --password --host --port --help"
|
||||
|
||||
if [[ ${subcmd} == call ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
||||
return 0
|
||||
fi
|
||||
|
||||
# oe initialize
|
||||
|
||||
opts="--database --addons --all-modules --exclude --no-create --help"
|
||||
|
||||
if [[ ${subcmd} == initialize ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
||||
return 0
|
||||
fi
|
||||
|
||||
# oe model
|
||||
|
||||
opts="--database --model --field --verbose --help"
|
||||
|
||||
if [[ ${subcmd} == model ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
||||
return 0
|
||||
fi
|
||||
|
||||
# oe read
|
||||
|
||||
opts="--database --model --id --field --verbose --short --help"
|
||||
|
||||
if [[ ${subcmd} == read ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
||||
return 0
|
||||
fi
|
||||
|
||||
# oe run-tests
|
||||
|
||||
opts="--database --addons --module --dry-run --help"
|
||||
|
||||
if [[ ${subcmd} == run-tests ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
||||
return 0
|
||||
fi
|
||||
|
||||
# oe scaffold
|
||||
|
||||
opts="--help"
|
||||
|
||||
if [[ ${subcmd} == scaffold ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
||||
return 0
|
||||
fi
|
||||
|
||||
# fallback for unimplemented completion
|
||||
|
||||
opts="--help"
|
||||
|
||||
if [[ true ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
complete -F _oe oe
|
File diff suppressed because it is too large
Load Diff
|
@ -136,12 +136,13 @@ class ir_cron(osv.osv):
|
|||
except Exception, e:
|
||||
self._handle_callback_exception(cr, uid, model_name, method_name, args, job_id, e)
|
||||
|
||||
def _process_job(self, cr, job):
|
||||
def _process_job(self, job_cr, job, cron_cr):
|
||||
""" Run a given job taking care of the repetition.
|
||||
|
||||
The cursor has a lock on the job (aquired by _acquire_job()).
|
||||
|
||||
:param job_cr: cursor to use to execute the job, safe to commit/rollback
|
||||
:param job: job to be run (as a dictionary).
|
||||
:param cron_cr: cursor holding lock on the cron job row, to use to update the next exec date,
|
||||
must not be committed/rolled back!
|
||||
"""
|
||||
try:
|
||||
now = datetime.now()
|
||||
|
@ -153,19 +154,19 @@ class ir_cron(osv.osv):
|
|||
if numbercall > 0:
|
||||
numbercall -= 1
|
||||
if not ok or job['doall']:
|
||||
self._callback(cr, job['user_id'], job['model'], job['function'], job['args'], job['id'])
|
||||
self._callback(job_cr, job['user_id'], job['model'], job['function'], job['args'], job['id'])
|
||||
if numbercall:
|
||||
nextcall += _intervalTypes[job['interval_type']](job['interval_number'])
|
||||
ok = True
|
||||
addsql = ''
|
||||
if not numbercall:
|
||||
addsql = ', active=False'
|
||||
cr.execute("UPDATE ir_cron SET nextcall=%s, numbercall=%s"+addsql+" WHERE id=%s",
|
||||
cron_cr.execute("UPDATE ir_cron SET nextcall=%s, numbercall=%s"+addsql+" WHERE id=%s",
|
||||
(nextcall.strftime(DEFAULT_SERVER_DATETIME_FORMAT), numbercall, job['id']))
|
||||
|
||||
finally:
|
||||
cr.commit()
|
||||
cr.close()
|
||||
job_cr.commit()
|
||||
cron_cr.commit()
|
||||
|
||||
@classmethod
|
||||
def _acquire_job(cls, db_name):
|
||||
|
@ -181,44 +182,14 @@ class ir_cron(osv.osv):
|
|||
"""
|
||||
db = openerp.sql_db.db_connect(db_name)
|
||||
cr = db.cursor()
|
||||
jobs = []
|
||||
try:
|
||||
# Careful to compare timestamps with 'UTC' - everything is UTC as of v6.1.
|
||||
cr.execute("""SELECT * FROM ir_cron
|
||||
WHERE numbercall != 0
|
||||
AND active AND nextcall <= (now() at time zone 'UTC')
|
||||
ORDER BY priority""")
|
||||
for job in cr.dictfetchall():
|
||||
task_cr = db.cursor()
|
||||
try:
|
||||
# Try to grab an exclusive lock on the job row from within the task transaction
|
||||
acquired_lock = False
|
||||
task_cr.execute("""SELECT *
|
||||
FROM ir_cron
|
||||
WHERE id=%s
|
||||
FOR UPDATE NOWAIT""",
|
||||
(job['id'],), log_exceptions=False)
|
||||
acquired_lock = True
|
||||
except psycopg2.OperationalError, e:
|
||||
if e.pgcode == '55P03':
|
||||
# Class 55: Object not in prerequisite state; 55P03: lock_not_available
|
||||
_logger.debug('Another process/thread is already busy executing job `%s`, skipping it.', job['name'])
|
||||
continue
|
||||
else:
|
||||
# Unexpected OperationalError
|
||||
raise
|
||||
finally:
|
||||
if not acquired_lock:
|
||||
# we're exiting due to an exception while acquiring the lot
|
||||
task_cr.close()
|
||||
|
||||
# Got the lock on the job row, run its code
|
||||
_logger.debug('Starting job `%s`.', job['name'])
|
||||
openerp.modules.registry.RegistryManager.check_registry_signaling(db_name)
|
||||
registry = openerp.pooler.get_pool(db_name)
|
||||
registry[cls._name]._process_job(task_cr, job)
|
||||
openerp.modules.registry.RegistryManager.signal_caches_change(db_name)
|
||||
return True
|
||||
|
||||
jobs = cr.dictfetchall()
|
||||
except psycopg2.ProgrammingError, e:
|
||||
if e.pgcode == '42P01':
|
||||
# Class 42 — Syntax Error or Access Rule Violation; 42P01: undefined_table
|
||||
|
@ -228,12 +199,43 @@ class ir_cron(osv.osv):
|
|||
raise
|
||||
except Exception:
|
||||
_logger.warning('Exception in cron:', exc_info=True)
|
||||
|
||||
finally:
|
||||
cr.commit()
|
||||
cr.close()
|
||||
|
||||
return False
|
||||
for job in jobs:
|
||||
lock_cr = db.cursor()
|
||||
try:
|
||||
# Try to grab an exclusive lock on the job row from within the task transaction
|
||||
lock_cr.execute("""SELECT *
|
||||
FROM ir_cron
|
||||
WHERE id=%s
|
||||
FOR UPDATE NOWAIT""",
|
||||
(job['id'],), log_exceptions=False)
|
||||
|
||||
# Got the lock on the job row, run its code
|
||||
_logger.debug('Starting job `%s`.', job['name'])
|
||||
job_cr = db.cursor()
|
||||
try:
|
||||
openerp.modules.registry.RegistryManager.check_registry_signaling(db_name)
|
||||
registry = openerp.pooler.get_pool(db_name)
|
||||
registry[cls._name]._process_job(job_cr, job, lock_cr)
|
||||
openerp.modules.registry.RegistryManager.signal_caches_change(db_name)
|
||||
except Exception:
|
||||
_logger.exception('Unexpected exception while processing cron job %r', job)
|
||||
finally:
|
||||
job_cr.close()
|
||||
|
||||
except psycopg2.OperationalError, e:
|
||||
if e.pgcode == '55P03':
|
||||
# Class 55: Object not in prerequisite state; 55P03: lock_not_available
|
||||
_logger.debug('Another process/thread is already busy executing job `%s`, skipping it.', job['name'])
|
||||
continue
|
||||
else:
|
||||
# Unexpected OperationalError
|
||||
raise
|
||||
finally:
|
||||
# we're exiting due to an exception while acquiring the lock
|
||||
lock_cr.close()
|
||||
|
||||
def _try_lock(self, cr, uid, ids, context=None):
|
||||
"""Try to grab a dummy exclusive write-lock to the rows with the given ids,
|
||||
|
|
|
@ -199,7 +199,7 @@ class ir_model(osv.osv):
|
|||
|
||||
def instanciate(self, cr, user, model, context=None):
|
||||
class x_custom_model(osv.osv):
|
||||
pass
|
||||
_custom = True
|
||||
x_custom_model._name = model
|
||||
x_custom_model._module = False
|
||||
a = x_custom_model.create_instance(self.pool, cr)
|
||||
|
|
|
@ -177,7 +177,6 @@
|
|||
<field name="fax"/>
|
||||
<field name="email" widget="email"/>
|
||||
<field name="title" domain="[('domain', '=', 'contact')]"
|
||||
groups="base.group_no_one"
|
||||
options='{"no_open": True}' attrs="{'invisible': [('is_company','=', True)]}" />
|
||||
</group>
|
||||
</group>
|
||||
|
|
|
@ -36,6 +36,11 @@ import time
|
|||
import types
|
||||
from pprint import pformat
|
||||
|
||||
try:
|
||||
import psutil
|
||||
except ImportError:
|
||||
psutil = None
|
||||
|
||||
# TODO modules that import netsvc only for things from loglevels must be changed to use loglevels.
|
||||
from loglevels import *
|
||||
import tools
|
||||
|
@ -273,6 +278,9 @@ def dispatch_rpc(service_name, method, params):
|
|||
rpc_response_flag = rpc_response.isEnabledFor(logging.DEBUG)
|
||||
if rpc_request_flag or rpc_response_flag:
|
||||
start_time = time.time()
|
||||
start_rss, start_vms = 0, 0
|
||||
if psutil:
|
||||
start_rss, start_vms = psutil.Process(os.getpid()).get_memory_info()
|
||||
if rpc_request and rpc_response_flag:
|
||||
log(rpc_request,logging.DEBUG,'%s.%s'%(service_name,method), replace_request_password(params))
|
||||
|
||||
|
@ -282,10 +290,14 @@ def dispatch_rpc(service_name, method, params):
|
|||
|
||||
if rpc_request_flag or rpc_response_flag:
|
||||
end_time = time.time()
|
||||
end_rss, end_vms = 0, 0
|
||||
if psutil:
|
||||
end_rss, end_vms = psutil.Process(os.getpid()).get_memory_info()
|
||||
logline = '%s.%s time:%.3fs mem: %sk -> %sk (diff: %sk)' % (service_name, method, end_time - start_time, start_vms / 1024, end_vms / 1024, (end_vms - start_vms)/1024)
|
||||
if rpc_response_flag:
|
||||
log(rpc_response,logging.DEBUG,'%s.%s time:%.3fs '%(service_name,method,end_time - start_time), result)
|
||||
log(rpc_response,logging.DEBUG, logline, result)
|
||||
else:
|
||||
log(rpc_request,logging.DEBUG,'%s.%s time:%.3fs '%(service_name,method,end_time - start_time), replace_request_password(params), depth=1)
|
||||
log(rpc_request,logging.DEBUG, logline, replace_request_password(params), depth=1)
|
||||
|
||||
return result
|
||||
except openerp.exceptions.AccessError:
|
||||
|
|
|
@ -629,7 +629,8 @@ class MetaModel(type):
|
|||
self._module = module_name
|
||||
|
||||
# Remember which models to instanciate for this module.
|
||||
self.module_to_models.setdefault(self._module, []).append(self)
|
||||
if not self._custom:
|
||||
self.module_to_models.setdefault(self._module, []).append(self)
|
||||
|
||||
|
||||
# Definition of log access columns, automatically added to models if
|
||||
|
@ -666,6 +667,7 @@ class BaseModel(object):
|
|||
_name = None
|
||||
_columns = {}
|
||||
_constraints = []
|
||||
_custom = False
|
||||
_defaults = {}
|
||||
_rec_name = None
|
||||
_parent_name = 'parent_id'
|
||||
|
@ -942,7 +944,8 @@ class BaseModel(object):
|
|||
# managed by the metaclass.
|
||||
module_model_list = MetaModel.module_to_models.setdefault(cls._module, [])
|
||||
if cls not in module_model_list:
|
||||
module_model_list.append(cls)
|
||||
if not cls._custom:
|
||||
module_model_list.append(cls)
|
||||
|
||||
# Since we don't return an instance here, the __init__
|
||||
# method won't be called.
|
||||
|
|
|
@ -30,7 +30,7 @@ RELEASE_LEVELS_DISPLAY = {ALPHA: ALPHA,
|
|||
# properly comparable using normal operarors, for example:
|
||||
# (6,1,0,'beta',0) < (6,1,0,'candidate',1) < (6,1,0,'candidate',2)
|
||||
# (6,1,0,'candidate',2) < (6,1,0,'final',0) < (6,1,2,'final',0)
|
||||
version_info = (7, 0, 0, ALPHA, 0)
|
||||
version_info = (7, 0, 0, FINAL, 0)
|
||||
version = '.'.join(map(str, version_info[:2])) + RELEASE_LEVELS_DISPLAY[version_info[3]] + str(version_info[4] or '')
|
||||
serie = major_version = '.'.join(map(str, version_info[:2]))
|
||||
|
||||
|
|
|
@ -617,9 +617,7 @@ class report_sxw(report_rml, preprocess.report):
|
|||
create_doc = self.generators[mime_type]
|
||||
odt = etree.tostring(create_doc(rml_dom, rml_parser.localcontext),
|
||||
encoding='utf-8', xml_declaration=True)
|
||||
sxw_z = zipfile.ZipFile(sxw_io, mode='a')
|
||||
sxw_z.writestr('content.xml', odt)
|
||||
sxw_z.writestr('meta.xml', meta)
|
||||
sxw_contents = {'content.xml':odt, 'meta.xml':meta}
|
||||
|
||||
if report_xml.header:
|
||||
#Add corporate header/footer
|
||||
|
@ -638,12 +636,25 @@ class report_sxw(report_rml, preprocess.report):
|
|||
rml_parser._add_header(odt)
|
||||
odt = etree.tostring(odt, encoding='utf-8',
|
||||
xml_declaration=True)
|
||||
sxw_z.writestr('styles.xml', odt)
|
||||
sxw_contents['styles.xml'] = odt
|
||||
finally:
|
||||
rml_file.close()
|
||||
sxw_z.close()
|
||||
final_op = sxw_io.getvalue()
|
||||
|
||||
#created empty zip writing sxw contents to avoid duplication
|
||||
sxw_out = StringIO.StringIO()
|
||||
sxw_out_zip = zipfile.ZipFile(sxw_out, mode='w')
|
||||
sxw_template_zip = zipfile.ZipFile (sxw_io, 'r')
|
||||
for item in sxw_template_zip.infolist():
|
||||
if item.filename not in sxw_contents:
|
||||
buffer = sxw_template_zip.read(item.filename)
|
||||
sxw_out_zip.writestr(item.filename, buffer)
|
||||
for item_filename, buffer in sxw_contents.iteritems():
|
||||
sxw_out_zip.writestr(item_filename, buffer)
|
||||
sxw_template_zip.close()
|
||||
sxw_out_zip.close()
|
||||
final_op = sxw_out.getvalue()
|
||||
sxw_io.close()
|
||||
sxw_out.close()
|
||||
return final_op, mime_type
|
||||
|
||||
def create_single_html2html(self, cr, uid, ids, data, report_xml, context=None):
|
||||
|
|
|
@ -379,12 +379,17 @@ class WorkerCron(Worker):
|
|||
time.sleep(interval)
|
||||
|
||||
def process_work(self):
|
||||
rpc_request = logging.getLogger('openerp.netsvc.rpc.request')
|
||||
rpc_request_flag = rpc_request.isEnabledFor(logging.DEBUG)
|
||||
_logger.debug("WorkerCron (%s) polling for jobs", self.pid)
|
||||
if config['db_name']:
|
||||
db_names = config['db_name'].split(',')
|
||||
else:
|
||||
db_names = openerp.netsvc.ExportService._services['db'].exp_list(True)
|
||||
for db_name in db_names:
|
||||
if rpc_request_flag:
|
||||
start_time = time.time()
|
||||
start_rss, start_vms = psutil.Process(os.getpid()).get_memory_info()
|
||||
while True:
|
||||
# acquired = openerp.addons.base.ir.ir_cron.ir_cron._acquire_job(db_name)
|
||||
# TODO why isnt openerp.addons.base defined ?
|
||||
|
@ -395,7 +400,12 @@ class WorkerCron(Worker):
|
|||
# dont keep cursors in multi database mode
|
||||
if len(db_names) > 1:
|
||||
openerp.sql_db.close_db(db_name)
|
||||
# TODO Each job should be considered as one request instead of each db
|
||||
if rpc_request_flag:
|
||||
end_time = time.time()
|
||||
end_rss, end_vms = psutil.Process(os.getpid()).get_memory_info()
|
||||
logline = '%s time:%.3fs mem: %sk -> %sk (diff: %sk)' % (db_name, end_time - start_time, start_vms / 1024, end_vms / 1024, (end_vms - start_vms)/1024)
|
||||
_logger.debug("WorkerCron (%s) %s", self.pid, logline)
|
||||
# TODO Each job should be considered as one request instead of each run
|
||||
self.request_count += 1
|
||||
|
||||
def start(self):
|
||||
|
|
|
@ -43,6 +43,47 @@ test12</font></div><div><font color="#1f1f1f" face="monospace" size="2"><br></fo
|
|||
<a href="javascript:alert('malicious code')">test link</a>
|
||||
"""
|
||||
|
||||
EDI_LIKE_HTML_SOURCE = """<div style="font-family: 'Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: #FFF; ">
|
||||
<p>Hello ${object.partner_id.name},</p>
|
||||
<p>A new invoice is available for you: </p>
|
||||
<p style="border-left: 1px solid #8e0000; margin-left: 30px;">
|
||||
<strong>REFERENCES</strong><br />
|
||||
Invoice number: <strong>${object.number}</strong><br />
|
||||
Invoice total: <strong>${object.amount_total} ${object.currency_id.name}</strong><br />
|
||||
Invoice date: ${object.date_invoice}<br />
|
||||
Order reference: ${object.origin}<br />
|
||||
Your contact: <a href="mailto:${object.user_id.email or ''}?subject=Invoice%20${object.number}">${object.user_id.name}</a>
|
||||
</p>
|
||||
<br/>
|
||||
<p>It is also possible to directly pay with Paypal:</p>
|
||||
<a style="margin-left: 120px;" href="${object.paypal_url}">
|
||||
<img class="oe_edi_paypal_button" src="https://www.paypal.com/en_US/i/btn/btn_paynowCC_LG.gif"/>
|
||||
</a>
|
||||
<br/>
|
||||
<p>If you have any question, do not hesitate to contact us.</p>
|
||||
<p>Thank you for choosing ${object.company_id.name or 'us'}!</p>
|
||||
<br/>
|
||||
<br/>
|
||||
<div style="width: 375px; margin: 0px; padding: 0px; background-color: #8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; background-repeat: repeat no-repeat;">
|
||||
<h3 style="margin: 0px; padding: 2px 14px; font-size: 12px; color: #DDD;">
|
||||
<strong style="text-transform:uppercase;">${object.company_id.name}</strong></h3>
|
||||
</div>
|
||||
<div style="width: 347px; margin: 0px; padding: 5px 14px; line-height: 16px; background-color: #F2F2F2;">
|
||||
<span style="color: #222; margin-bottom: 5px; display: block; ">
|
||||
${object.company_id.street}<br/>
|
||||
${object.company_id.street2}<br/>
|
||||
${object.company_id.zip} ${object.company_id.city}<br/>
|
||||
${object.company_id.state_id and ('%s, ' % object.company_id.state_id.name) or ''} ${object.company_id.country_id.name or ''}<br/>
|
||||
</span>
|
||||
<div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">
|
||||
Phone: ${object.company_id.phone}
|
||||
</div>
|
||||
<div>
|
||||
Web : <a href="${object.company_id.website}">${object.company_id.website}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div></body></html>"""
|
||||
|
||||
TEXT_MAIL1 = """I contact you about our meeting for tomorrow. Here is the schedule I propose:
|
||||
9 AM: brainstorming about our new amazing business app</span></li>
|
||||
9.45 AM: summary
|
||||
|
@ -126,23 +167,85 @@ bert.tartopoils@miam.miam
|
|||
class TestSanitizer(unittest2.TestCase):
|
||||
""" Test the html sanitizer that filters html to remove unwanted attributes """
|
||||
|
||||
def test_simple(self):
|
||||
x = "yop"
|
||||
self.assertEqual(x, html_sanitize(x))
|
||||
def test_basic_sanitizer(self):
|
||||
cases = [
|
||||
("yop", "<p>yop</p>"), # simple
|
||||
("lala<p>yop</p>xxx", "<div><p>lala</p><p>yop</p>xxx</div>"), # trailing text
|
||||
("Merci à l'intérêt pour notre produit.nous vous contacterons bientôt. Merci",
|
||||
u"<p>Merci à l'intérêt pour notre produit.nous vous contacterons bientôt. Merci</p>"), # unicode
|
||||
]
|
||||
for content, expected in cases:
|
||||
html = html_sanitize(content)
|
||||
self.assertEqual(html, expected, 'html_sanitize is broken')
|
||||
|
||||
def test_trailing_text(self):
|
||||
x = 'lala<p>yop</p>xxx'
|
||||
self.assertEqual(x, html_sanitize(x))
|
||||
def test_evil_malicious_code(self):
|
||||
# taken from https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Tests
|
||||
cases = [
|
||||
("<IMG SRC=javascript:alert('XSS')>"), # no quotes and semicolons
|
||||
("<IMG SRC=javascript:alert('XSS')>"), # UTF-8 Unicode encoding
|
||||
("<IMG SRC=javascript:alert('XSS')>"), # hex encoding
|
||||
("<IMG SRC=\"jav
ascript:alert('XSS');\">"), # embedded carriage return
|
||||
("<IMG SRC=\"jav
ascript:alert('XSS');\">"), # embedded newline
|
||||
("<IMG SRC=\"jav ascript:alert('XSS');\">"), # embedded tab
|
||||
("<IMG SRC=\"jav	ascript:alert('XSS');\">"), # embedded encoded tab
|
||||
("<IMG SRC=\"  javascript:alert('XSS');\">"), # spaces and meta-characters
|
||||
("<IMG SRC=\"javascript:alert('XSS')\""), # half-open html
|
||||
("<IMG \"\"\"><SCRIPT>alert(\"XSS\")</SCRIPT>\">"), # malformed tag
|
||||
("<SCRIPT/XSS SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT>"), # non-alpha-non-digits
|
||||
("<SCRIPT/SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT>"), # non-alpha-non-digits
|
||||
("<<SCRIPT>alert(\"XSS\");//<</SCRIPT>"), # extraneous open brackets
|
||||
("<SCRIPT SRC=http://ha.ckers.org/xss.js?< B >"), # non-closing script tags
|
||||
("<INPUT TYPE=\"IMAGE\" SRC=\"javascript:alert('XSS');\">"), # input image
|
||||
("<BODY BACKGROUND=\"javascript:alert('XSS')\">"), # body image
|
||||
("<IMG DYNSRC=\"javascript:alert('XSS')\">"), # img dynsrc
|
||||
("<IMG LOWSRC=\"javascript:alert('XSS')\">"), # img lowsrc
|
||||
("<TABLE BACKGROUND=\"javascript:alert('XSS')\">"), # table
|
||||
("<TABLE><TD BACKGROUND=\"javascript:alert('XSS')\">"), # td
|
||||
("<DIV STYLE=\"background-image: url(javascript:alert('XSS'))\">"), # div background
|
||||
("<DIV STYLE=\"background-image:\0075\0072\006C\0028'\006a\0061\0076\0061\0073\0063\0072\0069\0070\0074\003a\0061\006c\0065\0072\0074\0028.1027\0058.1053\0053\0027\0029'\0029\">"), # div background with unicoded exploit
|
||||
("<DIV STYLE=\"background-image: url(javascript:alert('XSS'))\">"), # div background + extra characters
|
||||
("<IMG SRC='vbscript:msgbox(\"XSS\")'>"), # VBscrip in an image
|
||||
("<BODY ONLOAD=alert('XSS')>"), # event handler
|
||||
("<BR SIZE=\"&{alert('XSS')}\>"), # & javascript includes
|
||||
("<LINK REL=\"stylesheet\" HREF=\"javascript:alert('XSS');\">"), # style sheet
|
||||
("<LINK REL=\"stylesheet\" HREF=\"http://ha.ckers.org/xss.css\">"), # remote style sheet
|
||||
("<STYLE>@import'http://ha.ckers.org/xss.css';</STYLE>"), # remote style sheet 2
|
||||
("<META HTTP-EQUIV=\"Link\" Content=\"<http://ha.ckers.org/xss.css>; REL=stylesheet\">"), # remote style sheet 3
|
||||
("<STYLE>BODY{-moz-binding:url(\"http://ha.ckers.org/xssmoz.xml#xss\")}</STYLE>"), # remote style sheet 4
|
||||
("<IMG STYLE=\"xss:expr/*XSS*/ession(alert('XSS'))\">"), # style attribute using a comment to break up expression
|
||||
("""<!--[if gte IE 4]>
|
||||
<SCRIPT>alert('XSS');</SCRIPT>
|
||||
<![endif]-->"""), # down-level hidden block
|
||||
]
|
||||
for content in cases:
|
||||
html = html_sanitize(content)
|
||||
self.assertNotIn('javascript', html, 'html_sanitize did not remove a malicious javascript')
|
||||
self.assertTrue('ha.ckers.org' not in html or 'http://ha.ckers.org/xss.css' in html, 'html_sanitize did not remove a malicious code in %s (%s)' % (content, html))
|
||||
|
||||
def test_html(self):
|
||||
sanitized_html = html_sanitize(HTML_SOURCE)
|
||||
for tag in ['<font>', '<div>', '<b>', '<i>', '<u>', '<strike>', '<li>', '<blockquote>', '<a href']:
|
||||
for tag in ['<div', '<b', '<i', '<u', '<strike', '<li', '<blockquote', '<a href']:
|
||||
self.assertIn(tag, sanitized_html, 'html_sanitize stripped too much of original html')
|
||||
for attr in ['style', 'javascript']:
|
||||
for attr in ['javascript']:
|
||||
self.assertNotIn(attr, sanitized_html, 'html_sanitize did not remove enough unwanted attributes')
|
||||
|
||||
def test_unicode(self):
|
||||
html_sanitize("Merci à l'intérêt pour notre produit.nous vous contacterons bientôt. Merci")
|
||||
emails =[("Charles <charles.bidule@truc.fr>", "Charles <charles.bidule@truc.fr>"),
|
||||
("Dupuis <'tr/-: ${dupuis#$'@truc.baz.fr>", "Dupuis <'tr/-: ${dupuis#$'@truc.baz.fr>"),
|
||||
("Technical <service/technical+2@open.com>", "Technical <service/technical+2@open.com>"),
|
||||
("Div nico <div-nico@open.com>", "Div nico <div-nico@open.com>")]
|
||||
for email in emails:
|
||||
self.assertIn(email[1], html_sanitize(email[0]), 'html_sanitize stripped emails of original html')
|
||||
|
||||
|
||||
def test_edi_source(self):
|
||||
html = html_sanitize(EDI_LIKE_HTML_SOURCE)
|
||||
self.assertIn('div style="font-family: \'Lucica Grande\', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: #FFF;', html,
|
||||
'html_sanitize removed valid style attribute')
|
||||
self.assertIn('<span style="color: #222; margin-bottom: 5px; display: block; ">', html,
|
||||
'html_sanitize removed valid style attribute')
|
||||
self.assertIn('img class="oe_edi_paypal_button" src="https://www.paypal.com/en_US/i/btn/btn_paynowCC_LG.gif"', html,
|
||||
'html_sanitize removed valid img')
|
||||
self.assertNotIn('</body></html>', html, 'html_sanitize did not remove extra closing tags')
|
||||
|
||||
|
||||
class TestCleaner(unittest2.TestCase):
|
||||
|
@ -181,6 +284,7 @@ class TestCleaner(unittest2.TestCase):
|
|||
new_html = html_email_clean(u'<?xml version="1.0" encoding="iso-8859-1"?>\n<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"\n "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">\n <head>\n <title>404 - Not Found</title>\n </head>\n <body>\n <h1>404 - Not Found</h1>\n </body>\n</html>\n')
|
||||
self.assertNotIn('encoding', new_html, 'html_email_cleaner did not remove correctly encoding attributes')
|
||||
|
||||
|
||||
class TestHtmlTools(unittest2.TestCase):
|
||||
""" Test some of our generic utility functions about html """
|
||||
|
||||
|
|
|
@ -284,10 +284,10 @@ class configmanager(object):
|
|||
help="Specify the number of workers, 0 disable prefork mode.",
|
||||
type="int")
|
||||
group.add_option("--limit-memory-soft", dest="limit_memory_soft", my_default=640 * 1024 * 1024,
|
||||
help="Maximum allowed virtual memory per worker, when reached the worker be reset after the current request (default 640M).",
|
||||
help="Maximum allowed virtual memory per worker, when reached the worker be reset after the current request (default 671088640 aka 640MB).",
|
||||
type="int")
|
||||
group.add_option("--limit-memory-hard", dest="limit_memory_hard", my_default=768 * 1024 * 1024,
|
||||
help="Maximum allowed virtual memory per worker, when reached, any memory allocation will fail (default 768M).",
|
||||
help="Maximum allowed virtual memory per worker, when reached, any memory allocation will fail (default 805306368 aka 768MB).",
|
||||
type="int")
|
||||
group.add_option("--limit-time-cpu", dest="limit_time_cpu", my_default=60,
|
||||
help="Maximum allowed CPU time per request (default 60).",
|
||||
|
|
|
@ -23,8 +23,8 @@ from lxml import etree
|
|||
import cgi
|
||||
import logging
|
||||
import lxml.html
|
||||
import lxml.html.clean as clean
|
||||
import openerp.pooler as pooler
|
||||
import operator
|
||||
import random
|
||||
import re
|
||||
import socket
|
||||
|
@ -40,71 +40,32 @@ _logger = logging.getLogger(__name__)
|
|||
# HTML Sanitizer
|
||||
#----------------------------------------------------------
|
||||
|
||||
tags_to_kill = ["script", "head", "meta", "title", "link", "style", "frame", "iframe", "base", "object", "embed"]
|
||||
tags_to_remove = ['html', 'body', 'font']
|
||||
|
||||
|
||||
def html_sanitize(src):
|
||||
if not src:
|
||||
return src
|
||||
src = ustr(src, errors='replace')
|
||||
root = lxml.html.fromstring(u"<div>%s</div>" % src)
|
||||
result = handle_element(root)
|
||||
res = []
|
||||
for element in children(result[0]):
|
||||
if isinstance(element, basestring):
|
||||
res.append(element)
|
||||
else:
|
||||
element.tail = ""
|
||||
res.append(lxml.html.tostring(element))
|
||||
return ''.join(res)
|
||||
|
||||
# FIXME: shouldn't this be a whitelist rather than a blacklist?!
|
||||
to_remove = set(["script", "head", "meta", "title", "link", "img"])
|
||||
to_unwrap = set(["html", "body"])
|
||||
|
||||
javascript_regex = re.compile(r"^\s*javascript\s*:.*$", re.IGNORECASE)
|
||||
|
||||
def handle_a(el, new):
|
||||
href = el.get("href", "#")
|
||||
if javascript_regex.search(href):
|
||||
href = "#"
|
||||
new.set("href", href)
|
||||
|
||||
special = {
|
||||
"a": handle_a,
|
||||
}
|
||||
|
||||
def handle_element(element):
|
||||
if isinstance(element, basestring):
|
||||
return [element]
|
||||
if element.tag in to_remove:
|
||||
return []
|
||||
if element.tag in to_unwrap:
|
||||
return reduce(operator.add, [handle_element(x) for x in children(element)])
|
||||
result = lxml.html.fromstring("<%s />" % element.tag)
|
||||
for c in children(element):
|
||||
append_to(handle_element(c), result)
|
||||
if element.tag in special:
|
||||
special[element.tag](element, result)
|
||||
return [result]
|
||||
|
||||
def children(node):
|
||||
res = []
|
||||
if node.text is not None:
|
||||
res.append(node.text)
|
||||
for child_node in node.getchildren():
|
||||
res.append(child_node)
|
||||
if child_node.tail is not None:
|
||||
res.append(child_node.tail)
|
||||
return res
|
||||
|
||||
def append_to(elements, dest_node):
|
||||
for element in elements:
|
||||
if isinstance(element, basestring):
|
||||
children = dest_node.getchildren()
|
||||
if len(children) == 0:
|
||||
dest_node.text = element
|
||||
else:
|
||||
children[-1].tail = element
|
||||
else:
|
||||
dest_node.append(element)
|
||||
# html encode email tags
|
||||
part = re.compile(r"(<[^<>]+@[^<>]+>)", re.IGNORECASE | re.DOTALL)
|
||||
src = part.sub(lambda m: cgi.escape(m.group(1)), src)
|
||||
|
||||
# some corner cases make the parser crash (such as <SCRIPT/XSS SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT> in test_mail)
|
||||
try:
|
||||
cleaner = clean.Cleaner(page_structure=True, style=False, safe_attrs_only=False, forms=False, kill_tags=tags_to_kill, remove_tags=tags_to_remove)
|
||||
cleaned = cleaner.clean_html(src)
|
||||
except TypeError, e:
|
||||
# lxml.clean version < 2.3.1 does not have a kill_tags attribute
|
||||
# to remove in 2014
|
||||
cleaner = clean.Cleaner(page_structure=True, style=False, safe_attrs_only=False, forms=False, remove_tags=tags_to_kill+tags_to_remove)
|
||||
cleaned = cleaner.clean_html(src)
|
||||
except:
|
||||
_logger.warning('html_sanitize failed to parse %s' % (src))
|
||||
cleaned = '<p>Impossible to parse</p>'
|
||||
return cleaned
|
||||
|
||||
|
||||
#----------------------------------------------------------
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import argparse
|
||||
import textwrap
|
||||
|
||||
from .call import Call
|
||||
from .client import Open, Show, ConsumeNothing, ConsumeMemory, LeakMemory, ConsumeCPU
|
||||
from .benchmarks import Bench, BenchRead, BenchFieldsViewGet, BenchDummy, BenchLogin
|
||||
from .bench_sale_mrp import BenchSaleMrp
|
||||
from . import common
|
||||
|
||||
from . import conf # Not really server-side (in the `for` below).
|
||||
from . import drop
|
||||
from . import initialize
|
||||
from . import model
|
||||
from . import module
|
||||
from . import read
|
||||
from . import run_tests
|
||||
from . import scaffold
|
||||
from . import uninstall
|
||||
from . import update
|
||||
|
||||
command_list_server = (conf, drop, initialize, model, module, read, run_tests,
|
||||
scaffold, uninstall, update, )
|
||||
|
||||
command_list_client = (Call, Open, Show, ConsumeNothing, ConsumeMemory,
|
||||
LeakMemory, ConsumeCPU, Bench, BenchRead,
|
||||
BenchFieldsViewGet, BenchDummy, BenchLogin,
|
||||
BenchSaleMrp, )
|
||||
|
||||
def main_parser():
|
||||
parser = argparse.ArgumentParser(
|
||||
usage=argparse.SUPPRESS,
|
||||
description=textwrap.fill(textwrap.dedent("""\
|
||||
OpenERP Command provides a set of command-line tools around
|
||||
the OpenERP framework: openobject-server. All the tools are
|
||||
sub-commands of a single oe executable.""")),
|
||||
epilog="""Use <command> --help to get information about the command.""",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
description = []
|
||||
for x in command_list_server:
|
||||
description.append(x.__name__[len(__package__)+1:])
|
||||
if x.__doc__:
|
||||
description.extend([
|
||||
":\n",
|
||||
textwrap.fill(str(x.__doc__).strip(),
|
||||
subsequent_indent=' ',
|
||||
initial_indent=' '),
|
||||
])
|
||||
description.append("\n\n")
|
||||
subparsers = parser.add_subparsers(
|
||||
title="Available commands",
|
||||
help=argparse.SUPPRESS,
|
||||
description="".join(description[:-1]),
|
||||
)
|
||||
# Server-side commands.
|
||||
for x in command_list_server:
|
||||
x.add_parser(subparsers)
|
||||
# Client-side commands. TODO one per .py file.
|
||||
for x in command_list_client:
|
||||
x(subparsers)
|
||||
return parser
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Nothing here, the module provides only data.
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -0,0 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'bench_sale_mrp',
|
||||
'version': '0.1',
|
||||
'category': 'Benchmarks',
|
||||
'description': """Prepare some data to run a benchmark.""",
|
||||
'author': 'OpenERP SA',
|
||||
'maintainer': 'OpenERP SA',
|
||||
'website': 'http://www.openerp.com',
|
||||
'depends': ['base', 'sale_mrp'],
|
||||
'data': ['data.yml'],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -0,0 +1,41 @@
|
|||
-
|
||||
This is a subset of `sale_mrp/test/sale_mrp.yml`.
|
||||
-
|
||||
I define a product category `Mobile Products Sellable`.
|
||||
-
|
||||
!record {model: product.category, id: my_product_category_0}:
|
||||
name: Mobile Products Sellable
|
||||
-
|
||||
I define a product `Slider Mobile`
|
||||
-
|
||||
!record {model: product.product, id: my_slider_mobile_0}:
|
||||
categ_id: my_product_category_0
|
||||
cost_method: standard
|
||||
list_price: 200.0
|
||||
mes_type: fixed
|
||||
name: Slider Mobile
|
||||
procure_method: make_to_order
|
||||
seller_delay: '1'
|
||||
seller_ids:
|
||||
- delay: 1
|
||||
name: base.res_partner_agrolait
|
||||
min_qty: 2.0
|
||||
qty: 5.0
|
||||
standard_price: 189.0
|
||||
supply_method: produce
|
||||
type: product
|
||||
uom_id: product.product_uom_unit
|
||||
uom_po_id: product.product_uom_unit
|
||||
-
|
||||
I create a Bill of Material for the `Slider Mobile` product.
|
||||
-
|
||||
!record {model: mrp.bom, id: mrp_bom_slidermobile0}:
|
||||
company_id: base.main_company
|
||||
name: Slider Mobile
|
||||
product_efficiency: 1.0
|
||||
product_id: my_slider_mobile_0
|
||||
product_qty: 1.0
|
||||
product_uom: product.product_uom_unit
|
||||
product_uos_qty: 0.0
|
||||
sequence: 0.0
|
||||
type: normal
|
|
@ -0,0 +1,68 @@
|
|||
"""
|
||||
Benchmark based on the `sale_mrp` addons (in `sale_mrp/test/sale_mrp.yml`).
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from .benchmarks import Bench
|
||||
|
||||
class BenchSaleMrp(Bench):
|
||||
"""\
|
||||
Similar to `sale_mrp/test/sale_mrp.yml`.
|
||||
|
||||
This benchmarks the OpenERP server `sale_mrp` module by creating and
|
||||
confirming a sale order. As it creates data in the server, it is necessary
|
||||
to ensure unique names for the newly created data. You can use the --seed
|
||||
argument to give a lower bound to those names. (The number of generated
|
||||
names is --jobs * --samples.)
|
||||
"""
|
||||
|
||||
command_name = 'bench-sale-mrp'
|
||||
bench_name = '`sale_mrp/test/sale_mrp.yml`'
|
||||
|
||||
def measure_once(self, i):
|
||||
if self.worker >= 0:
|
||||
i = int(self.args.seed) + i + (self.worker * int(self.args.samples))
|
||||
else:
|
||||
i = int(self.args.seed) + i
|
||||
|
||||
# Resolve a few external-ids (this has little impact on the running
|
||||
# time of the whole method).
|
||||
product_uom_unit = self.execute('ir.model.data', 'get_object_reference', 'product', 'product_uom_unit')[1]
|
||||
my_slider_mobile_0 = self.execute('ir.model.data', 'get_object_reference', 'bench_sale_mrp', 'my_slider_mobile_0')[1]
|
||||
res_partner_4 = self.execute('ir.model.data', 'get_object_reference', 'base', 'res_partner_4')[1]
|
||||
res_partner_address_7 = self.execute('ir.model.data', 'get_object_reference', 'base', 'res_partner_address_7')[1]
|
||||
list0 = self.execute('ir.model.data', 'get_object_reference', 'product', 'list0')[1]
|
||||
shop = self.execute('ir.model.data', 'get_object_reference', 'sale', 'shop')[1]
|
||||
|
||||
# Create a sale order for the product `Slider Mobile`.
|
||||
data = {
|
||||
'client_order_ref': 'ref_xxx_' + str(i).rjust(6, '0'),
|
||||
'date_order': time.strftime('%Y-%m-%d'),
|
||||
'invoice_quantity': 'order',
|
||||
'name': 'sale_order_ref_xxx_' + str(i).rjust(6, '0'),
|
||||
'order_line': [(0, 0, {
|
||||
'name': 'Slider Mobile',
|
||||
'price_unit': 2,
|
||||
'product_uom': product_uom_unit,
|
||||
'product_uom_qty': 5.0,
|
||||
'state': 'draft',
|
||||
'delay': 7.0,
|
||||
'product_id': my_slider_mobile_0,
|
||||
'product_uos_qty': 5,
|
||||
'type': 'make_to_order',
|
||||
})],
|
||||
'order_policy': 'manual',
|
||||
'partner_id': res_partner_4,
|
||||
'partner_invoice_id': res_partner_address_7,
|
||||
'partner_order_id': res_partner_address_7,
|
||||
'partner_shipping_id': res_partner_address_7,
|
||||
'picking_policy': 'direct',
|
||||
'pricelist_id': list0,
|
||||
'shop_id': shop,
|
||||
}
|
||||
sale_order_id = self.execute('sale.order', 'create', data, {})
|
||||
|
||||
# Confirm the sale order.
|
||||
self.object_proxy.exec_workflow(self.database, self.uid, self.password, 'sale.order', 'order_confirm', sale_order_id, {})
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
"""
|
||||
Define a base class for client-side benchmarking.
|
||||
"""
|
||||
import hashlib
|
||||
import multiprocessing
|
||||
import sys
|
||||
import time
|
||||
|
||||
from .client import Client
|
||||
|
||||
class Bench(Client):
|
||||
"""
|
||||
Base class for concurrent benchmarks. The measure_once() method must be
|
||||
overriden.
|
||||
|
||||
Each sub-benchmark will be run in its own process then a report is done
|
||||
with all the results (shared with the main process using a
|
||||
`multiprocessing.Array`).
|
||||
"""
|
||||
|
||||
def __init__(self, subparsers=None):
|
||||
super(Bench, self).__init__(subparsers)
|
||||
self.parser.add_argument('-n', '--samples', metavar='INT',
|
||||
default=100, help='number of measurements to take')
|
||||
# TODO if -n <int>s is given (instead of -n <int>), run the
|
||||
# benchmark for <int> seconds and return the number of iterations.
|
||||
self.parser.add_argument('-o', '--output', metavar='PATH',
|
||||
required=True, help='path to save the generated report')
|
||||
self.parser.add_argument('--append', action='store_true',
|
||||
default=False, help='append the report to an existing file')
|
||||
self.parser.add_argument('-j', '--jobs', metavar='JOBS',
|
||||
default=1, help='number of concurrent workers')
|
||||
self.parser.add_argument('--seed', metavar='SEED',
|
||||
default=0, help='a value to ensure different runs can create unique data')
|
||||
self.worker = -1
|
||||
|
||||
def work(self, iarr=None):
|
||||
if iarr:
|
||||
# If an array is given, it means we are a worker process...
|
||||
self.work_slave(iarr)
|
||||
else:
|
||||
# ... else we are the main process and we will spawn workers,
|
||||
# passing them an array.
|
||||
self.work_master()
|
||||
|
||||
def work_master(self):
|
||||
N = int(self.args.samples)
|
||||
self.arrs = [(i, multiprocessing.Array('f', range(N)))
|
||||
for i in xrange(int(self.args.jobs))]
|
||||
ps = [multiprocessing.Process(target=self.run, args=(arr,))
|
||||
for arr in self.arrs]
|
||||
[p.start() for p in ps]
|
||||
[p.join() for p in ps]
|
||||
|
||||
self.report_html()
|
||||
|
||||
def work_slave(self, iarr):
|
||||
j, arr = iarr
|
||||
self.worker = j
|
||||
N = int(self.args.samples)
|
||||
total_t0 = time.time()
|
||||
for i in xrange(N):
|
||||
t0 = time.time()
|
||||
self.measure_once(i)
|
||||
t1 = time.time()
|
||||
arr[i] = t1 - t0
|
||||
print >> sys.stdout, '\r%s' % ('|' * (i * 60 / N)),
|
||||
print >> sys.stdout, '%s %s%%' % \
|
||||
(' ' * (60 - (i * 60 / N)), int(float(i+1)/N*100)),
|
||||
sys.stdout.flush()
|
||||
total_t1 = time.time()
|
||||
print '\nDone in %ss.' % (total_t1 - total_t0)
|
||||
|
||||
def report_html(self):
|
||||
series = []
|
||||
for arr in self.arrs:
|
||||
serie = """{
|
||||
data: %s,
|
||||
points: { show: true }
|
||||
}""" % ([[x, i] for i, x in enumerate(arr)],)
|
||||
series.append(serie)
|
||||
chart_id = hashlib.md5(" ".join(sys.argv)).hexdigest()
|
||||
HEADER = """<!doctype html>
|
||||
<title>Benchmarks</title>
|
||||
<meta charset=utf-8>
|
||||
<script type="text/javascript" src="js/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="js/jquery.flot.js"></script>
|
||||
"""
|
||||
|
||||
CONTENT = """<h1>%s</h1>
|
||||
%s
|
||||
<div id='chart_%s' style='width:400px;height:300px;'>...</div>
|
||||
<script type="text/javascript">
|
||||
$.plot($("#chart_%s"), [%s],
|
||||
{yaxis: { ticks: false }});
|
||||
</script>""" % (self.bench_name, ' '.join(sys.argv), chart_id, chart_id,
|
||||
','.join(series))
|
||||
if self.args.append:
|
||||
with open(self.args.output, 'a') as f:
|
||||
f.write(CONTENT,)
|
||||
else:
|
||||
with open(self.args.output, 'w') as f:
|
||||
f.write(HEADER + CONTENT,)
|
||||
|
||||
def measure_once(self, i):
|
||||
"""
|
||||
The `measure_once` method is called --jobs times. A `i` argument is
|
||||
supplied to allow to create unique values for each execution (e.g. to
|
||||
supply fresh identifiers to a `create` method.
|
||||
"""
|
||||
pass
|
||||
|
||||
class BenchRead(Bench):
|
||||
"""Read a record repeatedly."""
|
||||
|
||||
command_name = 'bench-read'
|
||||
bench_name = 'res.users.read(1)'
|
||||
|
||||
def __init__(self, subparsers=None):
|
||||
super(BenchRead, self).__init__(subparsers)
|
||||
self.parser.add_argument('-m', '--model', metavar='MODEL',
|
||||
required=True, help='the model')
|
||||
self.parser.add_argument('-i', '--id', metavar='RECORDID',
|
||||
required=True, help='the record id')
|
||||
|
||||
def measure_once(self, i):
|
||||
self.execute(self.args.model, 'read', [self.args.id], [])
|
||||
|
||||
class BenchFieldsViewGet(Bench):
|
||||
"""Read a record's fields and view architecture repeatedly."""
|
||||
|
||||
command_name = 'bench-view'
|
||||
bench_name = 'res.users.fields_view_get(1)'
|
||||
|
||||
def __init__(self, subparsers=None):
|
||||
super(BenchFieldsViewGet, self).__init__(subparsers)
|
||||
self.parser.add_argument('-m', '--model', metavar='MODEL',
|
||||
required=True, help='the model')
|
||||
self.parser.add_argument('-i', '--id', metavar='RECORDID',
|
||||
required=True, help='the record id')
|
||||
|
||||
def measure_once(self, i):
|
||||
self.execute(self.args.model, 'fields_view_get', self.args.id)
|
||||
|
||||
class BenchDummy(Bench):
|
||||
"""Dummy (call test.limits.model.consume_nothing())."""
|
||||
|
||||
command_name = 'bench-dummy'
|
||||
bench_name = 'test.limits.model.consume_nothing()'
|
||||
|
||||
def __init__(self, subparsers=None):
|
||||
super(BenchDummy, self).__init__(subparsers)
|
||||
self.parser.add_argument('-a', '--args', metavar='ARGS',
|
||||
default='', help='some arguments to serialize')
|
||||
|
||||
def measure_once(self, i):
|
||||
self.execute('test.limits.model', 'consume_nothing')
|
||||
|
||||
class BenchLogin(Bench):
|
||||
"""Login (update res_users.date)."""
|
||||
|
||||
command_name = 'bench-login'
|
||||
bench_name = 'res.users.login(1)'
|
||||
|
||||
def measure_once(self, i):
|
||||
self.common_proxy.login(self.database, self.user, self.password)
|
|
@ -0,0 +1,44 @@
|
|||
"""
|
||||
Call an arbitrary model's method.
|
||||
"""
|
||||
import ast
|
||||
import os
|
||||
import pprint
|
||||
import sys
|
||||
import time
|
||||
import xmlrpclib
|
||||
|
||||
import client
|
||||
|
||||
class Call(client.Client):
|
||||
"""\
|
||||
Call an arbitrary model's method.
|
||||
|
||||
Example:
|
||||
> oe call res.users.read '[1, 3]' '[]' -u 1 -p admin
|
||||
"""
|
||||
# TODO The above docstring is completely borked in the
|
||||
# --help message.
|
||||
|
||||
command_name = 'call'
|
||||
|
||||
def __init__(self, subparsers=None):
|
||||
super(Call, self).__init__(subparsers)
|
||||
self.parser.add_argument('call', metavar='MODEL.METHOD',
|
||||
help='the model and the method to call, using the '
|
||||
'<model>.<method> format.')
|
||||
self.parser.add_argument('args', metavar='ARGUMENT',
|
||||
nargs='+',
|
||||
help='the argument for the method call, must be '
|
||||
'`ast.literal_eval` compatible. Can be repeated.')
|
||||
|
||||
def work(self):
|
||||
try:
|
||||
model, method = self.args.call.rsplit('.', 1)
|
||||
except:
|
||||
print "Invalid syntax `%s` must have the form <model>.<method>."
|
||||
sys.exit(1)
|
||||
args = tuple(map(ast.literal_eval, self.args.args)) if self.args.args else ()
|
||||
x = self.execute(model, method, *args)
|
||||
pprint.pprint(x, indent=4)
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
"""
|
||||
Define a few common arguments for client-side command-line tools.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import xmlrpclib
|
||||
|
||||
import common
|
||||
|
||||
class Client(common.Command):
|
||||
"""
|
||||
Base class for XML-RPC command-line clients. It must be inherited and the
|
||||
work() method overriden.
|
||||
"""
|
||||
|
||||
def __init__(self, subparsers=None):
|
||||
super(Client, self).__init__(subparsers)
|
||||
required_or_default = common.required_or_default
|
||||
self.parser.add_argument('-H', '--host', metavar='HOST',
|
||||
**required_or_default('HOST', 'the server host'))
|
||||
self.parser.add_argument('-P', '--port', metavar='PORT',
|
||||
**required_or_default('PORT', 'the server port'))
|
||||
|
||||
def execute(self, *args):
|
||||
return self.object_proxy.execute(self.database, self.uid, self.password, *args)
|
||||
|
||||
def initialize(self):
|
||||
self.host = self.args.host
|
||||
self.port = int(self.args.port)
|
||||
self.database = self.args.database
|
||||
self.user = self.args.user
|
||||
self.password = self.args.password
|
||||
|
||||
self.url = 'http://%s:%d/xmlrpc/' % (self.host, self.port)
|
||||
self.common_proxy = xmlrpclib.ServerProxy(self.url + 'common')
|
||||
self.object_proxy = xmlrpclib.ServerProxy(self.url + 'object')
|
||||
|
||||
try:
|
||||
self.uid = int(self.user)
|
||||
except ValueError, e:
|
||||
self.uid = self.common_proxy.login(self.database, self.user, self.password)
|
||||
|
||||
def run(self, *args):
|
||||
self.initialize()
|
||||
self.work(*args)
|
||||
|
||||
def work(self, *args):
|
||||
pass
|
||||
|
||||
class Open(Client):
|
||||
"""Get the web client's URL to view a specific model."""
|
||||
|
||||
command_name = 'open'
|
||||
|
||||
def __init__(self, subparsers=None):
|
||||
super(Open, self).__init__(subparsers)
|
||||
self.parser.add_argument('-m', '--model', metavar='MODEL',
|
||||
required=True, help='the view type')
|
||||
self.parser.add_argument('-v', '--view-mode', metavar='VIEWMODE',
|
||||
default='tree', help='the view mode')
|
||||
|
||||
def work(self):
|
||||
ids = self.execute('ir.actions.act_window', 'search', [
|
||||
('res_model', '=', self.args.model),
|
||||
('view_mode', 'like', self.args.view_mode),
|
||||
])
|
||||
xs = self.execute('ir.actions.act_window', 'read', ids, [])
|
||||
for x in xs:
|
||||
print x['id'], x['name']
|
||||
d = {}
|
||||
d['host'] = self.host
|
||||
d['port'] = self.port
|
||||
d['action_id'] = x['id']
|
||||
print " http://%(host)s:%(port)s/web/webclient/home#action_id=%(action_id)s" % d
|
||||
|
||||
class Show(Client):
|
||||
"""Display a record."""
|
||||
|
||||
command_name = 'show'
|
||||
|
||||
def __init__(self, subparsers=None):
|
||||
super(Show, self).__init__(subparsers)
|
||||
self.parser.add_argument('-m', '--model', metavar='MODEL',
|
||||
required=True, help='the model')
|
||||
self.parser.add_argument('-i', '--id', metavar='RECORDID',
|
||||
required=True, help='the record id')
|
||||
|
||||
def work(self):
|
||||
xs = self.execute(self.args.model, 'read', [self.args.id], [])
|
||||
if xs:
|
||||
x = xs[0]
|
||||
print x['name']
|
||||
else:
|
||||
print "Record not found."
|
||||
|
||||
class ConsumeNothing(Client):
|
||||
"""Call test.limits.model.consume_nothing()."""
|
||||
|
||||
command_name = 'consume-nothing'
|
||||
|
||||
def work(self):
|
||||
xs = self.execute('test.limits.model', 'consume_nothing')
|
||||
|
||||
class ConsumeMemory(Client):
|
||||
"""Call test.limits.model.consume_memory()."""
|
||||
|
||||
command_name = 'consume-memory'
|
||||
|
||||
def __init__(self, subparsers=None):
|
||||
super(ConsumeMemory, self).__init__(subparsers)
|
||||
self.parser.add_argument('--size', metavar='SIZE',
|
||||
required=True, help='size of the list to allocate')
|
||||
|
||||
def work(self):
|
||||
xs = self.execute('test.limits.model', 'consume_memory', int(self.args.size))
|
||||
|
||||
class LeakMemory(ConsumeMemory):
|
||||
"""Call test.limits.model.leak_memory()."""
|
||||
|
||||
command_name = 'leak-memory'
|
||||
|
||||
def work(self):
|
||||
xs = self.execute('test.limits.model', 'leak_memory', int(self.args.size))
|
||||
|
||||
class ConsumeCPU(Client):
|
||||
"""Call test.limits.model.consume_cpu_time()."""
|
||||
|
||||
command_name = 'consume-cpu'
|
||||
|
||||
def __init__(self, subparsers=None):
|
||||
super(ConsumeCPU, self).__init__(subparsers)
|
||||
self.parser.add_argument('--seconds', metavar='INT',
|
||||
required=True, help='how much CPU time to consume')
|
||||
|
||||
def work(self):
|
||||
xs = self.execute('test.limits.model', 'consume_cpu_time', int(self.args.seconds))
|
|
@ -0,0 +1,88 @@
|
|||
"""
|
||||
Define a few common arguments for server-side command-line tools.
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
def add_addons_argument(parser):
|
||||
"""
|
||||
Add a common --addons argument to a parser.
|
||||
"""
|
||||
parser.add_argument('--addons', metavar='ADDONS',
|
||||
**required_or_default('ADDONS',
|
||||
'colon-separated list of paths to addons'))
|
||||
|
||||
def get_addons_from_paths(paths, exclude):
|
||||
"""
|
||||
Build a list of available modules from a list of addons paths.
|
||||
"""
|
||||
exclude = exclude or []
|
||||
module_names = []
|
||||
for p in paths:
|
||||
if os.path.exists(p):
|
||||
names = list(set(os.listdir(p)))
|
||||
names = filter(lambda a: not (a.startswith('.') or a in exclude), names)
|
||||
module_names.extend(names)
|
||||
else:
|
||||
print "The addons path `%s` doesn't exist." % p
|
||||
sys.exit(1)
|
||||
return module_names
|
||||
|
||||
def required_or_default(name, h):
|
||||
"""
|
||||
Helper to define `argparse` arguments. If the name is the environment,
|
||||
the argument is optional and draw its value from the environment if not
|
||||
supplied on the command-line. If it is not in the environment, make it
|
||||
a mandatory argument.
|
||||
"""
|
||||
if os.environ.get('OPENERP_' + name.upper()):
|
||||
d = {'default': os.environ['OPENERP_' + name.upper()]}
|
||||
else:
|
||||
d = {'required': True}
|
||||
d['help'] = h + '. The environment variable OPENERP_' + \
|
||||
name.upper() + ' can be used instead.'
|
||||
return d
|
||||
|
||||
class Command(object):
|
||||
"""
|
||||
Base class to create command-line tools. It must be inherited and the
|
||||
run() method overriden.
|
||||
"""
|
||||
|
||||
command_name = 'stand-alone'
|
||||
|
||||
def __init__(self, subparsers=None):
|
||||
if subparsers:
|
||||
self.parser = parser = subparsers.add_parser(self.command_name,
|
||||
description=self.__class__.__doc__)
|
||||
else:
|
||||
self.parser = parser = argparse.ArgumentParser(
|
||||
description=self.__class__.__doc__)
|
||||
|
||||
parser.add_argument('-d', '--database', metavar='DATABASE',
|
||||
**required_or_default('DATABASE', 'the database to connect to'))
|
||||
parser.add_argument('-u', '--user', metavar='USER',
|
||||
**required_or_default('USER', 'the user login or ID. When using '
|
||||
'RPC, providing an ID avoid the login() step'))
|
||||
parser.add_argument('-p', '--password', metavar='PASSWORD',
|
||||
**required_or_default('PASSWORD', 'the user password')) # TODO read it from the command line or from file.
|
||||
|
||||
parser.set_defaults(run=self.run_with_args)
|
||||
|
||||
def run_with_args(self, args):
|
||||
self.args = args
|
||||
self.run()
|
||||
|
||||
def run(self):
|
||||
print 'Stub Command.run().'
|
||||
|
||||
@classmethod
|
||||
def stand_alone(cls):
|
||||
"""
|
||||
A single Command object is a complete command-line program. See
|
||||
`openerp-command/stand-alone` for an example.
|
||||
"""
|
||||
command = cls()
|
||||
args = command.parser.parse_args()
|
||||
args.run(args)
|
|
@ -0,0 +1,25 @@
|
|||
"""
|
||||
Display the currently used configuration. The configuration for any
|
||||
sub-command is normally given by options. But some options can be specified
|
||||
using environment variables. This sub-command shows those variables.
|
||||
A `set` sub-command should be provided when the configuration is in a real
|
||||
configuration file instead of environment variables.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
def run(args):
|
||||
for x in ('database', 'addons', 'host', 'port'):
|
||||
x_ = ('openerp_' + x).upper()
|
||||
if x_ in os.environ:
|
||||
print '%s: %s' % (x, os.environ[x_])
|
||||
else:
|
||||
print '%s: <not set>' % (x, )
|
||||
os.environ['OPENERP_DATABASE'] = 'yeah'
|
||||
|
||||
def add_parser(subparsers):
|
||||
parser = subparsers.add_parser('conf',
|
||||
description='Display the currently used configuration.')
|
||||
|
||||
parser.set_defaults(run=run)
|
|
@ -0,0 +1,45 @@
|
|||
"""
|
||||
Drop a database.
|
||||
"""
|
||||
|
||||
import common
|
||||
|
||||
# TODO turn template1 in a parameter
|
||||
# This should be exposed from openerp (currently in
|
||||
# openerp/service/web_services.py).
|
||||
def drop_database(database_name):
|
||||
import openerp
|
||||
db = openerp.sql_db.db_connect('template1')
|
||||
cr = db.cursor()
|
||||
cr.autocommit(True) # avoid transaction block
|
||||
try:
|
||||
# TODO option for doing this.
|
||||
# Try to terminate all other connections that might prevent
|
||||
# dropping the database
|
||||
try:
|
||||
cr.execute("""SELECT pg_terminate_backend(procpid)
|
||||
FROM pg_stat_activity
|
||||
WHERE datname = %s AND
|
||||
procpid != pg_backend_pid()""",
|
||||
(database_name,))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
cr.execute('DROP DATABASE "%s"' % database_name)
|
||||
except Exception, e:
|
||||
print "Can't drop %s" % (database_name,)
|
||||
finally:
|
||||
cr.close()
|
||||
|
||||
def run(args):
|
||||
assert args.database
|
||||
drop_database(args.database)
|
||||
|
||||
def add_parser(subparsers):
|
||||
parser = subparsers.add_parser('drop',
|
||||
description='Drop a database.')
|
||||
parser.add_argument('-d', '--database', metavar='DATABASE',
|
||||
**common.required_or_default('DATABASE', 'the database to create'))
|
||||
|
||||
parser.set_defaults(run=run)
|
|
@ -0,0 +1,103 @@
|
|||
"""
|
||||
Install OpenERP on a new (by default) database.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
import common
|
||||
|
||||
def install_openerp(database_name, create_database_flag, module_names, install_demo_data):
|
||||
import openerp
|
||||
config = openerp.tools.config
|
||||
|
||||
if create_database_flag:
|
||||
create_database(database_name)
|
||||
|
||||
config['init'] = dict.fromkeys(module_names, 1)
|
||||
|
||||
# Install the import hook, to import openerp.addons.<module>.
|
||||
openerp.modules.module.initialize_sys_path()
|
||||
if hasattr(openerp.modules.loading, 'open_openerp_namespace'):
|
||||
openerp.modules.loading.open_openerp_namespace()
|
||||
|
||||
registry = openerp.modules.registry.RegistryManager.get(
|
||||
database_name, update_module=True, force_demo=install_demo_data)
|
||||
|
||||
return registry
|
||||
|
||||
# TODO turn template1 in a parameter
|
||||
# This should be exposed from openerp (currently in
|
||||
# openerp/service/web_services.py).
|
||||
def create_database(database_name):
|
||||
import openerp
|
||||
db = openerp.sql_db.db_connect('template1')
|
||||
cr = db.cursor() # TODO `with db as cr:`
|
||||
try:
|
||||
cr.autocommit(True)
|
||||
cr.execute("""CREATE DATABASE "%s"
|
||||
ENCODING 'unicode' TEMPLATE "template1" """ \
|
||||
% (database_name,))
|
||||
finally:
|
||||
cr.close()
|
||||
|
||||
def run(args):
|
||||
assert args.database
|
||||
assert not (args.module and args.all_modules)
|
||||
|
||||
import openerp
|
||||
|
||||
config = openerp.tools.config
|
||||
|
||||
if args.tests:
|
||||
config['log_handler'] = [':TEST']
|
||||
config['test_enable'] = True
|
||||
config['without_demo'] = False
|
||||
else:
|
||||
config['log_handler'] = [':CRITICAL']
|
||||
config['test_enable'] = False
|
||||
config['without_demo'] = True
|
||||
|
||||
if args.addons:
|
||||
args.addons = args.addons.split(':')
|
||||
else:
|
||||
args.addons = []
|
||||
config['addons_path'] = ','.join(args.addons)
|
||||
|
||||
if args.all_modules:
|
||||
module_names = common.get_addons_from_paths(args.addons, args.exclude)
|
||||
elif args.module:
|
||||
module_names = args.module
|
||||
else:
|
||||
module_names = ['base']
|
||||
|
||||
openerp.netsvc.init_logger()
|
||||
registry = install_openerp(args.database, not args.no_create, module_names, not config['without_demo'])
|
||||
|
||||
# The `_assertion_report` attribute was added on the registry during the
|
||||
# OpenERP 7.0 development.
|
||||
if hasattr(registry, '_assertion_report'):
|
||||
sys.exit(1 if registry._assertion_report.failures else 0)
|
||||
|
||||
def add_parser(subparsers):
|
||||
parser = subparsers.add_parser('initialize',
|
||||
description='Create and initialize a new OpenERP database.')
|
||||
parser.add_argument('-d', '--database', metavar='DATABASE',
|
||||
**common.required_or_default('DATABASE', 'the database to create'))
|
||||
common.add_addons_argument(parser)
|
||||
parser.add_argument('--module', metavar='MODULE', action='append',
|
||||
help='specify a module to install'
|
||||
' (this option can be repeated)')
|
||||
parser.add_argument('--all-modules', action='store_true',
|
||||
help='install all visible modules (not compatible with --module)')
|
||||
parser.add_argument('--no-create', action='store_true',
|
||||
help='do not create the database, only initialize it')
|
||||
parser.add_argument('--exclude', metavar='MODULE', action='append',
|
||||
help='exclude a module from installation'
|
||||
' (this option can be repeated)')
|
||||
parser.add_argument('--tests', action='store_true',
|
||||
help='run the tests as modules are installed'
|
||||
' (use the `run-tests` command to choose specific'
|
||||
' tests to run against an existing database).'
|
||||
' Demo data are installed.')
|
||||
|
||||
parser.set_defaults(run=run)
|
|
@ -0,0 +1,7 @@
|
|||
import openerpcommand
|
||||
|
||||
def run():
|
||||
""" Main entry point for the openerp-command tool."""
|
||||
parser = openerpcommand.main_parser()
|
||||
args = parser.parse_args()
|
||||
args.run(args)
|
|
@ -0,0 +1,61 @@
|
|||
"""
|
||||
Display information about a given model.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
def run(args):
|
||||
assert args.database
|
||||
assert args.model
|
||||
import openerp
|
||||
openerp.tools.config['log_level'] = 100
|
||||
openerp.netsvc.init_logger()
|
||||
registry = openerp.modules.registry.RegistryManager.get(
|
||||
args.database, update_module=False)
|
||||
model = registry.get(args.model)
|
||||
longest_k = 1
|
||||
longest_string = 1
|
||||
columns = model._columns
|
||||
|
||||
if args.field and args.field not in columns:
|
||||
print "No such field."
|
||||
sys.exit(1)
|
||||
|
||||
if args.field:
|
||||
columns = { args.field: columns[args.field] }
|
||||
else:
|
||||
print "Fields (model `%s`, database `%s`):" % (args.model, args.database)
|
||||
|
||||
for k, v in columns.items():
|
||||
longest_k = len(k) if longest_k < len(k) else longest_k
|
||||
longest_string = len(v.string) \
|
||||
if longest_string < len(v.string) else longest_string
|
||||
for k, v in sorted(columns.items()):
|
||||
attr = []
|
||||
if v.required:
|
||||
attr.append("Required")
|
||||
if v.readonly:
|
||||
attr.append("Read-only")
|
||||
attr = '/'.join(attr)
|
||||
attr = '(' + attr + ')' if attr else attr
|
||||
if args.verbose:
|
||||
print v.string, '-- ' + k + ', ' + v._type, attr
|
||||
else:
|
||||
print k.ljust(longest_k + 2), v._type, attr
|
||||
if args.verbose and v.help:
|
||||
print textwrap.fill(v.help, initial_indent=' ', subsequent_indent=' ')
|
||||
|
||||
def add_parser(subparsers):
|
||||
parser = subparsers.add_parser('model',
|
||||
description='Display information about a given model for an existing database.')
|
||||
parser.add_argument('-d', '--database', metavar='DATABASE', required=True,
|
||||
help='the database to connect to')
|
||||
parser.add_argument('-m', '--model', metavar='MODEL', required=True,
|
||||
help='the model for which information should be displayed')
|
||||
parser.add_argument('-v', '--verbose', action='store_true',
|
||||
help='display more information')
|
||||
parser.add_argument('-f', '--field', metavar='FIELD',
|
||||
help='display information only for this particular field')
|
||||
|
||||
parser.set_defaults(run=run)
|
|
@ -0,0 +1,68 @@
|
|||
"""
|
||||
Show module information for a given database or from the file-system.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
from . import common
|
||||
|
||||
# TODO provide a --rpc flag to use XML-RPC (with a specific username) instead
|
||||
# of server-side library.
|
||||
def run(args):
|
||||
assert args.database
|
||||
import openerp
|
||||
|
||||
config = openerp.tools.config
|
||||
config['log_handler'] = [':CRITICAL']
|
||||
if args.addons:
|
||||
args.addons = args.addons.split(':')
|
||||
else:
|
||||
args.addons = []
|
||||
config['addons_path'] = ','.join(args.addons)
|
||||
openerp.netsvc.init_logger()
|
||||
|
||||
if args.filesystem:
|
||||
module_names = common.get_addons_from_paths(args.addons, [])
|
||||
print "Modules (addons path %s):" % (', '.join(args.addons),)
|
||||
for x in sorted(module_names):
|
||||
print x
|
||||
else:
|
||||
registry = openerp.modules.registry.RegistryManager.get(
|
||||
args.database, update_module=False)
|
||||
|
||||
xs = []
|
||||
ir_module_module = registry.get('ir.module.module')
|
||||
cr = registry.db.cursor() # TODO context manager
|
||||
try:
|
||||
ids = ir_module_module.search(cr, openerp.SUPERUSER_ID, [], {})
|
||||
xs = ir_module_module.read(cr, openerp.SUPERUSER_ID, ids, [], {})
|
||||
finally:
|
||||
cr.close()
|
||||
|
||||
if xs:
|
||||
print "Modules (database `%s`):" % (args.database,)
|
||||
for x in xs:
|
||||
if args.short:
|
||||
print '%3d %s' % (x['id'], x['name'])
|
||||
else:
|
||||
print '%3d %s %s' % (x['id'], x['name'], {'installed': '(installed)'}.get(x['state'], ''))
|
||||
else:
|
||||
print "No module found (database `%s`)." % (args.database,)
|
||||
|
||||
def add_parser(subparsers):
|
||||
parser = subparsers.add_parser('module',
|
||||
description='Display modules known from a given database or on file-system.')
|
||||
parser.add_argument('-d', '--database', metavar='DATABASE',
|
||||
**common.required_or_default('DATABASE', 'the database to modify'))
|
||||
common.add_addons_argument(parser)
|
||||
parser.add_argument('-m', '--module', metavar='MODULE', required=False,
|
||||
help='the module for which information should be shown')
|
||||
parser.add_argument('-v', '--verbose', action='store_true',
|
||||
help='display more information')
|
||||
parser.add_argument('--short', action='store_true',
|
||||
help='display less information')
|
||||
parser.add_argument('-f', '--filesystem', action='store_true',
|
||||
help='display module in the addons path, not in db')
|
||||
|
||||
parser.set_defaults(run=run)
|
|
@ -0,0 +1,60 @@
|
|||
"""
|
||||
Read a record.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
# TODO provide a --rpc flag to use XML-RPC (with a specific username) instead
|
||||
# of server-side library.
|
||||
def run(args):
|
||||
assert args.database
|
||||
assert args.model
|
||||
import openerp
|
||||
config = openerp.tools.config
|
||||
config['log_handler'] = [':CRITICAL']
|
||||
openerp.netsvc.init_logger()
|
||||
registry = openerp.modules.registry.RegistryManager.get(
|
||||
args.database, update_module=False)
|
||||
model = registry.get(args.model)
|
||||
cr = registry.db.cursor() # TODO context manager
|
||||
field_names = [args.field] if args.field else []
|
||||
if args.short:
|
||||
# ignore --field
|
||||
field_names = ['name']
|
||||
try:
|
||||
xs = model.read(cr, 1, args.id, field_names, {})
|
||||
finally:
|
||||
cr.close()
|
||||
|
||||
if xs:
|
||||
print "Records (model `%s`, database `%s`):" % (args.model, args.database)
|
||||
x = xs[0]
|
||||
if args.short:
|
||||
print str(x['id']) + '.', x['name']
|
||||
else:
|
||||
longest_k = 1
|
||||
for k, v in x.items():
|
||||
longest_k = len(k) if longest_k < len(k) else longest_k
|
||||
for k, v in sorted(x.items()):
|
||||
print (k + ':').ljust(longest_k + 2), v
|
||||
else:
|
||||
print "Record not found."
|
||||
|
||||
def add_parser(subparsers):
|
||||
parser = subparsers.add_parser('read',
|
||||
description='Display a record.')
|
||||
parser.add_argument('-d', '--database', metavar='DATABASE', required=True,
|
||||
help='the database to connect to')
|
||||
parser.add_argument('-m', '--model', metavar='MODEL', required=True,
|
||||
help='the model for which a record should be read')
|
||||
parser.add_argument('-i', '--id', metavar='RECORDID', required=True,
|
||||
help='the record id')
|
||||
parser.add_argument('-v', '--verbose', action='store_true',
|
||||
help='display more information')
|
||||
parser.add_argument('--short', action='store_true',
|
||||
help='display less information')
|
||||
parser.add_argument('-f', '--field', metavar='FIELD',
|
||||
help='display information only for this particular field')
|
||||
|
||||
parser.set_defaults(run=run)
|
|
@ -0,0 +1,202 @@
|
|||
"""
|
||||
Execute the unittest2 tests available in OpenERP addons.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
|
||||
import common
|
||||
|
||||
def get_test_modules(module, submodule, explode):
|
||||
"""
|
||||
Return a list of submodules containing tests.
|
||||
`submodule` can be:
|
||||
- None
|
||||
- the name of a submodule
|
||||
- '__fast_suite__'
|
||||
- '__sanity_checks__'
|
||||
"""
|
||||
# Turn command-line module, submodule into importable names.
|
||||
if module is None:
|
||||
pass
|
||||
elif module == 'openerp':
|
||||
module = 'openerp.tests'
|
||||
else:
|
||||
module = 'openerp.addons.' + module + '.tests'
|
||||
|
||||
# Try to import the module
|
||||
try:
|
||||
__import__(module)
|
||||
except Exception, e:
|
||||
if explode:
|
||||
print 'Can not `import %s`.' % module
|
||||
import logging
|
||||
logging.exception('')
|
||||
sys.exit(1)
|
||||
else:
|
||||
if str(e) == 'No module named tests':
|
||||
# It seems the module has no `tests` sub-module, no problem.
|
||||
pass
|
||||
else:
|
||||
print 'Can not `import %s`.' % module
|
||||
return []
|
||||
|
||||
# Discover available test sub-modules.
|
||||
m = sys.modules[module]
|
||||
submodule_names = sorted([x for x in dir(m) \
|
||||
if x.startswith('test_') and \
|
||||
isinstance(getattr(m, x), types.ModuleType)])
|
||||
submodules = [getattr(m, x) for x in submodule_names]
|
||||
|
||||
def show_submodules_and_exit():
|
||||
if submodule_names:
|
||||
print 'Available submodules are:'
|
||||
for x in submodule_names:
|
||||
print ' ', x
|
||||
sys.exit(1)
|
||||
|
||||
if submodule is None:
|
||||
# Use auto-discovered sub-modules.
|
||||
ms = submodules
|
||||
elif submodule == '__fast_suite__':
|
||||
# Obtain the explicit test sub-modules list.
|
||||
ms = getattr(sys.modules[module], 'fast_suite', None)
|
||||
# `suite` was used before the 6.1 release instead of `fast_suite`.
|
||||
ms = ms if ms else getattr(sys.modules[module], 'suite', None)
|
||||
if ms is None:
|
||||
if explode:
|
||||
print 'The module `%s` has no defined test suite.' % (module,)
|
||||
show_submodules_and_exit()
|
||||
else:
|
||||
ms = []
|
||||
elif submodule == '__sanity_checks__':
|
||||
ms = getattr(sys.modules[module], 'checks', None)
|
||||
if ms is None:
|
||||
if explode:
|
||||
print 'The module `%s` has no defined sanity checks.' % (module,)
|
||||
show_submodules_and_exit()
|
||||
else:
|
||||
ms = []
|
||||
else:
|
||||
# Pick the command-line-specified test sub-module.
|
||||
m = getattr(sys.modules[module], submodule, None)
|
||||
ms = [m]
|
||||
|
||||
if m is None:
|
||||
if explode:
|
||||
print 'The module `%s` has no submodule named `%s`.' % \
|
||||
(module, submodule)
|
||||
show_submodules_and_exit()
|
||||
else:
|
||||
ms = []
|
||||
|
||||
return ms
|
||||
|
||||
def run(args):
|
||||
import unittest2
|
||||
|
||||
import openerp
|
||||
|
||||
config = openerp.tools.config
|
||||
config['db_name'] = args.database
|
||||
if args.port:
|
||||
config['xmlrpc_port'] = int(args.port)
|
||||
config['admin_passwd'] = 'admin'
|
||||
config['db_password'] = 'a2aevl8w' # TODO from .openerpserverrc
|
||||
config['addons_path'] = args.addons.replace(':',',')
|
||||
if args.addons:
|
||||
args.addons = args.addons.split(':')
|
||||
else:
|
||||
args.addons = []
|
||||
if args.sanity_checks and args.fast_suite:
|
||||
print 'Only at most one of `--sanity-checks` and `--fast-suite` ' \
|
||||
'can be specified.'
|
||||
sys.exit(1)
|
||||
|
||||
import logging
|
||||
openerp.netsvc.init_alternative_logger()
|
||||
logging.getLogger('openerp').setLevel(logging.CRITICAL)
|
||||
|
||||
# Install the import hook, to import openerp.addons.<module>.
|
||||
openerp.modules.module.initialize_sys_path()
|
||||
openerp.modules.loading.open_openerp_namespace()
|
||||
|
||||
# Extract module, submodule from the command-line args.
|
||||
if args.module is None:
|
||||
module, submodule = None, None
|
||||
else:
|
||||
splitted = args.module.split('.')
|
||||
if len(splitted) == 1:
|
||||
module, submodule = splitted[0], None
|
||||
elif len(splitted) == 2:
|
||||
module, submodule = splitted
|
||||
else:
|
||||
print 'The `module` argument must have the form ' \
|
||||
'`module[.submodule]`.'
|
||||
sys.exit(1)
|
||||
|
||||
# Import the necessary modules and get the corresponding suite.
|
||||
if module is None:
|
||||
# TODO
|
||||
modules = common.get_addons_from_paths(args.addons, []) # TODO openerp.addons.base is not included ?
|
||||
test_modules = []
|
||||
for module in ['openerp'] + modules:
|
||||
if args.fast_suite:
|
||||
submodule = '__fast_suite__'
|
||||
if args.sanity_checks:
|
||||
submodule = '__sanity_checks__'
|
||||
test_modules.extend(get_test_modules(module,
|
||||
submodule, explode=False))
|
||||
else:
|
||||
if submodule and args.fast_suite:
|
||||
print "Submodule name `%s` given, ignoring `--fast-suite`." % (submodule,)
|
||||
if submodule and args.sanity_checks:
|
||||
print "Submodule name `%s` given, ignoring `--sanity-checks`." % (submodule,)
|
||||
if not submodule and args.fast_suite:
|
||||
submodule = '__fast_suite__'
|
||||
if not submodule and args.sanity_checks:
|
||||
submodule = '__sanity_checks__'
|
||||
test_modules = get_test_modules(module,
|
||||
submodule, explode=True)
|
||||
|
||||
# Run the test suite.
|
||||
if not args.dry_run:
|
||||
suite = unittest2.TestSuite()
|
||||
for test_module in test_modules:
|
||||
suite.addTests(unittest2.TestLoader().loadTestsFromModule(test_module))
|
||||
r = unittest2.TextTestRunner(verbosity=2).run(suite)
|
||||
if r.errors or r.failures:
|
||||
sys.exit(1)
|
||||
else:
|
||||
print 'Test modules:'
|
||||
for test_module in test_modules:
|
||||
print ' ', test_module.__name__
|
||||
|
||||
def add_parser(subparsers):
|
||||
parser = subparsers.add_parser('run-tests',
|
||||
description='Run the OpenERP server and/or addons tests.')
|
||||
parser.add_argument('-d', '--database', metavar='DATABASE', required=True,
|
||||
help='the database to test. Depending on the test suites, the '
|
||||
'database must already exist or not.')
|
||||
parser.add_argument('-p', '--port', metavar='PORT',
|
||||
help='the port used for WML-RPC tests')
|
||||
common.add_addons_argument(parser)
|
||||
parser.add_argument('-m', '--module', metavar='MODULE',
|
||||
default=None,
|
||||
help='the module to test in `module[.submodule]` notation. '
|
||||
'Use `openerp` for the core OpenERP tests. '
|
||||
'Leave empty to run every declared tests. '
|
||||
'Give a module but no submodule to run all the module\'s declared '
|
||||
'tests. If both the module and the submodule are given, '
|
||||
'the sub-module can be run even if it is not declared in the module.')
|
||||
parser.add_argument('--fast-suite', action='store_true',
|
||||
help='run only the tests explicitely declared in the fast suite (this '
|
||||
'makes sense only with the bare `module` notation or no module at '
|
||||
'all).')
|
||||
parser.add_argument('--sanity-checks', action='store_true',
|
||||
help='run only the sanity check tests')
|
||||
parser.add_argument('--dry-run', action='store_true',
|
||||
help='do not run the tests')
|
||||
|
||||
parser.set_defaults(run=run)
|
|
@ -0,0 +1,77 @@
|
|||
"""
|
||||
Generate an OpenERP module skeleton.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
def run(args):
|
||||
assert args.module
|
||||
module = args.module
|
||||
|
||||
if os.path.exists(module):
|
||||
print "The path `%s` already exists."
|
||||
sys.exit(1)
|
||||
|
||||
os.mkdir(module)
|
||||
os.mkdir(os.path.join(module, 'models'))
|
||||
with open(os.path.join(module, '__openerp__.py'), 'w') as h:
|
||||
h.write(MANIFEST)
|
||||
with open(os.path.join(module, '__init__.py'), 'w') as h:
|
||||
h.write(INIT_PY)
|
||||
with open(os.path.join(module, 'models', '__init__.py'), 'w') as h:
|
||||
h.write(MODELS_PY % (module,))
|
||||
|
||||
def add_parser(subparsers):
|
||||
parser = subparsers.add_parser('scaffold',
|
||||
description='Generate an OpenERP module skeleton.')
|
||||
parser.add_argument('module', metavar='MODULE',
|
||||
help='the name of the generated module')
|
||||
|
||||
parser.set_defaults(run=run)
|
||||
|
||||
MANIFEST = """\
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': '<Module name>',
|
||||
'version': '0.0',
|
||||
'category': '<Category>',
|
||||
'description': '''
|
||||
<Long description>
|
||||
''',
|
||||
'author': '<author>',
|
||||
'maintainer': '<maintainer>',
|
||||
'website': 'http://<website>',
|
||||
# Add any module that are necessary for this module to correctly work in
|
||||
# the `depends` list.
|
||||
'depends': ['base'],
|
||||
'data': [
|
||||
],
|
||||
'test': [
|
||||
],
|
||||
# Set to False if you want to prevent the module to be known by OpenERP
|
||||
# (and thus appearing in the list of modules).
|
||||
'installable': True,
|
||||
# Set to True if you want the module to be automatically whenever all its
|
||||
# dependencies are installed.
|
||||
'auto_install': False,
|
||||
}
|
||||
"""
|
||||
|
||||
INIT_PY = """\
|
||||
# -*- coding: utf-8 -*-
|
||||
import models
|
||||
"""
|
||||
|
||||
MODELS_PY = """\
|
||||
# -*- coding: utf-8 -*-
|
||||
import openerp
|
||||
|
||||
# Define a new model.
|
||||
class my_model(openerp.osv.osv.Model):
|
||||
|
||||
_name = '%s.my_model'
|
||||
|
||||
_columns = {
|
||||
}
|
||||
"""
|
|
@ -0,0 +1,67 @@
|
|||
"""
|
||||
Install OpenERP on a new (by default) database.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
import common
|
||||
|
||||
# TODO turn template1 in a parameter
|
||||
# This should be exposed from openerp (currently in
|
||||
# openerp/service/web_services.py).
|
||||
def create_database(database_name):
|
||||
import openerp
|
||||
db = openerp.sql_db.db_connect('template1')
|
||||
cr = db.cursor() # TODO `with db as cr:`
|
||||
try:
|
||||
cr.autocommit(True)
|
||||
cr.execute("""CREATE DATABASE "%s"
|
||||
ENCODING 'unicode' TEMPLATE "template1" """ \
|
||||
% (database_name,))
|
||||
finally:
|
||||
cr.close()
|
||||
|
||||
def run(args):
|
||||
assert args.database
|
||||
assert args.module
|
||||
|
||||
import openerp
|
||||
|
||||
config = openerp.tools.config
|
||||
config['log_handler'] = [':CRITICAL']
|
||||
if args.addons:
|
||||
args.addons = args.addons.split(':')
|
||||
else:
|
||||
args.addons = []
|
||||
config['addons_path'] = ','.join(args.addons)
|
||||
openerp.netsvc.init_logger()
|
||||
|
||||
# Install the import hook, to import openerp.addons.<module>.
|
||||
openerp.modules.module.initialize_sys_path()
|
||||
openerp.modules.loading.open_openerp_namespace()
|
||||
|
||||
registry = openerp.modules.registry.RegistryManager.get(
|
||||
args.database, update_module=False)
|
||||
|
||||
ir_module_module = registry.get('ir.module.module')
|
||||
cr = registry.db.cursor() # TODO context manager
|
||||
try:
|
||||
ids = ir_module_module.search(cr, openerp.SUPERUSER_ID, [('name', 'in', args.module), ('state', '=', 'installed')], {})
|
||||
if len(ids) == len(args.module):
|
||||
ir_module_module.button_immediate_uninstall(cr, openerp.SUPERUSER_ID, ids, {})
|
||||
else:
|
||||
print "At least one module not found (database `%s`)." % (args.database,)
|
||||
finally:
|
||||
cr.close()
|
||||
|
||||
def add_parser(subparsers):
|
||||
parser = subparsers.add_parser('uninstall',
|
||||
description='Uninstall some modules from an OpenERP database.')
|
||||
parser.add_argument('-d', '--database', metavar='DATABASE',
|
||||
**common.required_or_default('DATABASE', 'the database to modify'))
|
||||
common.add_addons_argument(parser)
|
||||
parser.add_argument('--module', metavar='MODULE', action='append',
|
||||
help='specify a module to uninstall'
|
||||
' (this option can be repeated)')
|
||||
|
||||
parser.set_defaults(run=run)
|
|
@ -0,0 +1,19 @@
|
|||
"""
|
||||
Update an existing OpenERP database.
|
||||
"""
|
||||
|
||||
def run(args):
|
||||
assert args.database
|
||||
import openerp
|
||||
config = openerp.tools.config
|
||||
config['update']['all'] = 1
|
||||
openerp.modules.registry.RegistryManager.get(
|
||||
args.database, update_module=True)
|
||||
|
||||
def add_parser(subparsers):
|
||||
parser = subparsers.add_parser('update',
|
||||
description='Update an existing OpenERP database.')
|
||||
parser.add_argument('-d', '--database', metavar='DATABASE', required=True,
|
||||
help='the database to update')
|
||||
|
||||
parser.set_defaults(run=run)
|
Loading…
Reference in New Issue