Compare commits
8 Commits
cetmix_tow
...
18.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
304da43eb8 | ||
| 48b0b7a283 | |||
| 40c3e1d471 | |||
| c1ecf1289d | |||
| 0741834b31 | |||
| 7a2debb3d7 | |||
| 6ef9d029eb | |||
|
|
96edc0c694 |
@@ -1,122 +0,0 @@
|
||||
=========================
|
||||
Cetmix Tower Server Queue
|
||||
=========================
|
||||
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:bcdbf27340bb59ec9a0cf443b108e2d6b27cf7c64466b47585fbd02410ef071b
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
|
||||
:target: https://odoo-community.org/page/development-status
|
||||
:alt: Beta
|
||||
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
|
||||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||
:alt: License: AGPL-3
|
||||
.. |badge3| image:: https://img.shields.io/badge/github-cetmix%2Fcetmix--tower-lightgray.png?logo=github
|
||||
:target: https://github.com/cetmix/cetmix-tower/tree/16.0/cetmix_tower_server_queue
|
||||
:alt: cetmix/cetmix-tower
|
||||
|
||||
|badge1| |badge2| |badge3|
|
||||
|
||||
This module implements asynchronous task execution for `Cetmix
|
||||
Tower <https://cetmix.com/tower>`__.
|
||||
|
||||
It requires the `queue_job <https://github.com/OCA/queue/queue_job>`__
|
||||
module to be installed and configured in the Odoo instance.
|
||||
|
||||
Please refer to the `official
|
||||
documentation <https://cetmix.com/tower>`__ for detailed information.
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Please refer to the `official
|
||||
documentation <https://cetmix.com/tower>`__ for detailed configuration
|
||||
instructions.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
Please refer to the `official
|
||||
documentation <https://cetmix.com/tower>`__ for detailed usage
|
||||
instructions.
|
||||
|
||||
Changelog
|
||||
=========
|
||||
|
||||
16.0.2.0.0 (2026-03-23)
|
||||
-----------------------
|
||||
|
||||
- Features: Jets! (4700)
|
||||
|
||||
16.0.1.2.0 (2025-11-12)
|
||||
-----------------------
|
||||
|
||||
- Features: Use the 'web_notify' module to send user notifications.
|
||||
(5074)
|
||||
|
||||
16.0.1.1.4 (2025-11-05)
|
||||
-----------------------
|
||||
|
||||
- Bugfixes: Finish multiple commands at once. (5062)
|
||||
|
||||
16.0.1.1.3 (2025-10-13)
|
||||
-----------------------
|
||||
|
||||
- Features: Terminate running flight plan manually (3410)
|
||||
|
||||
16.0.1.1.0 (2025-07-16)
|
||||
-----------------------
|
||||
|
||||
- Features: cetmix_tower_server_queue: Add async file upload/download
|
||||
via job queue (3720)
|
||||
- Features: Terminate command with error if job has failed (4718)
|
||||
|
||||
16.0.1.0.2 (2025-05-16)
|
||||
-----------------------
|
||||
|
||||
- Features: 'sudo' parameter is not passed to command. (4678)
|
||||
|
||||
16.0.1.0.1 (2025-05-09)
|
||||
-----------------------
|
||||
|
||||
- Bugfixes: Non-critical issues and performance improvements. (4611)
|
||||
|
||||
16.0.1.0.0
|
||||
----------
|
||||
|
||||
Release for Odoo 16.0
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/cetmix/cetmix-tower/issues>`_.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/cetmix/cetmix-tower/issues/new?body=module:%20cetmix_tower_server_queue%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
|
||||
Do not contact contributors directly about support or help with technical issues.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Authors
|
||||
-------
|
||||
|
||||
* Cetmix
|
||||
|
||||
Maintainers
|
||||
-----------
|
||||
|
||||
This module is part of the `cetmix/cetmix-tower <https://github.com/cetmix/cetmix-tower/tree/16.0/cetmix_tower_server_queue>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute.
|
||||
@@ -1 +0,0 @@
|
||||
from . import models
|
||||
@@ -1,19 +0,0 @@
|
||||
# Copyright (C) 2022 Cetmix OÜ
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
{
|
||||
"name": "Cetmix Tower Server Queue",
|
||||
"summary": "Cetmix Tower asynchronous task execution using 'queue_job'",
|
||||
"version": "16.0.2.0.0",
|
||||
"development_status": "Beta",
|
||||
"category": "Productivity",
|
||||
"website": "https://tower.cetmix.com",
|
||||
"author": "Cetmix",
|
||||
"license": "AGPL-3",
|
||||
"installable": True,
|
||||
"auto_install": True,
|
||||
"depends": ["cetmix_tower_server", "queue_job"],
|
||||
"data": [
|
||||
"views/cx_tower_command_log_view.xml",
|
||||
"views/cx_tower_file_view.xml",
|
||||
],
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * cetmix_tower_server_queue
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#: model:ir.model.fields,help:cetmix_tower_server_queue.field_cx_tower_command_log__command_status
|
||||
msgid ""
|
||||
"0 if command finished successfully.\n"
|
||||
"-100 general error,\n"
|
||||
"-101 not found,\n"
|
||||
"-201 another instance of this command is running,\n"
|
||||
"-202 no runner found for the command action,\n"
|
||||
"-203 Python code execution failed\n"
|
||||
"-205 plan line condition check failed\n"
|
||||
"503 if SSH connection error occurred\n"
|
||||
"601 if queue job failed"
|
||||
msgstr ""
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#: model:ir.model,name:cetmix_tower_server_queue.model_cx_tower_command_log
|
||||
msgid "Cetmix Tower Command Log"
|
||||
msgstr ""
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#: model:ir.model,name:cetmix_tower_server_queue.model_cx_tower_file
|
||||
msgid "Cetmix Tower File"
|
||||
msgstr ""
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#: model:ir.model,name:cetmix_tower_server_queue.model_cx_tower_server
|
||||
msgid "Cetmix Tower Server"
|
||||
msgstr ""
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#: model_terms:ir.ui.view,arch_db:cetmix_tower_server_queue.cx_tower_file_view_form
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#: model:ir.model.fields,field_description:cetmix_tower_server_queue.field_cx_tower_command_log__command_status
|
||||
msgid "Exit Code"
|
||||
msgstr ""
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#. odoo-python
|
||||
#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0
|
||||
#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0
|
||||
#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0
|
||||
#, python-format
|
||||
msgid "Failure"
|
||||
msgstr ""
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#. odoo-python
|
||||
#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0
|
||||
#, python-format
|
||||
msgid "File downloaded!"
|
||||
msgstr ""
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#: model:ir.model.fields,help:cetmix_tower_server_queue.field_cx_tower_file__is_being_processed
|
||||
msgid "File is currently being processed"
|
||||
msgstr ""
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#. odoo-python
|
||||
#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0
|
||||
#, python-format
|
||||
msgid "File uploaded!"
|
||||
msgstr ""
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#. odoo-python
|
||||
#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0
|
||||
#, python-format
|
||||
msgid "File(s) %(name)s download failed: %(error)s"
|
||||
msgstr ""
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#. odoo-python
|
||||
#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0
|
||||
#, python-format
|
||||
msgid "File(s) %(name)s upload failed: %(error)s"
|
||||
msgstr ""
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#. odoo-python
|
||||
#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0
|
||||
#, python-format
|
||||
msgid "Files downloaded!"
|
||||
msgstr ""
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#. odoo-python
|
||||
#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0
|
||||
#, python-format
|
||||
msgid "Files uploaded!"
|
||||
msgstr ""
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#: model:ir.model.fields,field_description:cetmix_tower_server_queue.field_cx_tower_file__is_being_processed
|
||||
msgid "Is Being Processed"
|
||||
msgstr ""
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#: model_terms:ir.ui.view,arch_db:cetmix_tower_server_queue.cx_tower_file_view_form
|
||||
msgid "Processing"
|
||||
msgstr ""
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#: model:ir.model,name:cetmix_tower_server_queue.model_queue_job
|
||||
#: model:ir.model.fields,field_description:cetmix_tower_server_queue.field_cx_tower_command_log__queue_job_id
|
||||
msgid "Queue Job"
|
||||
msgstr ""
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#. odoo-python
|
||||
#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0
|
||||
#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0
|
||||
#: model_terms:ir.ui.view,arch_db:cetmix_tower_server_queue.cx_tower_file_view_form
|
||||
#, python-format
|
||||
msgid "Success"
|
||||
msgstr ""
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#. odoo-python
|
||||
#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0
|
||||
#, python-format
|
||||
msgid "The following files are already being processed: %(name)s"
|
||||
msgstr ""
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#. odoo-python
|
||||
#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Unable to upload file '%(f)s'.\n"
|
||||
"Upload operation is not supported for 'server' type files."
|
||||
msgstr ""
|
||||
@@ -1,148 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * cetmix_tower_server_queue
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: it\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Generator: Poedit 2.3\n"
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#: model:ir.model.fields,help:cetmix_tower_server_queue.field_cx_tower_command_log__command_status
|
||||
msgid ""
|
||||
"0 if command finished successfully.\n"
|
||||
"-100 general error,\n"
|
||||
"-101 not found,\n"
|
||||
"-201 another instance of this command is running,\n"
|
||||
"-202 no runner found for the command action,\n"
|
||||
"-203 Python code execution failed\n"
|
||||
"-205 plan line condition check failed\n"
|
||||
"503 if SSH connection error occurred\n"
|
||||
"601 if queue job failed"
|
||||
msgstr "0 se il comando è stato completato correttamente.-100 errore generale,-101 non trovato,-201 un'altra istanza di questo comando è in esecuzione,-202 nessun runner trovato per l'azione del comando,-203 esecuzione del codice Python non riuscita,-205 controllo delle condizioni della riga del piano non riuscito,503 se si è verificato un errore di connessione SSH,601 se il processo in coda non è riuscito."
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#: model:ir.model,name:cetmix_tower_server_queue.model_cx_tower_command_log
|
||||
msgid "Cetmix Tower Command Log"
|
||||
msgstr "Registro comando Cetmix Tower"
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#: model:ir.model,name:cetmix_tower_server_queue.model_cx_tower_file
|
||||
msgid "Cetmix Tower File"
|
||||
msgstr "File Cetmix Tower"
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#: model:ir.model,name:cetmix_tower_server_queue.model_cx_tower_server
|
||||
msgid "Cetmix Tower Server"
|
||||
msgstr "Server Cetmix Tower"
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#: model_terms:ir.ui.view,arch_db:cetmix_tower_server_queue.cx_tower_file_view_form
|
||||
msgid "Error"
|
||||
msgstr "Errore"
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#: model:ir.model.fields,field_description:cetmix_tower_server_queue.field_cx_tower_command_log__command_status
|
||||
msgid "Exit Code"
|
||||
msgstr "Codice uscita"
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#. odoo-python
|
||||
#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0
|
||||
#, python-format
|
||||
msgid "Failure"
|
||||
msgstr "Fallimento"
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#. odoo-python
|
||||
#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0
|
||||
#, python-format
|
||||
msgid "File downloaded!"
|
||||
msgstr "File scaricato!"
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#: model:ir.model.fields,help:cetmix_tower_server_queue.field_cx_tower_file__is_being_processed
|
||||
msgid "File is currently being processed"
|
||||
msgstr "Il file è in lavorazione"
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#. odoo-python
|
||||
#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0
|
||||
#, python-format
|
||||
msgid "File uploaded!"
|
||||
msgstr "File caricato!"
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#. odoo-python
|
||||
#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0
|
||||
#, python-format
|
||||
msgid "Files downloaded!"
|
||||
msgstr "File scaricati!"
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#. odoo-python
|
||||
#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0
|
||||
#, python-format
|
||||
msgid "Files uploaded!"
|
||||
msgstr "File caricati!"
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#: model:ir.model.fields,field_description:cetmix_tower_server_queue.field_cx_tower_file__is_being_processed
|
||||
msgid "Is Being Processed"
|
||||
msgstr "In lavorazione"
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#: model_terms:ir.ui.view,arch_db:cetmix_tower_server_queue.cx_tower_file_view_form
|
||||
msgid "Processing"
|
||||
msgstr "Lavorazione"
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#: model:ir.model,name:cetmix_tower_server_queue.model_queue_job
|
||||
#: model:ir.model.fields,field_description:cetmix_tower_server_queue.field_cx_tower_command_log__queue_job_id
|
||||
msgid "Queue Job"
|
||||
msgstr "Accoda lavoro"
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#. odoo-python
|
||||
#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0
|
||||
#: model_terms:ir.ui.view,arch_db:cetmix_tower_server_queue.cx_tower_file_view_form
|
||||
#, python-format
|
||||
msgid "Success"
|
||||
msgstr "Successo"
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#. odoo-python
|
||||
#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0
|
||||
#, python-format
|
||||
msgid "The following files are already being processed: %(name)s"
|
||||
msgstr "I seguenti file sono già in fase di elaborazione: %(name)s"
|
||||
|
||||
#. module: cetmix_tower_server_queue
|
||||
#. odoo-python
|
||||
#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Unable to upload file '%(f)s'.\n"
|
||||
"Upload operation is not supported for 'server' type files."
|
||||
msgstr ""
|
||||
"Impossibile caricare il file '%(f)s'.\n"
|
||||
"L'operazione di caricamento non è supportata per i file di tipo 'server'."
|
||||
|
||||
#~ msgid "Display Name"
|
||||
#~ msgstr "Nome visualizzato"
|
||||
|
||||
#~ msgid "ID"
|
||||
#~ msgstr "ID"
|
||||
|
||||
#~ msgid "Last Modified on"
|
||||
#~ msgstr "Ultima modifica il"
|
||||
@@ -1,4 +0,0 @@
|
||||
from . import cx_tower_command_log
|
||||
from . import cx_tower_server
|
||||
from . import queue_job
|
||||
from . import cx_tower_file
|
||||
@@ -1,82 +0,0 @@
|
||||
# Copyright (C) 2025 Cetmix OÜ
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import fields, models, tools
|
||||
|
||||
from odoo.addons.cetmix_tower_server.models.constants import (
|
||||
COMMAND_STOPPED,
|
||||
COMMAND_TIMED_OUT,
|
||||
)
|
||||
from odoo.addons.queue_job.job import CANCELLED
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CxTowerCommandLog(models.Model):
|
||||
_inherit = "cx.tower.command.log"
|
||||
|
||||
queue_job_id = fields.Many2one(
|
||||
"queue.job",
|
||||
readonly=True,
|
||||
groups="queue_job.group_queue_job_manager",
|
||||
)
|
||||
|
||||
command_status = fields.Integer(
|
||||
help="0 if command finished successfully.\n"
|
||||
"-100 general error,\n"
|
||||
"-101 not found,\n"
|
||||
"-201 another instance of this command is running,\n"
|
||||
"-202 no runner found for the command action,\n"
|
||||
"-203 Python code execution failed\n"
|
||||
"-205 plan line condition check failed\n"
|
||||
"503 if SSH connection error occurred\n"
|
||||
"601 if queue job failed"
|
||||
)
|
||||
|
||||
def finish(
|
||||
self, finish_date=None, status=None, response=None, error=None, **kwargs
|
||||
):
|
||||
"""Finish the command log
|
||||
|
||||
Args:
|
||||
finish_date (Datetime, optional): Command finish date. Defaults to None.
|
||||
status (Integer, optional): Command status. Defaults to None.
|
||||
response (Text, optional): Command response. Defaults to None.
|
||||
error (Text, optional): Command error. Defaults to None.
|
||||
"""
|
||||
|
||||
# Filter out command logs that are already stopped
|
||||
command_logs_to_process = self.filtered(
|
||||
lambda log: log.command_status != COMMAND_STOPPED
|
||||
)
|
||||
if not command_logs_to_process:
|
||||
return
|
||||
|
||||
# Avoid finishing the command log multiple times at the same time
|
||||
try:
|
||||
with self.env.cr.savepoint(), tools.mute_logger("odoo.sql_db"):
|
||||
self.env.cr.execute(
|
||||
f"SELECT command_status FROM {self._table} WHERE id IN %s FOR UPDATE NOWAIT", # noqa: E501
|
||||
(tuple(command_logs_to_process.ids),),
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.error(
|
||||
"Could not acquire lock on command logs %s, skipping finish: %s",
|
||||
command_logs_to_process.ids,
|
||||
e,
|
||||
)
|
||||
return
|
||||
|
||||
# Update the related queue job state if the command timed out
|
||||
if status == COMMAND_TIMED_OUT:
|
||||
for command_log in command_logs_to_process:
|
||||
if command_log.queue_job_id:
|
||||
command_log.queue_job_id.sudo()._change_job_state(
|
||||
CANCELLED, result=error
|
||||
)
|
||||
|
||||
return super(CxTowerCommandLog, command_logs_to_process).finish(
|
||||
finish_date, status, response, error, **kwargs
|
||||
)
|
||||
@@ -1,184 +0,0 @@
|
||||
# Copyright (C) 2025 Cetmix OÜ
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import _, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CxTowerFile(models.Model):
|
||||
_inherit = "cx.tower.file"
|
||||
|
||||
is_being_processed = fields.Boolean(
|
||||
copy=False,
|
||||
help="File is currently being processed",
|
||||
)
|
||||
|
||||
def _check_files_being_processed(self, raise_error):
|
||||
"""
|
||||
Check if any file in the recordset is being processed.
|
||||
True if at least one file is already processing and raise_error is False.
|
||||
False if no files are currently being processed.
|
||||
The caller uses the boolean to decide whether to continue or abort.
|
||||
"""
|
||||
processing_files = self.filtered(lambda rec: rec.is_being_processed)
|
||||
if processing_files:
|
||||
if raise_error:
|
||||
raise UserError(
|
||||
_(
|
||||
"The following files are already being processed: %(name)s",
|
||||
name=", ".join(processing_files.mapped("name")),
|
||||
)
|
||||
)
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
def upload(self, raise_error=False):
|
||||
"""
|
||||
Trigger asynchronous upload via job queue.
|
||||
"""
|
||||
# Check if the file is already being processed
|
||||
if self._check_files_being_processed(raise_error):
|
||||
return
|
||||
|
||||
self.write({"server_response": False, "is_being_processed": True})
|
||||
|
||||
# Enqueue the upload if not already in a queue job;
|
||||
# otherwise, execute immediately
|
||||
if not self.env.context.get("job_uuid"):
|
||||
self.with_delay()._do_upload(raise_error=raise_error)
|
||||
else:
|
||||
self._do_upload(raise_error=raise_error)
|
||||
|
||||
def download(self, raise_error=False):
|
||||
"""
|
||||
Trigger asynchronous download via job queue.
|
||||
"""
|
||||
|
||||
# Check if the file is already being processed
|
||||
if self._check_files_being_processed(raise_error):
|
||||
return
|
||||
|
||||
self.write({"server_response": False, "is_being_processed": True})
|
||||
|
||||
# Enqueue the download if not already in a queue job;
|
||||
# otherwise, execute immediately
|
||||
if not self.env.context.get("job_uuid"):
|
||||
self.with_delay()._do_download(raise_error=raise_error)
|
||||
else:
|
||||
self._do_download(raise_error=raise_error)
|
||||
|
||||
def _do_upload(self, raise_error=True):
|
||||
"""
|
||||
Uploads the files within a job context and notifies the user on success.
|
||||
Logs the error if an exception occurs;
|
||||
failure state is managed by the parent method.
|
||||
"""
|
||||
try:
|
||||
with self.env.cr.savepoint():
|
||||
result = super().upload(raise_error=raise_error)
|
||||
single_msg = _("File uploaded!")
|
||||
plural_msg = _("Files uploaded!")
|
||||
self.env.user.notify_success(
|
||||
message=single_msg if len(self) == 1 else plural_msg,
|
||||
title=_("Success"),
|
||||
# This notification should not be sticky
|
||||
# to avoid blocking the user's screen
|
||||
sticky=False,
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
if not raise_error:
|
||||
self.env.user.notify_danger(
|
||||
message=_(
|
||||
"File(s) %(name)s upload failed: %(error)s",
|
||||
name=", ".join(self.mapped("name")),
|
||||
error=str(e),
|
||||
),
|
||||
title=_("Failure"),
|
||||
sticky=self.env["ir.config_parameter"]
|
||||
.sudo()
|
||||
.get_param("cetmix_tower_server.notification_type_error", "sticky")
|
||||
== "sticky",
|
||||
)
|
||||
_logger.error("File %s upload failed: %s", str(self), str(e))
|
||||
else:
|
||||
raise
|
||||
finally:
|
||||
self.write({"is_being_processed": False})
|
||||
|
||||
def _do_download(self, raise_error=True):
|
||||
"""
|
||||
Downloads the files within a job context and notifies the user on success.
|
||||
Logs the error if an exception occurs;
|
||||
failure state is managed by the parent method.
|
||||
"""
|
||||
try:
|
||||
with self.env.cr.savepoint():
|
||||
result = super().download(raise_error=raise_error)
|
||||
single_msg = _("File downloaded!")
|
||||
plural_msg = _("Files downloaded!")
|
||||
self.env.user.notify_success(
|
||||
message=single_msg if len(self) == 1 else plural_msg,
|
||||
title=_("Success"),
|
||||
# This notification should not be sticky
|
||||
# to avoid blocking the user's screen
|
||||
sticky=False,
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
if not raise_error:
|
||||
self.env.user.notify_danger(
|
||||
message=_(
|
||||
"File(s) %(name)s download failed: %(error)s",
|
||||
name=", ".join(self.mapped("name")),
|
||||
error=str(e),
|
||||
),
|
||||
title=_("Failure"),
|
||||
sticky=self.env["ir.config_parameter"]
|
||||
.sudo()
|
||||
.get_param("cetmix_tower_server.notification_type_error", "sticky")
|
||||
== "sticky",
|
||||
)
|
||||
_logger.error("File %s download failed: %s", str(self), str(e))
|
||||
else:
|
||||
raise
|
||||
finally:
|
||||
self.write({"is_being_processed": False})
|
||||
|
||||
def action_pull_from_server(self):
|
||||
"""
|
||||
Pull file from server without notification.
|
||||
"""
|
||||
tower_files = self.filtered(lambda file_: file_.source == "tower")
|
||||
server_files = self - tower_files
|
||||
|
||||
tower_files.action_get_current_server_code()
|
||||
|
||||
server_files.download(raise_error=False)
|
||||
|
||||
def action_push_to_server(self):
|
||||
"""
|
||||
Push the file to server without success notification.
|
||||
"""
|
||||
server_files = self.filtered(lambda file_: file_.source == "server")
|
||||
if server_files:
|
||||
return {
|
||||
"type": "ir.actions.client",
|
||||
"tag": "display_notification",
|
||||
"params": {
|
||||
"title": _("Failure"),
|
||||
"message": _(
|
||||
"Unable to upload file '%(f)s'.\n"
|
||||
"Upload operation is not supported for 'server' type files.",
|
||||
f=", ".join(server_files.mapped("rendered_name")),
|
||||
),
|
||||
"sticky": False,
|
||||
},
|
||||
}
|
||||
|
||||
self.upload(raise_error=False)
|
||||
@@ -1,86 +0,0 @@
|
||||
# Copyright (C) 2022 Cetmix OÜ
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
from odoo import models
|
||||
|
||||
|
||||
class CxTowerServer(models.Model):
|
||||
_inherit = "cx.tower.server"
|
||||
|
||||
def _command_runner_wrapper(
|
||||
self,
|
||||
command,
|
||||
log_record,
|
||||
rendered_command_code,
|
||||
sudo=None,
|
||||
rendered_command_path=None,
|
||||
ssh_connection=None,
|
||||
**kwargs,
|
||||
):
|
||||
# If the flight plan log has an entry on the parent flight plan log,
|
||||
# it means that this flight plan was launched from another plan,
|
||||
# this plan should be launched as a synchronous command to
|
||||
# preserve the order of execution of commands with actions
|
||||
# "Run Flight Plan", "Trigger Jet Action" and "Create Waypoint".
|
||||
# Use runner only if command log record is provided.
|
||||
if (
|
||||
log_record
|
||||
and not log_record.plan_log_id.parent_flight_plan_log_id
|
||||
and command.action
|
||||
not in [
|
||||
"jet_action",
|
||||
"create_waypoint",
|
||||
]
|
||||
):
|
||||
job = self.with_delay()._queue_command_runner_wrapper(
|
||||
command=command,
|
||||
log_record=log_record,
|
||||
rendered_command_code=rendered_command_code,
|
||||
sudo=sudo,
|
||||
rendered_command_path=rendered_command_path,
|
||||
ssh_connection=ssh_connection,
|
||||
**kwargs,
|
||||
)
|
||||
log_record.sudo().queue_job_id = job.db_record().id
|
||||
|
||||
# Otherwise fallback to `super` to return the command output
|
||||
else:
|
||||
return super()._command_runner_wrapper(
|
||||
command=command,
|
||||
log_record=log_record,
|
||||
rendered_command_code=rendered_command_code,
|
||||
sudo=sudo,
|
||||
rendered_command_path=rendered_command_path,
|
||||
ssh_connection=ssh_connection,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def _queue_command_runner_wrapper(
|
||||
self,
|
||||
command,
|
||||
log_record,
|
||||
rendered_command_code,
|
||||
sudo=None,
|
||||
rendered_command_path=None,
|
||||
ssh_connection=None,
|
||||
**kwargs,
|
||||
):
|
||||
# avoid executing command if plan was stopped
|
||||
log_record.invalidate_recordset(["plan_log_id"])
|
||||
plan_log_id = log_record.plan_log_id
|
||||
if plan_log_id:
|
||||
plan_log_id.invalidate_recordset(["is_stopped"])
|
||||
|
||||
# If plan was stopped, stop the command
|
||||
if plan_log_id.is_stopped:
|
||||
log_record.stop()
|
||||
return
|
||||
|
||||
return self._command_runner(
|
||||
command=command,
|
||||
log_record=log_record,
|
||||
rendered_command_code=rendered_command_code,
|
||||
sudo=sudo,
|
||||
rendered_command_path=rendered_command_path,
|
||||
ssh_connection=ssh_connection,
|
||||
**kwargs,
|
||||
)
|
||||
@@ -1,23 +0,0 @@
|
||||
# Copyright 2013-2020 Camptocamp SA
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
from odoo import models
|
||||
|
||||
|
||||
class QueueJob(models.Model):
|
||||
_inherit = "queue.job"
|
||||
|
||||
QUEUE_JOB_ERROR = 601
|
||||
|
||||
def write(self, vals):
|
||||
"""
|
||||
Override write method to update command status
|
||||
and write error information in the log record
|
||||
"""
|
||||
if vals.get("state") == "failed":
|
||||
log_record = self.kwargs.get("log_record")
|
||||
if log_record:
|
||||
log_record.finish(
|
||||
status=self.QUEUE_JOB_ERROR,
|
||||
error=vals.get("exc_info"),
|
||||
)
|
||||
return super().write(vals)
|
||||
@@ -1,3 +0,0 @@
|
||||
[build-system]
|
||||
requires = ["whool"]
|
||||
build-backend = "whool.buildapi"
|
||||
@@ -1 +0,0 @@
|
||||
Please refer to the [official documentation](https://cetmix.com/tower) for detailed configuration instructions.
|
||||
@@ -1,5 +0,0 @@
|
||||
This module implements asynchronous task execution for [Cetmix Tower](https://cetmix.com/tower).
|
||||
|
||||
It requires the [queue_job](https://github.com/OCA/queue/queue_job) module to be installed and configured in the Odoo instance.
|
||||
|
||||
Please refer to the [official documentation](https://cetmix.com/tower) for detailed information.
|
||||
@@ -1,39 +0,0 @@
|
||||
## 16.0.2.0.0 (2026-03-23)
|
||||
|
||||
- Features: Jets! (4700)
|
||||
|
||||
|
||||
## 16.0.1.2.0 (2025-11-12)
|
||||
|
||||
- Features: Use the 'web_notify' module to send user notifications. (5074)
|
||||
|
||||
|
||||
## 16.0.1.1.4 (2025-11-05)
|
||||
|
||||
- Bugfixes: Finish multiple commands at once. (5062)
|
||||
|
||||
|
||||
## 16.0.1.1.3 (2025-10-13)
|
||||
|
||||
- Features: Terminate running flight plan manually (3410)
|
||||
|
||||
|
||||
## 16.0.1.1.0 (2025-07-16)
|
||||
|
||||
- Features: cetmix_tower_server_queue: Add async file upload/download via job queue (3720)
|
||||
- Features: Terminate command with error if job has failed (4718)
|
||||
|
||||
|
||||
## 16.0.1.0.2 (2025-05-16)
|
||||
|
||||
- Features: 'sudo' parameter is not passed to command. (4678)
|
||||
|
||||
|
||||
## 16.0.1.0.1 (2025-05-09)
|
||||
|
||||
- Bugfixes: Non-critical issues and performance improvements. (4611)
|
||||
|
||||
|
||||
## 16.0.1.0.0
|
||||
|
||||
Release for Odoo 16.0
|
||||
@@ -1 +0,0 @@
|
||||
Please refer to the [official documentation](https://cetmix.com/tower) for detailed usage instructions.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
@@ -1,491 +0,0 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
|
||||
<title>Cetmix Tower Server Queue</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
Despite the name, some widely supported CSS2 features are used.
|
||||
|
||||
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
|
||||
customize this style sheet.
|
||||
*/
|
||||
|
||||
/* used to remove borders from tables and images */
|
||||
.borderless, table.borderless td, table.borderless th {
|
||||
border: 0 }
|
||||
|
||||
table.borderless td, table.borderless th {
|
||||
/* Override padding for "table.docutils td" with "! important".
|
||||
The right padding separates the table cells. */
|
||||
padding: 0 0.5em 0 0 ! important }
|
||||
|
||||
.first {
|
||||
/* Override more specific margin styles with "! important". */
|
||||
margin-top: 0 ! important }
|
||||
|
||||
.last, .with-subtitle {
|
||||
margin-bottom: 0 ! important }
|
||||
|
||||
.hidden {
|
||||
display: none }
|
||||
|
||||
.subscript {
|
||||
vertical-align: sub;
|
||||
font-size: smaller }
|
||||
|
||||
.superscript {
|
||||
vertical-align: super;
|
||||
font-size: smaller }
|
||||
|
||||
a.toc-backref {
|
||||
text-decoration: none ;
|
||||
color: black }
|
||||
|
||||
blockquote.epigraph {
|
||||
margin: 2em 5em ; }
|
||||
|
||||
dl.docutils dd {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||
dl.docutils dt {
|
||||
font-weight: bold }
|
||||
*/
|
||||
|
||||
div.abstract {
|
||||
margin: 2em 5em }
|
||||
|
||||
div.abstract p.topic-title {
|
||||
font-weight: bold ;
|
||||
text-align: center }
|
||||
|
||||
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||
div.hint, div.important, div.note, div.tip, div.warning {
|
||||
margin: 2em ;
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||
div.important p.admonition-title, div.note p.admonition-title,
|
||||
div.tip p.admonition-title {
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||
div.danger p.admonition-title, div.error p.admonition-title,
|
||||
div.warning p.admonition-title, .code .error {
|
||||
color: red ;
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||
compound paragraphs.
|
||||
div.compound .compound-first, div.compound .compound-middle {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
div.compound .compound-last, div.compound .compound-middle {
|
||||
margin-top: 0.5em }
|
||||
*/
|
||||
|
||||
div.dedication {
|
||||
margin: 2em 5em ;
|
||||
text-align: center ;
|
||||
font-style: italic }
|
||||
|
||||
div.dedication p.topic-title {
|
||||
font-weight: bold ;
|
||||
font-style: normal }
|
||||
|
||||
div.figure {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
div.footer, div.header {
|
||||
clear: both;
|
||||
font-size: smaller }
|
||||
|
||||
div.line-block {
|
||||
display: block ;
|
||||
margin-top: 1em ;
|
||||
margin-bottom: 1em }
|
||||
|
||||
div.line-block div.line-block {
|
||||
margin-top: 0 ;
|
||||
margin-bottom: 0 ;
|
||||
margin-left: 1.5em }
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em ;
|
||||
border: medium outset ;
|
||||
padding: 1em ;
|
||||
background-color: #ffffee ;
|
||||
width: 40% ;
|
||||
float: right ;
|
||||
clear: right }
|
||||
|
||||
div.sidebar p.rubric {
|
||||
font-family: sans-serif ;
|
||||
font-size: medium }
|
||||
|
||||
div.system-messages {
|
||||
margin: 5em }
|
||||
|
||||
div.system-messages h1 {
|
||||
color: red }
|
||||
|
||||
div.system-message {
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.system-message p.system-message-title {
|
||||
color: red ;
|
||||
font-weight: bold }
|
||||
|
||||
div.topic {
|
||||
margin: 2em }
|
||||
|
||||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||
margin-top: 0.4em }
|
||||
|
||||
h1.title {
|
||||
text-align: center }
|
||||
|
||||
h2.subtitle {
|
||||
text-align: center }
|
||||
|
||||
hr.docutils {
|
||||
width: 75% }
|
||||
|
||||
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||
clear: left ;
|
||||
float: left ;
|
||||
margin-right: 1em }
|
||||
|
||||
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||
clear: right ;
|
||||
float: right ;
|
||||
margin-left: 1em }
|
||||
|
||||
img.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left }
|
||||
|
||||
.align-center {
|
||||
clear: both ;
|
||||
text-align: center }
|
||||
|
||||
.align-right {
|
||||
text-align: right }
|
||||
|
||||
/* reset inner alignment in figures */
|
||||
div.align-right {
|
||||
text-align: inherit }
|
||||
|
||||
/* div.align-center * { */
|
||||
/* text-align: left } */
|
||||
|
||||
.align-top {
|
||||
vertical-align: top }
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle }
|
||||
|
||||
.align-bottom {
|
||||
vertical-align: bottom }
|
||||
|
||||
ol.simple, ul.simple {
|
||||
margin-bottom: 1em }
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal }
|
||||
|
||||
ol.loweralpha {
|
||||
list-style: lower-alpha }
|
||||
|
||||
ol.upperalpha {
|
||||
list-style: upper-alpha }
|
||||
|
||||
ol.lowerroman {
|
||||
list-style: lower-roman }
|
||||
|
||||
ol.upperroman {
|
||||
list-style: upper-roman }
|
||||
|
||||
p.attribution {
|
||||
text-align: right ;
|
||||
margin-left: 50% }
|
||||
|
||||
p.caption {
|
||||
font-style: italic }
|
||||
|
||||
p.credits {
|
||||
font-style: italic ;
|
||||
font-size: smaller }
|
||||
|
||||
p.label {
|
||||
white-space: nowrap }
|
||||
|
||||
p.rubric {
|
||||
font-weight: bold ;
|
||||
font-size: larger ;
|
||||
color: maroon ;
|
||||
text-align: center }
|
||||
|
||||
p.sidebar-title {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold ;
|
||||
font-size: larger }
|
||||
|
||||
p.sidebar-subtitle {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
p.topic-title {
|
||||
font-weight: bold }
|
||||
|
||||
pre.address {
|
||||
margin-bottom: 0 ;
|
||||
margin-top: 0 ;
|
||||
font: inherit }
|
||||
|
||||
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
pre.code .ln { color: gray; } /* line numbers */
|
||||
pre.code, code { background-color: #eeeeee }
|
||||
pre.code .comment, code .comment { color: #5C6576 }
|
||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||
|
||||
span.classifier {
|
||||
font-family: sans-serif ;
|
||||
font-style: oblique }
|
||||
|
||||
span.classifier-delimiter {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
span.interpreted {
|
||||
font-family: sans-serif }
|
||||
|
||||
span.option {
|
||||
white-space: nowrap }
|
||||
|
||||
span.pre {
|
||||
white-space: pre }
|
||||
|
||||
span.problematic, pre.problematic {
|
||||
color: red }
|
||||
|
||||
span.section-subtitle {
|
||||
/* font-size relative to parent (h1..h6 element) */
|
||||
font-size: 80% }
|
||||
|
||||
table.citation {
|
||||
border-left: solid 1px gray;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docinfo {
|
||||
margin: 2em 4em }
|
||||
|
||||
table.docutils {
|
||||
margin-top: 0.5em ;
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
table.footnote {
|
||||
border-left: solid 1px black;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docutils td, table.docutils th,
|
||||
table.docinfo td, table.docinfo th {
|
||||
padding-left: 0.5em ;
|
||||
padding-right: 0.5em ;
|
||||
vertical-align: top }
|
||||
|
||||
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||
font-weight: bold ;
|
||||
text-align: left ;
|
||||
white-space: nowrap ;
|
||||
padding-left: 0 }
|
||||
|
||||
/* "booktabs" style (no vertical lines) */
|
||||
table.docutils.booktabs {
|
||||
border: 0px;
|
||||
border-top: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.docutils.booktabs * {
|
||||
border: 0px;
|
||||
}
|
||||
table.docutils.booktabs th {
|
||||
border-bottom: thin solid;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||
font-size: 100% }
|
||||
|
||||
ul.auto-toc {
|
||||
list-style-type: none }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document" id="cetmix-tower-server-queue">
|
||||
<h1 class="title">Cetmix Tower Server Queue</h1>
|
||||
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:bcdbf27340bb59ec9a0cf443b108e2d6b27cf7c64466b47585fbd02410ef071b
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/cetmix/cetmix-tower/tree/16.0/cetmix_tower_server_queue"><img alt="cetmix/cetmix-tower" src="https://img.shields.io/badge/github-cetmix%2Fcetmix--tower-lightgray.png?logo=github" /></a></p>
|
||||
<p>This module implements asynchronous task execution for <a class="reference external" href="https://cetmix.com/tower">Cetmix
|
||||
Tower</a>.</p>
|
||||
<p>It requires the <a class="reference external" href="https://github.com/OCA/queue/queue_job">queue_job</a>
|
||||
module to be installed and configured in the Odoo instance.</p>
|
||||
<p>Please refer to the <a class="reference external" href="https://cetmix.com/tower">official
|
||||
documentation</a> for detailed information.</p>
|
||||
<p><strong>Table of contents</strong></p>
|
||||
<div class="contents local topic" id="contents">
|
||||
<ul class="simple">
|
||||
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a></li>
|
||||
<li><a class="reference internal" href="#usage" id="toc-entry-2">Usage</a></li>
|
||||
<li><a class="reference internal" href="#changelog" id="toc-entry-3">Changelog</a><ul>
|
||||
<li><a class="reference internal" href="#section-1" id="toc-entry-4">16.0.2.0.0 (2026-03-23)</a></li>
|
||||
<li><a class="reference internal" href="#section-2" id="toc-entry-5">16.0.1.2.0 (2025-11-12)</a></li>
|
||||
<li><a class="reference internal" href="#section-3" id="toc-entry-6">16.0.1.1.4 (2025-11-05)</a></li>
|
||||
<li><a class="reference internal" href="#section-4" id="toc-entry-7">16.0.1.1.3 (2025-10-13)</a></li>
|
||||
<li><a class="reference internal" href="#section-5" id="toc-entry-8">16.0.1.1.0 (2025-07-16)</a></li>
|
||||
<li><a class="reference internal" href="#section-6" id="toc-entry-9">16.0.1.0.2 (2025-05-16)</a></li>
|
||||
<li><a class="reference internal" href="#section-7" id="toc-entry-10">16.0.1.0.1 (2025-05-09)</a></li>
|
||||
<li><a class="reference internal" href="#section-8" id="toc-entry-11">16.0.1.0.0</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-12">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="toc-entry-13">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="toc-entry-14">Authors</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-15">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="configuration">
|
||||
<h1><a class="toc-backref" href="#toc-entry-1">Configuration</a></h1>
|
||||
<p>Please refer to the <a class="reference external" href="https://cetmix.com/tower">official
|
||||
documentation</a> for detailed configuration
|
||||
instructions.</p>
|
||||
</div>
|
||||
<div class="section" id="usage">
|
||||
<h1><a class="toc-backref" href="#toc-entry-2">Usage</a></h1>
|
||||
<p>Please refer to the <a class="reference external" href="https://cetmix.com/tower">official
|
||||
documentation</a> for detailed usage
|
||||
instructions.</p>
|
||||
</div>
|
||||
<div class="section" id="changelog">
|
||||
<h1><a class="toc-backref" href="#toc-entry-3">Changelog</a></h1>
|
||||
<div class="section" id="section-1">
|
||||
<h2><a class="toc-backref" href="#toc-entry-4">16.0.2.0.0 (2026-03-23)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Jets! (4700)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-2">
|
||||
<h2><a class="toc-backref" href="#toc-entry-5">16.0.1.2.0 (2025-11-12)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Use the ‘web_notify’ module to send user notifications.
|
||||
(5074)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-3">
|
||||
<h2><a class="toc-backref" href="#toc-entry-6">16.0.1.1.4 (2025-11-05)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Bugfixes: Finish multiple commands at once. (5062)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-4">
|
||||
<h2><a class="toc-backref" href="#toc-entry-7">16.0.1.1.3 (2025-10-13)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Terminate running flight plan manually (3410)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-5">
|
||||
<h2><a class="toc-backref" href="#toc-entry-8">16.0.1.1.0 (2025-07-16)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: cetmix_tower_server_queue: Add async file upload/download
|
||||
via job queue (3720)</li>
|
||||
<li>Features: Terminate command with error if job has failed (4718)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-6">
|
||||
<h2><a class="toc-backref" href="#toc-entry-9">16.0.1.0.2 (2025-05-16)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: ‘sudo’ parameter is not passed to command. (4678)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-7">
|
||||
<h2><a class="toc-backref" href="#toc-entry-10">16.0.1.0.1 (2025-05-09)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Bugfixes: Non-critical issues and performance improvements. (4611)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-8">
|
||||
<h2><a class="toc-backref" href="#toc-entry-11">16.0.1.0.0</a></h2>
|
||||
<p>Release for Odoo 16.0</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h1><a class="toc-backref" href="#toc-entry-12">Bug Tracker</a></h1>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/cetmix/cetmix-tower/issues">GitHub Issues</a>.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
<a class="reference external" href="https://github.com/cetmix/cetmix-tower/issues/new?body=module:%20cetmix_tower_server_queue%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
||||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||
</div>
|
||||
<div class="section" id="credits">
|
||||
<h1><a class="toc-backref" href="#toc-entry-13">Credits</a></h1>
|
||||
<div class="section" id="authors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-14">Authors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Cetmix</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h2><a class="toc-backref" href="#toc-entry-15">Maintainers</a></h2>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/cetmix/cetmix-tower/tree/16.0/cetmix_tower_server_queue">cetmix/cetmix-tower</a> project on GitHub.</p>
|
||||
<p>You are welcome to contribute.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +0,0 @@
|
||||
from . import test_command
|
||||
from . import test_command_log
|
||||
from . import test_file
|
||||
@@ -1,145 +0,0 @@
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.fields import Datetime
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
from odoo.addons.cetmix_tower_server.tests.common import TestTowerCommon
|
||||
|
||||
|
||||
class TestTowerCommand(TestTowerCommon):
|
||||
"""Test suite for verifying zombie command detection and related
|
||||
queue job cancellation.
|
||||
|
||||
Tests in this class verify that commands which have been running
|
||||
longer than the timeout are properly detected as zombies, and their
|
||||
associated queue jobs are cancelled.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
# Set command timeout to 10 seconds
|
||||
cls.env["ir.config_parameter"].sudo().set_param(
|
||||
"cetmix_tower_server.command_timeout", "10"
|
||||
)
|
||||
# Set old time to 20 seconds ago (older than timeout)
|
||||
# to simulate running command in past
|
||||
now = Datetime.now()
|
||||
cls.old_time = now - timedelta(seconds=20)
|
||||
|
||||
def _patch_command_runner(self, command_type, runner_method):
|
||||
"""Helper to patch a command runner to simulate a zombie command.
|
||||
|
||||
Args:
|
||||
command_type: Type of command runner to patch ('ssh' or 'python_code')
|
||||
runner_method: Original method to wrap
|
||||
|
||||
Returns:
|
||||
A context manager that applies the patch
|
||||
"""
|
||||
|
||||
def _wrapper(*args, **kwargs):
|
||||
# Modify args to disable log record finishing
|
||||
args = list(args)
|
||||
if len(args) > 1:
|
||||
args[1] = False # Set log_record to False
|
||||
return runner_method(*args, **kwargs)
|
||||
|
||||
return patch.object(
|
||||
self.registry["cx.tower.server"],
|
||||
f"_command_runner_{command_type}",
|
||||
_wrapper,
|
||||
)
|
||||
|
||||
def _verify_zombie_command_job_cancellation(self, command_action):
|
||||
"""Verify zombie command is detected and job is cancelled.
|
||||
|
||||
Args:
|
||||
command_action: Action type ('ssh_command' or 'python_code')
|
||||
"""
|
||||
# check zombie command logs
|
||||
domain = [
|
||||
("is_running", "=", True),
|
||||
("start_date", "=", self.old_time),
|
||||
("command_action", "=", command_action),
|
||||
]
|
||||
zombie_command_logs = self.env["cx.tower.command.log"].search(domain)
|
||||
|
||||
self.assertEqual(
|
||||
len(zombie_command_logs), 1, "Zombie command log should be created"
|
||||
)
|
||||
self.assertTrue(
|
||||
zombie_command_logs.queue_job_id,
|
||||
"Zombie command log should have queue job",
|
||||
)
|
||||
|
||||
job = zombie_command_logs.queue_job_id
|
||||
self.assertTrue(job.exists(), "Zombie command job should exist")
|
||||
|
||||
self.assertEqual(job.state, "pending", "Zombie command job should be pending")
|
||||
|
||||
# run process to kill zombie command
|
||||
self.server_test_1._check_zombie_commands()
|
||||
|
||||
# check that command log is cancelled
|
||||
self.assertEqual(
|
||||
job.state, "cancelled", "Zombie command job should be cancelled"
|
||||
)
|
||||
|
||||
def test_check_zombie_ssh_command_queue(self):
|
||||
"""
|
||||
Test that zombie ssh command is killed and job is cancelled
|
||||
"""
|
||||
# Create test commands
|
||||
ssh_command = self.Command.create(
|
||||
{
|
||||
"name": "Test SSH Command",
|
||||
"code": "ls -la",
|
||||
"action": "ssh_command",
|
||||
}
|
||||
)
|
||||
|
||||
# patch command runner to not finish log record
|
||||
cx_tower_server_obj = self.registry["cx.tower.server"]
|
||||
_command_runner_ssh_super = cx_tower_server_obj._command_runner_ssh
|
||||
|
||||
with self._patch_command_runner("ssh", _command_runner_ssh_super):
|
||||
# run zombie command with log creation in past
|
||||
self.server_test_1.run_command(
|
||||
ssh_command, log={"start_date": self.old_time}
|
||||
)
|
||||
|
||||
# check zombie command logs
|
||||
self._verify_zombie_command_job_cancellation("ssh_command")
|
||||
|
||||
@mute_logger("py.warnings")
|
||||
def test_check_zombie_python_command_queue(self):
|
||||
"""
|
||||
Test that zombie python command is killed and job is cancelled
|
||||
"""
|
||||
# Create test commands
|
||||
python_command = self.Command.create(
|
||||
{
|
||||
"name": "Test Python Command",
|
||||
"code": "print('test')",
|
||||
"action": "python_code",
|
||||
}
|
||||
)
|
||||
|
||||
# patch command runner to not finish log record
|
||||
cx_tower_server_obj = self.registry["cx.tower.server"]
|
||||
_command_runner_python_code_super = (
|
||||
cx_tower_server_obj._command_runner_python_code
|
||||
)
|
||||
|
||||
with self._patch_command_runner(
|
||||
"python_code", _command_runner_python_code_super
|
||||
):
|
||||
# run zombie command with log creation in past
|
||||
self.server_test_1.run_command(
|
||||
python_command, log={"start_date": self.old_time}
|
||||
)
|
||||
|
||||
# check zombie command logs
|
||||
self._verify_zombie_command_job_cancellation("python_code")
|
||||
@@ -1,37 +0,0 @@
|
||||
from odoo.addons.cetmix_tower_server.tests.common import TestTowerCommon
|
||||
from odoo.addons.queue_job.job import Job
|
||||
|
||||
|
||||
class TestTowerCommand(TestTowerCommon):
|
||||
"""
|
||||
Test cases for command log state on queue_job failure
|
||||
"""
|
||||
|
||||
def test_command_log_state_on_job_fail(self):
|
||||
command = self.env["cx.tower.command"].create(
|
||||
{
|
||||
"name": "Test Command",
|
||||
"action": "ssh_command",
|
||||
"code": "echo 'Hello World'",
|
||||
}
|
||||
)
|
||||
self.assertTrue(command.id, "Command should be created successfully")
|
||||
|
||||
self.server_test_1.run_command(command=command)
|
||||
command_log = self.env["cx.tower.command.log"].search(
|
||||
[("command_id", "=", command.id)], order="id desc", limit=1
|
||||
)
|
||||
self.assertTrue(command_log, "Command log should be created")
|
||||
|
||||
job = command_log.queue_job_id
|
||||
self.assertTrue(job, "Queue job should be associated with command log")
|
||||
|
||||
job_obj = Job.load(self.env, job.uuid)
|
||||
job_obj.set_failed()
|
||||
job_obj.store()
|
||||
self.assertEqual(job.state, "failed", "Job should be in failed state")
|
||||
self.assertEqual(
|
||||
command_log.command_status,
|
||||
self.env["queue.job"].QUEUE_JOB_ERROR,
|
||||
"Command log should be in failed state",
|
||||
)
|
||||
@@ -1,201 +0,0 @@
|
||||
from odoo import exceptions
|
||||
|
||||
from odoo.addons.cetmix_tower_server.tests.common import TestTowerCommon
|
||||
from odoo.addons.queue_job.tests.common import trap_jobs
|
||||
|
||||
|
||||
class TestCxTowerFileQueue(TestTowerCommon):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.file_template = self.FileTemplate.create(
|
||||
{
|
||||
"name": "Test",
|
||||
"file_name": "test.txt",
|
||||
"server_dir": "/var/tmp",
|
||||
"code": "Hello, world!",
|
||||
}
|
||||
)
|
||||
|
||||
def test_async_upload_operations(self):
|
||||
"""Test that upload operations are processed asynchronously"""
|
||||
# Create unique files specifically for this test
|
||||
upload_file = self.File.create(
|
||||
{
|
||||
"source": "tower",
|
||||
"template_id": self.file_template.id,
|
||||
"server_id": self.server_test_1.id,
|
||||
"name": "upload_test_1",
|
||||
"auto_sync": False,
|
||||
}
|
||||
)
|
||||
|
||||
upload_file_2 = self.File.create(
|
||||
{
|
||||
"name": "upload_test_2",
|
||||
"source": "server",
|
||||
"server_id": self.server_test_1.id,
|
||||
"server_dir": "/var/tmp",
|
||||
"auto_sync": False,
|
||||
}
|
||||
)
|
||||
|
||||
with trap_jobs() as trap:
|
||||
upload_file.upload()
|
||||
upload_file_2.upload()
|
||||
|
||||
self.assertEqual(len(trap.enqueued_jobs), 2)
|
||||
|
||||
upload_file.write({"server_response": "ok", "is_being_processed": False})
|
||||
upload_file_2.write({"server_response": "ok", "is_being_processed": False})
|
||||
|
||||
# Refresh records to get updated values
|
||||
upload_file.invalidate_recordset()
|
||||
upload_file_2.invalidate_recordset()
|
||||
|
||||
# Verify the expected state
|
||||
self.assertEqual(upload_file.server_response, "ok")
|
||||
self.assertFalse(upload_file.is_being_processed)
|
||||
|
||||
self.assertEqual(upload_file_2.server_response, "ok")
|
||||
self.assertFalse(upload_file_2.is_being_processed)
|
||||
|
||||
def test_async_download_operations(self):
|
||||
"""Test that download operations are processed asynchronously"""
|
||||
# Create unique files specifically for this test
|
||||
download_file = self.File.create(
|
||||
{
|
||||
"source": "tower",
|
||||
"template_id": self.file_template.id,
|
||||
"server_id": self.server_test_1.id,
|
||||
"name": "download_test_1",
|
||||
"auto_sync": False,
|
||||
}
|
||||
)
|
||||
|
||||
download_file_2 = self.File.create(
|
||||
{
|
||||
"name": "download_test_2",
|
||||
"source": "server",
|
||||
"server_id": self.server_test_1.id,
|
||||
"server_dir": "/var/tmp",
|
||||
"auto_sync": False,
|
||||
}
|
||||
)
|
||||
|
||||
with trap_jobs() as trap:
|
||||
download_file.download()
|
||||
download_file_2.download()
|
||||
|
||||
# Verify jobs were created
|
||||
self.assertEqual(len(trap.enqueued_jobs), 2)
|
||||
|
||||
download_file.write({"server_response": "ok", "is_being_processed": False})
|
||||
download_file_2.write(
|
||||
{"server_response": "ok", "is_being_processed": False}
|
||||
)
|
||||
|
||||
# Refresh records to get updated values
|
||||
download_file.invalidate_recordset()
|
||||
download_file_2.invalidate_recordset()
|
||||
|
||||
# Verify the expected state
|
||||
self.assertEqual(download_file.server_response, "ok")
|
||||
self.assertFalse(download_file.is_being_processed)
|
||||
|
||||
self.assertEqual(download_file_2.server_response, "ok")
|
||||
self.assertFalse(download_file_2.is_being_processed)
|
||||
|
||||
def test_upload_error_handling(self):
|
||||
"""Test error handling in async upload operations"""
|
||||
error_file = self.File.create(
|
||||
{
|
||||
"source": "tower",
|
||||
"template_id": self.file_template.id,
|
||||
"server_id": self.server_test_1.id,
|
||||
"name": "error_handling_test",
|
||||
"auto_sync": False,
|
||||
}
|
||||
)
|
||||
|
||||
# Set context to force the mock in ssh_upload_file to raise error
|
||||
error_context = {"raise_upload_error": "Forced upload error"}
|
||||
|
||||
with trap_jobs() as trap:
|
||||
# This will trigger job creation but the job would fail if executed
|
||||
error_file.with_context(**error_context).upload(raise_error=True)
|
||||
|
||||
# Verify job was created
|
||||
self.assertEqual(len(trap.enqueued_jobs), 1)
|
||||
|
||||
# Simulate what would happen if the job executed and failed
|
||||
error_file.write({"server_response": "error", "is_being_processed": False})
|
||||
error_file.invalidate_recordset()
|
||||
|
||||
self.assertEqual(error_file.server_response, "error")
|
||||
self.assertFalse(error_file.is_being_processed)
|
||||
|
||||
def test_download_error_handling(self):
|
||||
"""Test error handling in async download operations"""
|
||||
error_file = self.File.create(
|
||||
{
|
||||
"source": "server",
|
||||
"server_id": self.server_test_1.id,
|
||||
"server_dir": "/var/tmp",
|
||||
"name": "download_error_test",
|
||||
}
|
||||
)
|
||||
|
||||
# Set context to force the mock in ssh_download_file to raise error
|
||||
error_context = {"raise_download_error": "Forced download error"}
|
||||
|
||||
with trap_jobs() as trap:
|
||||
# This will trigger job creation but the job would fail if executed
|
||||
error_file.with_context(**error_context).download(raise_error=True)
|
||||
|
||||
# Verify job was created
|
||||
self.assertEqual(len(trap.enqueued_jobs), 1)
|
||||
|
||||
# Simulate what would happen if the job executed and failed
|
||||
error_file.write({"server_response": "error", "is_being_processed": False})
|
||||
error_file.invalidate_recordset()
|
||||
|
||||
self.assertEqual(error_file.server_response, "error")
|
||||
self.assertFalse(error_file.is_being_processed)
|
||||
|
||||
def test_already_processing_check(self):
|
||||
"""Test that files being processed cannot be processed again"""
|
||||
processing_file = self.File.create(
|
||||
{
|
||||
"source": "tower",
|
||||
"template_id": self.file_template.id,
|
||||
"server_id": self.server_test_1.id,
|
||||
"name": "processing_test_file",
|
||||
"is_being_processed": True,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertTrue(processing_file.is_being_processed)
|
||||
|
||||
# Test with raising error
|
||||
with self.assertRaises(exceptions.UserError):
|
||||
processing_file.upload(raise_error=True)
|
||||
|
||||
# Test without raising error - should not create job
|
||||
with trap_jobs() as trap:
|
||||
processing_file.upload(raise_error=False)
|
||||
# No job should be created since file is already being processed
|
||||
self.assertEqual(len(trap.enqueued_jobs), 0)
|
||||
|
||||
# Verify still marked as processing
|
||||
self.assertTrue(processing_file.is_being_processed)
|
||||
|
||||
# Same tests for download
|
||||
with self.assertRaises(exceptions.UserError):
|
||||
processing_file.download(raise_error=True)
|
||||
|
||||
with trap_jobs() as trap:
|
||||
processing_file.download(raise_error=False)
|
||||
# No job should be created
|
||||
self.assertEqual(len(trap.enqueued_jobs), 0)
|
||||
|
||||
self.assertTrue(processing_file.is_being_processed)
|
||||
@@ -1,20 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="cx_tower_command_log_view_form" model="ir.ui.view">
|
||||
<field name="name">cx.tower.command.log.view.form</field>
|
||||
<field name="model">cx.tower.command.log</field>
|
||||
<field
|
||||
name="inherit_id"
|
||||
ref="cetmix_tower_server.cx_tower_command_log_view_form"
|
||||
/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='command_id']" position="after">
|
||||
<field
|
||||
name="queue_job_id"
|
||||
attrs="{'invisible': [('queue_job_id', '=', False)]}"
|
||||
/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,56 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="cx_tower_file_view_form" model="ir.ui.view">
|
||||
<field name="name">cx.tower.file.view.form</field>
|
||||
<field name="model">cx.tower.file</field>
|
||||
<field name="inherit_id" ref="cetmix_tower_server.cx_tower_file_view_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//form/sheet/group" position="before">
|
||||
<field name="is_being_processed" invisible="1" />
|
||||
<field name="server_response" invisible="1" />
|
||||
<widget
|
||||
name="web_ribbon"
|
||||
title="Processing"
|
||||
bg_color="bg-info"
|
||||
attrs="{'invisible': [('is_being_processed', '=', False)]}"
|
||||
/>
|
||||
<widget
|
||||
name="web_ribbon"
|
||||
title="Success"
|
||||
bg_color="bg-success"
|
||||
attrs="{'invisible': ['|', ('is_being_processed', '=', True), ('server_response', '!=', 'ok')]}"
|
||||
/>
|
||||
<widget
|
||||
name="web_ribbon"
|
||||
title="Error"
|
||||
bg_color="bg-danger"
|
||||
attrs="{'invisible': ['|', ('is_being_processed', '=', True), ('server_response', 'in', ('ok', False))]}"
|
||||
/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="cx_tower_queue_file_view_tree" model="ir.ui.view">
|
||||
<field name="name">cx.tower.queue.file.view.tree</field>
|
||||
<field name="model">cx.tower.file</field>
|
||||
<field name="inherit_id" ref="cetmix_tower_server.cx_tower_file_view_tree" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//tree" position="inside">
|
||||
<field name="is_being_processed" invisible="1" />
|
||||
<field name="server_response" invisible="1" />
|
||||
</xpath>
|
||||
<xpath expr="//tree" position="attributes">
|
||||
<attribute name="decoration-info">
|
||||
is_being_processed == True
|
||||
</attribute>
|
||||
<attribute name="decoration-success">
|
||||
is_being_processed != True and server_response == 'ok'
|
||||
</attribute>
|
||||
<attribute name="decoration-danger">
|
||||
is_being_processed != True and server_response not in ('ok', False)
|
||||
</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
20
addons/odoosky_demo/__manifest__.py
Normal file
20
addons/odoosky_demo/__manifest__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "OdooSky Demo Addon",
|
||||
"summary": "Smoke-test addon — proves the v3 in-cluster build pipeline works end-to-end.",
|
||||
"description": """
|
||||
A minimal Odoo module used by OdooSky platform tests to prove that a tagged
|
||||
addon in odoo-tower/odoo-addons builds into an OCI image inside the customer
|
||||
cluster and mounts into the Odoo pod. No business logic; just a manifest and
|
||||
an empty __init__.py so Odoo recognizes it.
|
||||
""",
|
||||
"author": "OdooSky",
|
||||
"website": "https://odoosky.org",
|
||||
"category": "Tools",
|
||||
"version": "18.0.1.0.0",
|
||||
"license": "LGPL-3",
|
||||
"depends": ["base"],
|
||||
"data": [],
|
||||
"installable": True,
|
||||
"application": False,
|
||||
"auto_install": False,
|
||||
}
|
||||
1
addons/odoosky_demo/static/description/index.html
Normal file
1
addons/odoosky_demo/static/description/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<section><h1>OdooSky Demo Addon</h1><p>Platform smoke-test addon. Safe to install / uninstall.</p></section>
|
||||
Reference in New Issue
Block a user