Files
odoo-addons/addons/cetmix_tower_server/models/cx_tower_plan_line.py

302 lines
10 KiB
Python

# Copyright (C) 2022 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools.safe_eval import safe_eval
from .constants import PLAN_LINE_CONDITION_CHECK_FAILED
_logger = logging.getLogger(__name__)
class CxTowerPlanLine(models.Model):
"""Flight Plan Line"""
_name = "cx.tower.plan.line"
_inherit = [
"cx.tower.reference.mixin",
]
_order = "sequence, plan_id"
_description = "Cetmix Tower Flight Plan Line"
active = fields.Boolean(related="plan_id.active", readonly=True)
sequence = fields.Integer(default=10)
name = fields.Char(related="command_id.name", readonly=True)
plan_id = fields.Many2one(
string="Flight Plan",
comodel_name="cx.tower.plan",
auto_join=True,
ondelete="cascade",
)
action = fields.Selection(
selection=lambda self: self.command_id._selection_action(),
compute="_compute_action",
required=True,
readonly=False,
)
command_id = fields.Many2one(
comodel_name="cx.tower.command",
required=True,
ondelete="restrict",
domain="[('action', '=', action)]",
)
note = fields.Text(related="command_id.note", readonly=True)
path = fields.Char(
help="Location where command will be executed. Overrides command default path. "
"You can use {{ variables }} in path",
)
use_sudo = fields.Boolean(
help="Will use sudo based on server settings."
"If no sudo is configured will run without sudo"
)
action_ids = fields.One2many(
string="Actions",
comodel_name="cx.tower.plan.line.action",
inverse_name="line_id",
auto_join=True,
copy=True,
help="Actions trigger based on command result."
" If empty next command will be executed",
)
command_code = fields.Text(
related="command_id.code",
readonly=True,
)
tag_ids = fields.Many2many(related="command_id.tag_ids", readonly=True)
access_level = fields.Selection(
related="plan_id.access_level",
readonly=True,
store=True,
)
condition = fields.Char(
help="Conditions under which this Flight Plan Line "
"will be launched. e.g.: {{ odoo_version}} == '14.0'",
)
variable_ids = fields.Many2many(
comodel_name="cx.tower.variable",
relation="cx_tower_plan_line_variable_rel",
column1="plan_line_id",
column2="variable_id",
string="Variables",
compute="_compute_variable_ids",
store=True,
)
# -- Command related entities
plan_run_id = fields.Many2one(
comodel_name="cx.tower.plan",
related="command_id.flight_plan_id",
readonly=True,
string="Run Flight Plan",
)
plan_run_line_ids = fields.One2many(
comodel_name="cx.tower.plan.line",
related="command_id.flight_plan_id.line_ids",
string="Flight Plan Lines",
readonly=True,
)
file_template_id = fields.Many2one(
comodel_name="cx.tower.file.template",
related="command_id.file_template_id",
readonly=True,
)
file_template_code = fields.Text(
string="Template Code",
related="file_template_id.code",
readonly=True,
)
@api.depends("condition")
def _compute_variable_ids(self):
"""
Compute variable_ids based on condition field.
"""
template_mixin_obj = self.env["cx.tower.template.mixin"]
for record in self:
record.variable_ids = template_mixin_obj._prepare_variable_commands(
["condition"], force_record=record
)
def _compute_action(self):
"""
Compute action based on command.
"""
# We set action only once, so there is no 'depends' in this function
for record in self:
if record.action:
continue
if record.command_id:
record.action = record.command_id.action
else:
record.action = False
@api.constrains("command_id")
def _check_command_id(self):
"""
Check recursive plan line execution.
"""
for line in self:
# Check recursive plan line execution
visited_plans = set()
self._check_recursive_plan(line.command_id, visited_plans)
@api.onchange("action")
def _inverse_action(self):
"""
Reset command when action changes.
"""
self.command_id = False
def _check_recursive_plan(self, command, visited_plans):
"""
Recursively check if the command plan creates a cycle.
Raise a ValidationError if a cycle is detected.
"""
if command.flight_plan_id and command.action == "plan":
if command.flight_plan_id.id in visited_plans:
raise ValidationError(
_(
"Recursive plan call detected in plan %(name)s.",
name=command.flight_plan_id.name,
)
)
visited_plans.add(command.flight_plan_id.id)
# recursively check the lines in the plan
for line in command.flight_plan_id.line_ids:
self._check_recursive_plan(line.command_id, visited_plans)
def _run(self, server, plan_log_record, **kwargs):
"""Run command from the Flight Plan line
Args:
server (cx.tower.server()): Server object
plan_log_record (cx.tower.plan.log()): Log record object
kwargs (dict): Optional arguments
Following are supported but not limited to:
- "plan_log": {values passed to flightplan logger}
- "log": {values passed to command logger}
- "key": {values passed to key parser}
"""
self.ensure_one()
# Set current line as currently executed in log
plan_log_record.plan_line_executed_id = self
# It is necessary to save information about which plan log
# was created for a command log that has the command action “plan”
flight_plan_command_log = kwargs.get("flight_plan_command_log")
if flight_plan_command_log:
flight_plan_command_log.triggered_plan_log_id = plan_log_record.id
# Pass plan_log to command so it will be saved in command log
log_vals = kwargs.get("log", {})
log_vals.update({"plan_log_id": plan_log_record.id})
kwargs.update({"log": log_vals})
# Set 'sudo' value
use_sudo = self.use_sudo and server.use_sudo
# Use sudo to bypass access rules for execute command with higher access level
command_as_root = self.sudo().command_id
# Set path
path = self.path or command_as_root.path
server.run_command(command_as_root, path, sudo=use_sudo, **kwargs)
def _is_executable_line(self, server, variable_values=None):
"""
Check if this line can be executed based on its condition.
Args:
server (cx.tower.server()): The server on which conditions are checked.
variable_values (dict, optional): Custom values provided when running the
flight plan. These values are merged with server variables when
rendering the condition.
Returns:
bool: True if the line can be executed, otherwise False.
"""
self.ensure_one()
condition = self.condition
if condition:
# Collect variable references used in the condition
variables = self.command_id.get_variables_from_code(condition)
# Values from server variables referenced in the condition
server_values = {}
if variables:
variable_values_dict = server.get_variable_values(variables)
server_values = variable_values_dict.get(server.id, {}) or {}
# Merge with custom values passed to the flight plan (if any)
merged_values = {**server_values, **(variable_values or {})}
# Render condition with all available values (in pythonic mode)
if merged_values:
condition = self.command_id.render_code_custom(
condition, pythonic_mode=True, **merged_values
)
# For evaluate a string that contains an expression that mostly uses
# Python constants, arithmetic expressions and the objects directly provided
# in context we need use `safe_eval`
# We catch all exceptions and return False to avoid raising an exception
try:
result = safe_eval(condition)
except Exception as e:
_logger.error(
"Error evaluating condition '%s' for plan line '%s' "
"in plan '%s' for server '%s'. Line is skipped. Error: %s",
condition,
self.name,
self.plan_id.name,
server.name,
str(e),
)
result = False
return result
return True # Assume the line can be executed if no condition is specified
def _skip(self, server, plan_log_record, **kwargs):
"""
Triggered when plan line skipped by condition
"""
self.ensure_one()
# Set current line as currently executed in log
plan_log_record.plan_line_executed_id = self
# Log the unsuccessful execution attempt
now = fields.Datetime.now()
log_vals = kwargs.get("log", {})
self.env["cx.tower.command.log"].record(
server.id,
self.command_id.id,
now,
now,
PLAN_LINE_CONDITION_CHECK_FAILED,
None,
_("Plan line condition check failed."),
plan_log_id=plan_log_record.id,
condition=self.condition,
is_skipped=True,
**log_vals,
)
def _get_dependent_model_relation_fields(self):
"""Check cx.tower.reference.mixin for the function documentation"""
res = super()._get_dependent_model_relation_fields()
return res + ["action_ids"]
def _get_pre_populated_model_data(self):
"""Check cx.tower.reference.mixin for the function documentation"""
res = super()._get_pre_populated_model_data()
res.update({"cx.tower.plan.line": ["cx.tower.plan", "plan_id"]})
return res