Files
odoo_source/addons/onboarding/models/onboarding_progress.py
T
Florian CharlierandJulien Castiaux beaae001f8 [FIX] web,onboarding: fix multiple progress records
and better handle progress requests.

In previous versions, it sometimes occurred that multiple
onboarding_progress records were created despite our measures
against that.

Two problems were identified and fixed here, together with a simple
mean to handle this error on the client-side.

1. `_sql_constraints` entry was not adequate to prevent multiple
  records from being created for onboardings which are not to be
  completed per-company but per database because in our version(s)
  of PostgreSQL `NULL` values cannot be considered `NOT DISTINCT`,
  i.e., all NULL values are different from one another so `UNIQUE`
  is respected when we wouldn't want it.
  Therefore, we implement here a `UNIQUE` index via the model's init,
  allowing to catch unique violations when company_id is not set.

2. While we search if a record exists before creating a new one, it
  could happen that another worker created a record between these
  steps. As the database is configured (`REPEATABLE_READ`, which
  applies until the end of the transaction, when we leave the
  controller), it is very difficult to retrieve the record created by
  the other process. See additional details in the testing part below.
  Therefore, we chose to ask the client to perform a new request if
  desirable.

Existing extra records are removed with the related UPG PR script.

### Tests

Tests are included to make sure it is not possible to create multiple
onboarding_progress records for the same onboarding and company or
without any. This includes a non-standard (runbot will not run it)
and "database breaking" (we try to clean up our mess but we cannot
give any guarantee) concurrent test that reproduce the following
scenario:

The `/onboarding/<string:route_name>` is called twice in two different
workers for a same `route_name`. The corresponding onboarding exists
and has no corresponding `onboarding.progress` record yet. Python side,
the two workers are oblivious of what's happening in the transaction of
the other worker thus any python-side-only unicity constraint would
fail to detect any progress created by other workers. This would end up
with two progress records created for a single onboarding which is
illegal.

Task-3101666

closes odoo/odoo#109221

Related: odoo/upgrade#4142
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
Co-authored-by: Julien Castiaux <juc@odoo.com>
2023-02-08 12:25:52 +01:00

77 lines
3.4 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
ONBOARDING_PROGRESS_STATES = [
('not_done', 'Not done'),
('just_done', 'Just done'),
('done', 'Done'),
]
class OnboardingProgress(models.Model):
_name = 'onboarding.progress'
_description = 'Onboarding Progress Tracker'
_rec_name = 'onboarding_id'
onboarding_state = fields.Selection(
ONBOARDING_PROGRESS_STATES, string='Onboarding progress', compute='_compute_onboarding_state', store=True)
is_onboarding_closed = fields.Boolean('Was panel closed?')
company_id = fields.Many2one('res.company')
onboarding_id = fields.Many2one(
'onboarding.onboarding', 'Related onboarding tracked', required=True, ondelete='cascade')
progress_step_ids = fields.One2many('onboarding.progress.step', 'progress_id', 'Progress Steps Trackers')
def init(self):
# not in _sql_constraint because COALESCE is not supported for PostgreSQL constraint
self.env.cr.execute("""
CREATE UNIQUE INDEX IF NOT EXISTS onboarding_progress_onboarding_company_uniq
ON onboarding_progress (onboarding_id, COALESCE(company_id, 0))
""")
@api.depends('onboarding_id.step_ids', 'progress_step_ids', 'progress_step_ids.step_state')
def _compute_onboarding_state(self):
progress_steps_data = self.env['onboarding.progress.step'].read_group(
[('progress_id', 'in', self.ids), ('step_state', 'in', ['just_done', 'done'])],
['progress_id'], ['progress_id']
)
result = dict((data['progress_id'][0], data['progress_id_count']) for data in progress_steps_data)
for progress in self:
progress.onboarding_state = (
'not_done' if result.get(progress.id, 0) != len(progress.onboarding_id.step_ids)
else 'done')
def action_close(self):
self.is_onboarding_closed = True
def action_toggle_visibility(self):
for progress in self:
progress.is_onboarding_closed = not progress.is_onboarding_closed
def _get_and_update_onboarding_state(self):
"""Used to fetch the progress of an onboarding for rendering its panel and is expected to
be called by the onboarding controller. It also has the responsibility of updating the
'just_done' states into 'done' so that the 'just_done' states are only rendered once.
"""
self.ensure_one()
onboarding_states_values = {}
progress_steps_to_consolidate = self.env['onboarding.progress.step']
# Iterate over onboarding step_ids and not self.progress_step_ids because 'not_done' steps
# may not have a progress_step record.
for step in self.onboarding_id.step_ids:
step_state = step.current_step_state
if step_state == 'just_done':
progress_steps_to_consolidate |= step.current_progress_step_id
onboarding_states_values[step.id] = step_state
progress_steps_to_consolidate.action_consolidate_just_done()
if self.is_onboarding_closed:
onboarding_states_values['onboarding_state'] = 'closed'
elif self.onboarding_state == 'done':
onboarding_states_values['onboarding_state'] = 'just_done' if progress_steps_to_consolidate else 'done'
return onboarding_states_values