summaryrefslogtreecommitdiff
path: root/odoo
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-09-24 16:59:15 +0000
committerChristophe Simonis <chs@odoo.com>2019-09-24 17:45:37 +0000
commitd67b2483e5bfa5d8c1e6ef1a64fb933f5e080ad5 (patch)
tree25a7fd39ac993f3c04c5d8408beb628b3c9948ce /odoo
parent38d8b0526f0fccc32df5a11f5d086a3522ed5fcf (diff)
parent9ed4872ea03f28012118ed48a43d0800617a84f3 (diff)
[MERGE] forward port branch saas-12.4 up to 9ed4872ea03
closes odoo/odoo#37372 Signed-off-by: Christophe Simonis <chs@odoo.com>
Diffstat (limited to 'odoo')
-rw-r--r--odoo/addons/base/data/res_currency_data.xml2
-rw-r--r--odoo/addons/base/models/assetsbundle.py16
-rw-r--r--odoo/addons/base/models/ir_actions.py34
-rw-r--r--odoo/addons/base/models/ir_cron.py1
-rw-r--r--odoo/addons/base/models/res_config.py6
-rw-r--r--odoo/addons/base/models/res_users.py11
-rw-r--r--odoo/addons/base/tests/__init__.py1
-rw-r--r--odoo/addons/base/tests/test_non_regression.py32
-rw-r--r--odoo/addons/base/tests/test_user_has_group.py142
-rw-r--r--odoo/service/model.py1
-rw-r--r--odoo/service/server.py4
-rw-r--r--odoo/tools/misc.py23
12 files changed, 159 insertions, 114 deletions
diff --git a/odoo/addons/base/data/res_currency_data.xml b/odoo/addons/base/data/res_currency_data.xml
index 39e77487d3a..db5cb7a3467 100644
--- a/odoo/addons/base/data/res_currency_data.xml
+++ b/odoo/addons/base/data/res_currency_data.xml
@@ -869,7 +869,7 @@
<record id="KWD" model="res.currency">
<field name="name">KWD</field>
<field name="symbol"> د.ك </field>
- <field name="rounding">0.01</field>
+ <field name="rounding">0.001</field>
<field name="active" eval="False"/>
<field name="currency_unit_label">Dinar</field>
<field name="currency_subunit_label">Fils</field>
diff --git a/odoo/addons/base/models/assetsbundle.py b/odoo/addons/base/models/assetsbundle.py
index d1554594aa3..f5f344e6c5f 100644
--- a/odoo/addons/base/models/assetsbundle.py
+++ b/odoo/addons/base/models/assetsbundle.py
@@ -483,13 +483,15 @@ class AssetsBundle(object):
compiled = compiled.strip()
# Post process the produced css to add required vendor prefixes here
- compiled = re.sub(r'(appearance: (\w+);)', r'-webkit-appearance: \2; -moz-appearance: \2; \1', compiled);
-
- compiled = re.sub(r'(display: ((?:inline-)?)flex((?: ?!important)?);)', r'display: -webkit-\2box\3; display: -webkit-\2flex\3; \1', compiled) # For PhantomJS tests and wkhtmltopdf
- compiled = re.sub(r'(justify-content: flex-(\w+)((?: ?!important)?);)', r'-webkit-box-pack: \2\3; \1', compiled) # For wkhtmltopdf
- compiled = re.sub(r'(flex-flow: (\w+ \w+);)', r'-webkit-flex-flow: \2; \1', compiled) # For PhantomJS tests
- compiled = re.sub(r'(flex-direction: (column);)', r'-webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: \2; \1', compiled) # For wkhtmltopdf
- compiled = re.sub(r'(flex: ((\d)+ \d+ (?:\d+|auto));)', r'-webkit-box-flex: \3; -webkit-flex: \2; \1', compiled) # For PhantomJS tests and wkhtmltopdf
+ compiled = re.sub(r'(appearance: (\w+);)', r'-webkit-appearance: \2; -moz-appearance: \2; \1', compiled)
+
+ # Most of those are only useful for wkhtmltopdf (some for old PhantomJS)
+ compiled = re.sub(r'(display: ((?:inline-)?)flex((?: ?!important)?);)', r'display: -webkit-\2box\3; display: -webkit-\2flex\3; \1', compiled)
+ compiled = re.sub(r'(justify-content: flex-(\w+)((?: ?!important)?);)', r'-webkit-box-pack: \2\3; \1', compiled)
+ compiled = re.sub(r'(flex-flow: (\w+ \w+);)', r'-webkit-flex-flow: \2; \1', compiled)
+ compiled = re.sub(r'(flex-direction: (column);)', r'-webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: \2; \1', compiled)
+ compiled = re.sub(r'(flex-wrap: (\w+);)', r'-webkit-flex-wrap: \2; \1', compiled)
+ compiled = re.sub(r'(flex: ((\d)+ \d+ (?:\d+|auto));)', r'-webkit-box-flex: \3; -webkit-flex: \2; \1', compiled)
return compiled
diff --git a/odoo/addons/base/models/ir_actions.py b/odoo/addons/base/models/ir_actions.py
index 38d9ad6911f..5680c1f88e3 100644
--- a/odoo/addons/base/models/ir_actions.py
+++ b/odoo/addons/base/models/ir_actions.py
@@ -318,9 +318,7 @@ class IrActionsServer(models.Model):
The available actions are :
- 'Execute Python Code': a block of python code that will be executed
- - 'Run a Client Action': choose a client action to launch
- - 'Create or Copy a new Record': create a new record with new values, or
- copy an existing record in your database
+ - 'Create a new Record': create a new record with new values
- 'Write on a Record': update the values of a record
- 'Execute several actions': define an action that triggers several other
server actions
@@ -361,11 +359,12 @@ class IrActionsServer(models.Model):
default='object_write', required=True,
help="Type of server action. The following values are available:\n"
"- 'Execute Python Code': a block of python code that will be executed\n"
- "- 'Create or Copy a new Record': create a new record with new values, or copy an existing record in your database\n"
- "- 'Write on a Record': update the values of a record\n"
+ "- 'Create': create a new record with new values\n"
+ "- 'Update a Record': update the values of a record\n"
"- 'Execute several actions': define an action that triggers several other server actions\n"
- "- 'Add Followers': add followers to a record (available in Discuss)\n"
- "- 'Send Email': automatically send an email (available in email_template)")
+ "- 'Send Email': automatically send an email (Discuss)\n"
+ "- 'Add Followers': add followers to a record (Discuss)\n"
+ "- 'Create Next Activity': create an activity (Discuss)")
# Generic
sequence = fields.Integer(default=5,
help="When dealing with multiple actions, the execution order is "
@@ -443,15 +442,7 @@ class IrActionsServer(models.Model):
@api.model
def run_action_object_write(self, action, eval_context=None):
- """ Write server action.
-
- - 1. evaluate the value mapping
- - 2. depending on the write configuration:
-
- - `current`: id = active_id
- - `other`: id = from reference object
- - `expression`: id = from expression evaluation
- """
+ """Apply specified write changes to active_id."""
res = {}
for exp in action.fields_lines:
res[exp.col1.name] = exp.eval_value(eval_context=eval_context)[exp.id]
@@ -465,16 +456,9 @@ class IrActionsServer(models.Model):
@api.model
def run_action_object_create(self, action, eval_context=None):
- """ Create and Copy server action.
-
- - 1. evaluate the value mapping
- - 2. depending on the write configuration:
+ """Create specified model object with specified values.
- - `new`: new record in the base model
- - `copy_current`: copy the current record (id = active_id) + gives custom values
- - `new_other`: new record in target model
- - `copy_other`: copy the current record (id from reference object)
- + gives custom values
+ If applicable, link active_id.<self.link_field_id> to the new record.
"""
res = {}
for exp in action.fields_lines:
diff --git a/odoo/addons/base/models/ir_cron.py b/odoo/addons/base/models/ir_cron.py
index 3542b8adc2a..a9821e782f7 100644
--- a/odoo/addons/base/models/ir_cron.py
+++ b/odoo/addons/base/models/ir_cron.py
@@ -228,6 +228,7 @@ class ir_cron(models.Model):
try:
registry = odoo.registry(db_name)
registry[cls._name]._process_job(job_cr, job, lock_cr)
+ _logger.info('Job `%s` done.', job['cron_name'])
except Exception:
_logger.exception('Unexpected exception while processing cron job %r', job)
finally:
diff --git a/odoo/addons/base/models/res_config.py b/odoo/addons/base/models/res_config.py
index 3dca42a8d98..948134d9e17 100644
--- a/odoo/addons/base/models/res_config.py
+++ b/odoo/addons/base/models/res_config.py
@@ -517,19 +517,19 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin)
# Special case when value is the id of a deleted record, we do not want to
# block the settings screen
value = self.env[field.comodel_name].browse(int(value)).exists().id
- except ValueError:
+ except (ValueError, TypeError):
_logger.warning(WARNING_MESSAGE, value, field, icp)
value = False
elif field.type == 'integer':
try:
value = int(value)
- except ValueError:
+ except (ValueError, TypeError):
_logger.warning(WARNING_MESSAGE, value, field, icp)
value = 0
elif field.type == 'float':
try:
value = float(value)
- except ValueError:
+ except (ValueError, TypeError):
_logger.warning(WARNING_MESSAGE, value, field, icp)
value = 0.0
elif field.type == 'boolean':
diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py
index 66b0b2f3e49..dba2a76ffed 100644
--- a/odoo/addons/base/models/res_users.py
+++ b/odoo/addons/base/models/res_users.py
@@ -984,22 +984,19 @@ class UsersImplied(models.Model):
# complete 'groups_id' with implied groups
user = self.new(values)
gs = user.groups_id._origin
- group_public = self.env.ref('base.group_public', raise_if_not_found=False)
- group_portal = self.env.ref('base.group_portal', raise_if_not_found=False)
- if group_public and group_public in gs:
- gs = group_public
- elif group_portal and group_portal in gs:
- gs = group_portal
gs = gs | gs.trans_implied_ids
values['groups_id'] = type(self).groups_id.convert_to_write(gs, user)
return super(UsersImplied, self).create(vals_list)
def write(self, values):
+ users_before = self.filtered(lambda u: u.has_group('base.group_user'))
res = super(UsersImplied, self).write(values)
if values.get('groups_id'):
# add implied groups for all users
for user in self:
- if not user.has_group('base.group_user'):
+ if not user.has_group('base.group_user') and user in users_before:
+ # if we demoted a user, we strip him of all its previous privileges
+ # (but we should not do it if we are simply adding a technical group to a portal user)
vals = {'groups_id': [(5, 0, 0)] + values['groups_id']}
super(UsersImplied, user).write(vals)
gs = set(concat(g.trans_implied_ids for g in user.groups_id))
diff --git a/odoo/addons/base/tests/__init__.py b/odoo/addons/base/tests/__init__.py
index db7b07f81af..ca045e0c6bb 100644
--- a/odoo/addons/base/tests/__init__.py
+++ b/odoo/addons/base/tests/__init__.py
@@ -37,5 +37,4 @@ from . import test_res_partner
from . import test_res_partner_bank
from . import test_reports
from . import test_tests_tags
-from . import test_non_regression
from . import test_base_document_layout
diff --git a/odoo/addons/base/tests/test_non_regression.py b/odoo/addons/base/tests/test_non_regression.py
deleted file mode 100644
index 1af583c16fa..00000000000
--- a/odoo/addons/base/tests/test_non_regression.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
- Non-Regression Tests
-"""
-
-from odoo.tests.common import TransactionCase
-
-
-class TestNR(TransactionCase):
- def test_issue26036(self):
- # Coming from https://github.com/odoo/odoo/pull/26036
- U = self.env["res.users"]
- G = self.env["res.groups"]
- group_user = self.env.ref('base.group_user')
- group_no_one = self.env.ref('base.group_no_one')
-
- group_A = G.create({"name": "A"})
- group_AA = G.create({"name": "AA", "implied_ids": [(6, 0, [group_A.id])]})
- group_B = G.create({"name": "B"})
- group_BB = G.create({"name": "BB", "implied_ids": [(6, 0, [group_B.id])]})
- group_C = G.create({"name": "C"})
-
- user_a = U.create({"name": "a", "login": "a", "groups_id": [(6, 0, [group_AA.id, group_user.id])]})
- user_b = U.create({"name": "b", "login": "b", "groups_id": [(6, 0, [group_BB.id])]})
-
- self.assertEqual(user_a.groups_id, (group_AA + group_A + group_user + group_no_one))
-
- (user_a + user_b).write({"groups_id": [(4, group_C.id)]})
-
- self.assertEqual(user_a.groups_id, (group_AA + group_A + group_C + group_user + group_no_one))
- # As user_b is not an internal user, all its groups are removed
- self.assertEqual(user_b.groups_id, group_C)
diff --git a/odoo/addons/base/tests/test_user_has_group.py b/odoo/addons/base/tests/test_user_has_group.py
index 4f5b2bc61dd..f5ea69ea711 100644
--- a/odoo/addons/base/tests/test_user_has_group.py
+++ b/odoo/addons/base/tests/test_user_has_group.py
@@ -28,6 +28,8 @@ class TestHasGroup(TransactionCase):
self.grp_internal = self.env.ref(self.grp_internal_xml_id)
self.grp_portal_xml_id = 'base.group_portal'
self.grp_portal = self.env.ref(self.grp_portal_xml_id)
+ self.grp_public_xml_id = 'base.group_public'
+ self.grp_public = self.env.ref(self.grp_public_xml_id)
def test_env_uid(self):
Users = self.env['res.users'].with_user(self.test_user)
@@ -51,6 +53,10 @@ class TestHasGroup(TransactionCase):
)
def test_portal_creation(self):
+ """Here we check that portal user creation fails if it tries to create a user
+ who would also have group_user by implied_group.
+ Otherwise, it succeeds with the groups we asked for.
+ """
grp_public = self.env.ref('base.group_public')
grp_test_portal_xml_id = 'test_user_has_group.portal_implied_group'
grp_test_portal = self.env['res.groups']._load_records([
@@ -64,14 +70,16 @@ class TestHasGroup(TransactionCase):
dict(xml_id=grp_test_internal2_xml_id, values={'name': 'Test Group Internal 2'})
])
self.grp_portal.implied_ids = grp_test_portal
- grp_test_internal1.implied_ids = self.grp_internal
- grp_test_internal2.implied_ids = self.grp_internal
+
+ grp_test_internal1.implied_ids = False
+ grp_test_internal2.implied_ids = False
+
portal_user = self.env['res.users'].create({
'login': 'portalTest',
'name': 'Portal test',
'sel_groups_%s_%s_%s' % (self.grp_internal.id, self.grp_portal.id, grp_public.id): self.grp_portal.id,
'sel_groups_%s_%s' % (grp_test_internal1.id, grp_test_internal2.id): grp_test_internal2.id,
- })
+ })
self.assertTrue(
portal_user.has_group(self.grp_portal_xml_id),
@@ -81,45 +89,52 @@ class TestHasGroup(TransactionCase):
portal_user.has_group(grp_test_portal_xml_id),
"The portal user should belong to '%s'" % grp_test_portal_xml_id,
)
- self.assertFalse(
+ self.assertTrue(
portal_user.has_group(grp_test_internal2_xml_id),
- "The portal user should not belong to '%s'" % grp_test_internal2_xml_id
+ "The portal user should belong to '%s'" % grp_test_internal2_xml_id
)
self.assertFalse(
portal_user.has_group(self.grp_internal_xml_id),
"The portal user should not belong to '%s'" % self.grp_internal_xml_id
)
+ portal_user.unlink() # otherwise, badly modifying the implication would raise
+
+ grp_test_internal1.implied_ids = self.grp_internal
+ grp_test_internal2.implied_ids = self.grp_internal
+
+ with self.assertRaises(ValidationError): # current group implications forbid to create a portal user
+ portal_user = self.env['res.users'].create({
+ 'login': 'portalFail',
+ 'name': 'Portal fail',
+ 'sel_groups_%s_%s_%s' % (self.grp_internal.id, self.grp_portal.id, grp_public.id): self.grp_portal.id,
+ 'sel_groups_%s_%s' % (grp_test_internal1.id, grp_test_internal2.id): grp_test_internal2.id,
+ })
+
def test_portal_write(self):
- grp_remove_xml_id = 'test_portal_write.group_to_remove'
- grp_remove = self.env['res.groups']._load_records([
- dict(xml_id=grp_remove_xml_id, values={'name': 'Group to remove'})
- ])
+ """Check that adding a new group to a portal user works as expected,
+ except if it implies group_user/public, in chich case it should raise.
+ """
+ grp_test_portal = self.env["res.groups"].create({"name": "implied by portal"})
+ self.grp_portal.implied_ids = grp_test_portal
+
portal_user = self.env['res.users'].create({
'login': 'portalTest2',
'name': 'Portal test 2',
- 'groups_id': [(6, 0, [grp_remove.id])],
+ 'groups_id': [(6, 0, [self.grp_portal.id])],
})
- grp_test_portal_xml_id = 'test_portal_write.portal_implied_group'
- grp_test_portal = self.env['res.groups']._load_records([
- dict(xml_id=grp_test_portal_xml_id, values={'name': 'Test Group Portal'})
- ])
- self.grp_portal.implied_ids = grp_test_portal
- portal_user.write({'groups_id': [(4, self.grp_portal.id, 0)]})
- self.assertFalse(
- portal_user.has_group(grp_remove_xml_id),
- "The portal user should not belong to '%s'" % grp_remove_xml_id
- )
- self.assertTrue(
- portal_user.has_group(self.grp_portal_xml_id),
- "The portal user should belong to '%s'" % self.grp_portal_xml_id,
- )
- self.assertTrue(
- portal_user.has_group(grp_test_portal_xml_id),
- "The portal user should belong to '%s'" % grp_test_portal_xml_id,
+ self.assertEqual(
+ portal_user.groups_id, (self.grp_portal + grp_test_portal),
+ "The portal user should have the implied group.",
)
+ grp_fail = self.env["res.groups"].create(
+ {"name": "fail", "implied_ids": [(6, 0, [self.grp_internal.id])]})
+
+ with self.assertRaises(ValidationError):
+ portal_user.write({'groups_id': [(4, grp_fail.id)]})
+
def test_two_user_types(self):
#Create a user with two groups of user types kind (Internal and Portal)
grp_test = self.env['res.groups']._load_records([
@@ -161,3 +176,76 @@ class TestHasGroup(TransactionCase):
with self.assertRaises(ValidationError):
grp_test.write({'implied_ids': [(4, self.grp_portal.id)]})
+
+ def test_demote_user(self):
+ """When a user is demoted to the status of portal/public,
+ we should strip him of all his (previous) rights
+ """
+ group_0 = self.env.ref(self.group0) # the group to which test_user already belongs
+ group_U = self.env["res.groups"].create({"name": "U", "implied_ids": [(6, 0, [self.grp_internal.id])]})
+ self.grp_internal.implied_ids = False # only there to simplify the test by not having to care about its trans_implied_ids
+
+ self.test_user.write({'groups_id': [(4, group_U.id)]})
+ self.assertEqual(
+ self.test_user.groups_id, (group_0 + group_U + self.grp_internal),
+ "We should have our 2 groups and the implied user group",
+ )
+
+ # Now we demote him. The JS framework sends 3 and 4 commands,
+ # which is what we write here, but it should work even with a 5 command or whatever.
+ self.test_user.write({'groups_id': [
+ (3, self.grp_internal.id),
+ (3, self.grp_public.id),
+ (4, self.grp_portal.id),
+ ]})
+
+ # if we screw up the removing groups/adding the implied ids, we could end up in two situations:
+ # 1. we have a portal user with way too much rights (e.g. 'Contact Creation', which does not imply any other group)
+ # 2. because a group may be (transitively) implying group_user, then it would raise an exception
+ # so as a compromise we remove all groups when demoting a user
+ # (even technical display groups, e.g. TaxB2B, which could be re-added later)
+ self.assertEqual(
+ self.test_user.groups_id, (self.grp_portal),
+ "Here the portal group does not imply any other group, so we should only have this group.",
+ )
+
+ def test_implied_groups(self):
+ """ We check that the adding of implied ids works correctly for normal users and portal users.
+ In the second case, working normally means raising if a group implies to give 'group_user'
+ rights to a portal user.
+ """
+ U = self.env["res.users"]
+ G = self.env["res.groups"]
+ group_user = self.env.ref('base.group_user')
+ group_portal = self.env.ref('base.group_portal')
+ group_no_one = self.env.ref('base.group_no_one')
+
+ group_A = G.create({"name": "A"})
+ group_AA = G.create({"name": "AA", "implied_ids": [(6, 0, [group_A.id])]})
+ group_B = G.create({"name": "B"})
+ group_BB = G.create({"name": "BB", "implied_ids": [(6, 0, [group_B.id])]})
+
+ # user_a is a normal user, so we expect groups to be added when we add them,
+ # as well as 'implied_groups'; otherwise nothing else should happen.
+ # By contrast, for a portal user we want implied groups not to be added
+ # if and only if it would not give group_user (or group_public) privileges
+ user_a = U.create({"name": "a", "login": "a", "groups_id": [(6, 0, [group_AA.id, group_user.id])]})
+ self.assertEqual(user_a.groups_id, (group_AA + group_A + group_user + group_no_one))
+
+ user_b = U.create({"name": "b", "login": "b", "groups_id": [(6, 0, [group_portal.id, group_AA.id])]})
+ self.assertEqual(user_b.groups_id, (group_AA + group_A + group_portal))
+
+ # user_b is not an internal user, but giving it a new group just added a new group
+ (user_a + user_b).write({"groups_id": [(4, group_BB.id)]})
+ self.assertEqual(user_a.groups_id, (group_AA + group_A + group_BB + group_B + group_user + group_no_one))
+ self.assertEqual(user_b.groups_id, (group_AA + group_A + group_BB + group_B + group_portal))
+
+ # now we create a group that implies the group_user
+ # adding it to a user should work normally, whereas adding it to a portal user should raise
+ group_C = G.create({"name": "C", "implied_ids": [(6, 0, [group_user.id])]})
+
+ user_a.write({"groups_id": [(4, group_C.id)]})
+ self.assertEqual(user_a.groups_id, (group_AA + group_A + group_BB + group_B + group_C + group_user + group_no_one))
+
+ with self.assertRaises(ValidationError):
+ user_b.write({"groups_id": [(4, group_C.id)]})
diff --git a/odoo/service/model.py b/odoo/service/model.py
index 9673b6f98c9..82a32c2cfc1 100644
--- a/odoo/service/model.py
+++ b/odoo/service/model.py
@@ -157,6 +157,7 @@ def check(f):
return wrapper
def execute_cr(cr, uid, obj, method, *args, **kw):
+ odoo.api.Environment.reset() # clean cache etc if we retry the same transaction
recs = odoo.api.Environment(cr, uid, {}).get(obj)
if recs is None:
raise UserError(_("Object %s doesn't exist") % obj)
diff --git a/odoo/service/server.py b/odoo/service/server.py
index 15d3009e36b..7ff35146a85 100644
--- a/odoo/service/server.py
+++ b/odoo/service/server.py
@@ -507,6 +507,8 @@ class ThreadedServer(CommonServer):
# We wait there is no processing requests
# other than the ones exceeding the limits, up to 1 min,
# before asking for a reload.
+ _logger.info('Dumping stacktrace of limit exceeding threads before reloading')
+ dumpstacks(thread_idents=[thread.ident for thread in self.limits_reached_threads])
self.reload()
# `reload` increments `self.quit_signals_received`
# and the loop will end after this iteration,
@@ -709,7 +711,7 @@ class PreforkServer(CommonServer):
raise KeyboardInterrupt
elif sig == signal.SIGQUIT:
# dump stacks on kill -3
- self.dumpstacks()
+ dumpstacks()
elif sig == signal.SIGUSR1:
# log ormcache stats on kill -SIGUSR1
log_ormcache_stats()
diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py
index 5715c9b83c6..79f1d386949 100644
--- a/odoo/tools/misc.py
+++ b/odoo/tools/misc.py
@@ -926,8 +926,10 @@ class ConstantMapping(Mapping):
return self._value
-def dumpstacks(sig=None, frame=None):
- """ Signal handler: dump a stack trace for each existing thread."""
+def dumpstacks(sig=None, frame=None, thread_idents=None):
+ """ Signal handler: dump a stack trace for each existing thread or given
+ thread(s) specified through the ``thread_idents`` sequence.
+ """
code = []
def extract_stack(stack):
@@ -944,14 +946,15 @@ def dumpstacks(sig=None, frame=None):
'url': getattr(th, 'url', 'n/a')}
for th in threading.enumerate()}
for threadId, stack in sys._current_frames().items():
- thread_info = threads_info.get(threadId, {})
- code.append("\n# Thread: %s (db:%s) (uid:%s) (url:%s)" %
- (thread_info.get('repr', threadId),
- thread_info.get('dbname', 'n/a'),
- thread_info.get('uid', 'n/a'),
- thread_info.get('url', 'n/a')))
- for line in extract_stack(stack):
- code.append(line)
+ if not thread_idents or threadId in thread_idents:
+ thread_info = threads_info.get(threadId, {})
+ code.append("\n# Thread: %s (db:%s) (uid:%s) (url:%s)" %
+ (thread_info.get('repr', threadId),
+ thread_info.get('dbname', 'n/a'),
+ thread_info.get('uid', 'n/a'),
+ thread_info.get('url', 'n/a')))
+ for line in extract_stack(stack):
+ code.append(line)
if odoo.evented:
# code from http://stackoverflow.com/questions/12510648/in-gevent-how-can-i-dump-stack-traces-of-all-running-greenlets