diff --git a/queue_job/README.rst b/queue_job/README.rst new file mode 100644 index 0000000000..5c0ad323a5 --- /dev/null +++ b/queue_job/README.rst @@ -0,0 +1,685 @@ +========= +Job Queue +========= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:b9e93efc8b0afac10d4666c9b35305aac8ac679488e6ac1ad649adb7bac65f4a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Mature-brightgreen.png + :target: https://odoo-community.org/page/development-status + :alt: Mature +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fqueue-lightgray.png?logo=github + :target: https://github.com/OCA/queue/tree/18.0/queue_job + :alt: OCA/queue +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/queue-18-0/queue-18-0-queue_job + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/queue&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This addon adds an integrated Job Queue to Odoo. + +It allows to postpone method calls executed asynchronously. + +Jobs are executed in the background by a ``Jobrunner``, in their own +transaction. + +Example: + +.. code:: python + + from odoo import models, fields, api + + class MyModel(models.Model): + _name = 'my.model' + + def my_method(self, a, k=None): + _logger.info('executed with a: %s and k: %s', a, k) + + + class MyOtherModel(models.Model): + _name = 'my.other.model' + + def button_do_stuff(self): + self.env['my.model'].with_delay().my_method('a', k=2) + +In the snippet of code above, when we call ``button_do_stuff``, a job +**capturing the method and arguments** will be postponed. It will be +executed as soon as the Jobrunner has a free bucket, which can be +instantaneous if no other job is running. + +Features: + +- Views for jobs, jobs are stored in PostgreSQL +- Jobrunner: execute the jobs, highly efficient thanks to PostgreSQL's + NOTIFY +- Channels: give a capacity for the root channel and its sub-channels + and segregate jobs in them. Allow for instance to restrict heavy jobs + to be executed one at a time while little ones are executed 4 at a + times. +- Retries: Ability to retry jobs by raising a type of exception +- Retry Pattern: the 3 first tries, retry after 10 seconds, the 5 next + tries, retry after 1 minutes, ... +- Job properties: priorities, estimated time of arrival (ETA), custom + description, number of retries +- Related Actions: link an action on the job view, such as open the + record concerned by the job + +**Table of contents** + +.. contents:: + :local: + +Installation +============ + +Be sure to have the ``requests`` library. + +Configuration +============= + +- Using environment variables and command line: + + - Adjust environment variables (optional): + + - ``ODOO_QUEUE_JOB_CHANNELS=root:4`` or any other channels + configuration. The default is ``root:1`` + - if ``xmlrpc_port`` is not set: ``ODOO_QUEUE_JOB_PORT=8069`` + + - Start Odoo with ``--load=web,queue_job`` and ``--workers`` greater + than 1. [1]_ + +- Using the Odoo configuration file: + +.. code:: ini + + [options] + (...) + workers = 6 + server_wide_modules = web,queue_job + + (...) + [queue_job] + channels = root:2 + +- Confirm the runner is starting correctly by checking the odoo log + file: + +:: + + ...INFO...queue_job.jobrunner.runner: starting + ...INFO...queue_job.jobrunner.runner: initializing database connections + ...INFO...queue_job.jobrunner.runner: queue job runner ready for db + ...INFO...queue_job.jobrunner.runner: database connections ready + +- Create jobs (eg using ``base_import_async``) and observe they start + immediately and in parallel. +- Tip: to enable debug logging for the queue job, use + ``--log-handler=odoo.addons.queue_job:DEBUG`` + +.. [1] + It works with the threaded Odoo server too, although this way of + running Odoo is obviously not for production purposes. + +Usage +===== + +To use this module, you need to: + +1. Go to ``Job Queue`` menu + +Developers +---------- + +Delaying jobs +~~~~~~~~~~~~~ + +The fast way to enqueue a job for a method is to use ``with_delay()`` on +a record or model: + +.. code:: python + + def button_done(self): + self.with_delay().print_confirmation_document(self.state) + self.write({"state": "done"}) + return True + +Here, the method ``print_confirmation_document()`` will be executed +asynchronously as a job. ``with_delay()`` can take several parameters to +define more precisely how the job is executed (priority, ...). + +All the arguments passed to the method being delayed are stored in the +job and passed to the method when it is executed asynchronously, +including ``self``, so the current record is maintained during the job +execution (warning: the context is not kept). + +Dependencies can be expressed between jobs. To start a graph of jobs, +use ``delayable()`` on a record or model. The following is the +equivalent of ``with_delay()`` but using the long form: + +.. code:: python + + def button_done(self): + delayable = self.delayable() + delayable.print_confirmation_document(self.state) + delayable.delay() + self.write({"state": "done"}) + return True + +Methods of Delayable objects return itself, so it can be used as a +builder pattern, which in some cases allow to build the jobs +dynamically: + +.. code:: python + + def button_generate_simple_with_delayable(self): + self.ensure_one() + # Introduction of a delayable object, using a builder pattern + # allowing to chain jobs or set properties. The delay() method + # on the delayable object actually stores the delayable objects + # in the queue_job table + ( + self.delayable() + .generate_thumbnail((50, 50)) + .set(priority=30) + .set(description=_("generate xxx")) + .delay() + ) + +The simplest way to define a dependency is to use ``.on_done(job)`` on a +Delayable: + +.. code:: python + + def button_chain_done(self): + self.ensure_one() + job1 = self.browse(1).delayable().generate_thumbnail((50, 50)) + job2 = self.browse(1).delayable().generate_thumbnail((50, 50)) + job3 = self.browse(1).delayable().generate_thumbnail((50, 50)) + # job 3 is executed when job 2 is done which is executed when job 1 is done + job1.on_done(job2.on_done(job3)).delay() + +Delayables can be chained to form more complex graphs using the +``chain()`` and ``group()`` primitives. A chain represents a sequence of +jobs to execute in order, a group represents jobs which can be executed +in parallel. Using ``chain()`` has the same effect as using several +nested ``on_done()`` but is more readable. Both can be combined to form +a graph, for instance we can group [A] of jobs, which blocks another +group [B] of jobs. When and only when all the jobs of the group [A] are +executed, the jobs of the group [B] are executed. The code would look +like: + +.. code:: python + + from odoo.addons.queue_job.delay import group, chain + + def button_done(self): + group_a = group(self.delayable().method_foo(), self.delayable().method_bar()) + group_b = group(self.delayable().method_baz(1), self.delayable().method_baz(2)) + chain(group_a, group_b).delay() + self.write({"state": "done"}) + return True + +When a failure happens in a graph of jobs, the execution of the jobs +that depend on the failed job stops. They remain in a state +``wait_dependencies`` until their "parent" job is successful. This can +happen in two ways: either the parent job retries and is successful on a +second try, either the parent job is manually "set to done" by a user. +In these two cases, the dependency is resolved and the graph will +continue to be processed. Alternatively, the failed job and all its +dependent jobs can be canceled by a user. The other jobs of the graph +that do not depend on the failed job continue their execution in any +case. + +Note: ``delay()`` must be called on the delayable, chain, or group which +is at the top of the graph. In the example above, if it was called on +``group_a``, then ``group_b`` would never be delayed (but a warning +would be shown). + +Enqueing Job Options +~~~~~~~~~~~~~~~~~~~~ + +- priority: default is 10, the closest it is to 0, the faster it will + be executed +- eta: Estimated Time of Arrival of the job. It will not be executed + before this date/time +- max_retries: default is 5, maximum number of retries before giving up + and set the job state to 'failed'. A value of 0 means infinite + retries. +- description: human description of the job. If not set, description is + computed from the function doc or method name +- channel: the complete name of the channel to use to process the + function. If specified it overrides the one defined on the function +- identity_key: key uniquely identifying the job, if specified and a + job with the same key has not yet been run, the new job will not be + created + +Configure default options for jobs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In earlier versions, jobs could be configured using the ``@job`` +decorator. This is now obsolete, they can be configured using optional +``queue.job.function`` and ``queue.job.channel`` XML records. + +Example of channel: + +.. code:: XML + + + sale + + + +Example of job function: + +.. code:: XML + + + + action_done + + + + + +The general form for the ``name`` is: ``.method``. + +The channel, related action and retry pattern options are optional, they +are documented below. + +When writing modules, if 2+ modules add a job function or channel with +the same name (and parent for channels), they'll be merged in the same +record, even if they have different xmlids. On uninstall, the merged +record is deleted when all the modules using it are uninstalled. + +**Job function: model** + +If the function is defined in an abstract model, you can not write +```` +but you have to define a function for each model that inherits from the +abstract model. + +**Job function: channel** + +The channel where the job will be delayed. The default channel is +``root``. + +**Job function: related action** + +The *Related Action* appears as a button on the Job's view. The button +will execute the defined action. + +The default one is to open the view of the record related to the job +(form view when there is a single record, list view for several +records). In many cases, the default related action is enough and +doesn't need customization, but it can be customized by providing a +dictionary on the job function: + +.. code:: python + + { + "enable": False, + "func_name": "related_action_partner", + "kwargs": {"name": "Partner"}, + } + +- ``enable``: when ``False``, the button has no effect (default: + ``True``) +- ``func_name``: name of the method on ``queue.job`` that returns an + action +- ``kwargs``: extra arguments to pass to the related action method + +Example of related action code: + +.. code:: python + + class QueueJob(models.Model): + _inherit = 'queue.job' + + def related_action_partner(self, name): + self.ensure_one() + model = self.model_name + partner = self.records + action = { + 'name': name, + 'type': 'ir.actions.act_window', + 'res_model': model, + 'view_type': 'form', + 'view_mode': 'form', + 'res_id': partner.id, + } + return action + +**Job function: retry pattern** + +When a job fails with a retryable error type, it is automatically +retried later. By default, the retry is always 10 minutes later. + +A retry pattern can be configured on the job function. What a pattern +represents is "from X tries, postpone to Y seconds". It is expressed as +a dictionary where keys are tries and values are seconds to postpone as +integers: + +.. code:: python + + { + 1: 10, + 5: 20, + 10: 30, + 15: 300, + } + +Based on this configuration, we can tell that: + +- 5 first retries are postponed 10 seconds later +- retries 5 to 10 postponed 20 seconds later +- retries 10 to 15 postponed 30 seconds later +- all subsequent retries postponed 5 minutes later + +**Job Context** + +The context of the recordset of the job, or any recordset passed in +arguments of a job, is transferred to the job according to an +allow-list. + +The default allow-list is ("tz", "lang", "allowed_company_ids", +"force_company", "active_test"). It can be customized in +``Base._job_prepare_context_before_enqueue_keys``. **Bypass jobs on +running Odoo** + +When you are developing (ie: connector modules) you might want to bypass +the queue job and run your code immediately. + +To do so you can set QUEUE_JOB\__NO_DELAY=1 in your enviroment. + +**Bypass jobs in tests** + +When writing tests on job-related methods is always tricky to deal with +delayed recordsets. To make your testing life easier you can set +queue_job\__no_delay=True in the context. + +Tip: you can do this at test case level like this + +.. code:: python + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict( + cls.env.context, + queue_job__no_delay=True, # no jobs thanks + )) + +Then all your tests execute the job methods synchronously without +delaying any jobs. + +Testing +~~~~~~~ + +**Asserting enqueued jobs** + +The recommended way to test jobs, rather than running them directly and +synchronously is to split the tests in two parts: + + - one test where the job is mocked (trap jobs with ``trap_jobs()`` + and the test only verifies that the job has been delayed with the + expected arguments + - one test that only calls the method of the job synchronously, to + validate the proper behavior of this method only + +Proceeding this way means that you can prove that jobs will be enqueued +properly at runtime, and it ensures your code does not have a different +behavior in tests and in production (because running your jobs +synchronously may have a different behavior as they are in the same +transaction / in the middle of the method). Additionally, it gives more +control on the arguments you want to pass when calling the job's method +(synchronously, this time, in the second type of tests), and it makes +tests smaller. + +The best way to run such assertions on the enqueued jobs is to use +``odoo.addons.queue_job.tests.common.trap_jobs()``. + +A very small example (more details in ``tests/common.py``): + +.. code:: python + + # code + def my_job_method(self, name, count): + self.write({"name": " ".join([name] * count) + + def method_to_test(self): + count = self.env["other.model"].search_count([]) + self.with_delay(priority=15).my_job_method("Hi!", count=count) + return count + + # tests + from odoo.addons.queue_job.tests.common import trap_jobs + + # first test only check the expected behavior of the method and the proper + # enqueuing of jobs + def test_method_to_test(self): + with trap_jobs() as trap: + result = self.env["model"].method_to_test() + expected_count = 12 + + trap.assert_jobs_count(1, only=self.env["model"].my_job_method) + trap.assert_enqueued_job( + self.env["model"].my_job_method, + args=("Hi!",), + kwargs=dict(count=expected_count), + properties=dict(priority=15) + ) + self.assertEqual(result, expected_count) + + + # second test to validate the behavior of the job unitarily + def test_my_job_method(self): + record = self.env["model"].browse(1) + record.my_job_method("Hi!", count=12) + self.assertEqual(record.name, "Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi!") + +If you prefer, you can still test the whole thing in a single test, by +calling ``jobs_tester.perform_enqueued_jobs()`` in your test. + +.. code:: python + + def test_method_to_test(self): + with trap_jobs() as trap: + result = self.env["model"].method_to_test() + expected_count = 12 + + trap.assert_jobs_count(1, only=self.env["model"].my_job_method) + trap.assert_enqueued_job( + self.env["model"].my_job_method, + args=("Hi!",), + kwargs=dict(count=expected_count), + properties=dict(priority=15) + ) + self.assertEqual(result, expected_count) + + trap.perform_enqueued_jobs() + + record = self.env["model"].browse(1) + record.my_job_method("Hi!", count=12) + self.assertEqual(record.name, "Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi!") + +**Execute jobs synchronously when running Odoo** + +When you are developing (ie: connector modules) you might want to bypass +the queue job and run your code immediately. + +To do so you can set ``QUEUE_JOB__NO_DELAY=1`` in your environment. + +Warning + +Do not do this in production + +**Execute jobs synchronously in tests** + +You should use ``trap_jobs``, really, but if for any reason you could +not use it, and still need to have job methods executed synchronously in +your tests, you can do so by setting ``queue_job__no_delay=True`` in the +context. + +Tip: you can do this at test case level like this + +.. code:: python + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict( + cls.env.context, + queue_job__no_delay=True, # no jobs thanks + )) + +Then all your tests execute the job methods synchronously without +delaying any jobs. + +In tests you'll have to mute the logger like: + + @mute_logger('odoo.addons.queue_job.models.base') + +Note + +in graphs of jobs, the ``queue_job__no_delay`` context key must be in at +least one job's env of the graph for the whole graph to be executed +synchronously + +Tips and tricks +~~~~~~~~~~~~~~~ + +- **Idempotency** + (https://www.restapitutorial.com/lessons/idempotency.html): The + queue_job should be idempotent so they can be retried several times + without impact on the data. +- **The job should test at the very beginning its relevance**: the + moment the job will be executed is unknown by design. So the first + task of a job should be to check if the related work is still + relevant at the moment of the execution. + +Patterns +~~~~~~~~ + +Through the time, two main patterns emerged: + +1. For data exposed to users, a model should store the data and the + model should be the creator of the job. The job is kept hidden from + the users +2. For technical data, that are not exposed to the users, it is + generally alright to create directly jobs with data passed as + arguments to the job, without intermediary models. + +Known issues / Roadmap +====================== + +- After creating a new database or installing ``queue_job`` on an + existing database, Odoo must be restarted for the runner to detect + it. +- When Odoo shuts down normally, it waits for running jobs to finish. + However, when the Odoo server crashes or is otherwise force-stopped, + running jobs are interrupted while the runner has no chance to know + they have been aborted. In such situations, jobs may remain in + ``started`` or ``enqueued`` state after the Odoo server is halted. + Since the runner has no way to know if they are actually running or + not, and does not know for sure if it is safe to restart the jobs, it + does not attempt to restart them automatically. Such stale jobs + therefore fill the running queue and prevent other jobs to start. You + must therefore requeue them manually, either from the Jobs view, or + by running the following SQL statement *before starting Odoo*: + +.. code:: sql + + update queue_job set state='pending' where state in ('started', 'enqueued') + +Changelog +========= + +Next +---- + +- [ADD] Run jobrunner as a worker process instead of a thread in the + main process (when running with --workers > 0) +- [REF] ``@job`` and ``@related_action`` deprecated, any method can be + delayed, and configured using ``queue.job.function`` records +- [MIGRATION] from 13.0 branched at rev. e24ff4b + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Camptocamp +* ACSONE SA/NV + +Contributors +------------ + +- Guewen Baconnier +- Stéphane Bidoul +- Matthieu Dietrich +- Jos De Graeve +- David Lefever +- Laurent Mignon +- Laetitia Gangloff +- Cédric Pigeon +- Tatiana Deribina +- Souheil Bejaoui +- Eric Antones +- Simone Orsi +- Nguyen Minh Chien +- Tran Quoc Duong +- Vo Hong Thien + +Other credits +------------- + +The migration of this module from 17.0 to 18.0 was financially supported +by Camptocamp. + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-guewen| image:: https://github.com/guewen.png?size=40px + :target: https://github.com/guewen + :alt: guewen + +Current `maintainer `__: + +|maintainer-guewen| + +This module is part of the `OCA/queue `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/queue_job/__init__.py b/queue_job/__init__.py new file mode 100644 index 0000000000..6ca18c3309 --- /dev/null +++ b/queue_job/__init__.py @@ -0,0 +1,10 @@ +from . import controllers +from . import fields +from . import models +from . import wizards +from . import jobrunner +from .post_init_hook import post_init_hook +from .post_load import post_load + +# shortcuts +from .job import identity_exact diff --git a/queue_job/__manifest__.py b/queue_job/__manifest__.py new file mode 100644 index 0000000000..e30f5f3e05 --- /dev/null +++ b/queue_job/__manifest__.py @@ -0,0 +1,35 @@ +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +{ + "name": "Job Queue", + "version": "18.0.1.0.0", + "author": "Camptocamp,ACSONE SA/NV,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/queue", + "license": "LGPL-3", + "category": "Generic Modules", + "depends": ["mail", "base_sparse_field", "web"], + "external_dependencies": {"python": ["requests"]}, + "data": [ + "security/security.xml", + "security/ir.model.access.csv", + "views/queue_job_views.xml", + "views/queue_job_channel_views.xml", + "views/queue_job_function_views.xml", + "wizards/queue_jobs_to_done_views.xml", + "wizards/queue_jobs_to_cancelled_views.xml", + "wizards/queue_requeue_job_views.xml", + "views/queue_job_menus.xml", + "data/queue_data.xml", + "data/queue_job_function_data.xml", + ], + "assets": { + "web.assets_backend": [ + "/queue_job/static/src/views/**/*", + ], + }, + "installable": True, + "development_status": "Mature", + "maintainers": ["guewen"], + "post_init_hook": "post_init_hook", + "post_load": "post_load", +} diff --git a/queue_job/controllers/__init__.py b/queue_job/controllers/__init__.py new file mode 100644 index 0000000000..12a7e529b6 --- /dev/null +++ b/queue_job/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/queue_job/controllers/main.py b/queue_job/controllers/main.py new file mode 100644 index 0000000000..13288f03de --- /dev/null +++ b/queue_job/controllers/main.py @@ -0,0 +1,289 @@ +# Copyright (c) 2015-2016 ACSONE SA/NV () +# Copyright 2013-2016 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +import logging +import random +import time +import traceback +from io import StringIO + +from psycopg2 import OperationalError, errorcodes +from werkzeug.exceptions import BadRequest, Forbidden + +from odoo import SUPERUSER_ID, _, api, http, registry +from odoo.service.model import PG_CONCURRENCY_ERRORS_TO_RETRY + +from ..delay import chain, group +from ..exception import FailedJobError, RetryableJobError +from ..job import ENQUEUED, Job + +_logger = logging.getLogger(__name__) + +PG_RETRY = 5 # seconds + +DEPENDS_MAX_TRIES_ON_CONCURRENCY_FAILURE = 5 + + +class RunJobController(http.Controller): + def _try_perform_job(self, env, job): + """Try to perform the job.""" + job.set_started() + job.store() + env.cr.commit() + _logger.debug("%s started", job) + + job.perform() + # Triggers any stored computed fields before calling 'set_done' + # so that will be part of the 'exec_time' + env.flush_all() + job.set_done() + job.store() + env.flush_all() + env.cr.commit() + _logger.debug("%s done", job) + + def _enqueue_dependent_jobs(self, env, job): + tries = 0 + while True: + try: + job.enqueue_waiting() + except OperationalError as err: + # Automatically retry the typical transaction serialization + # errors + if err.pgcode not in PG_CONCURRENCY_ERRORS_TO_RETRY: + raise + if tries >= DEPENDS_MAX_TRIES_ON_CONCURRENCY_FAILURE: + _logger.info( + "%s, maximum number of tries reached to update dependencies", + errorcodes.lookup(err.pgcode), + ) + raise + wait_time = random.uniform(0.0, 2**tries) + tries += 1 + _logger.info( + "%s, retry %d/%d in %.04f sec...", + errorcodes.lookup(err.pgcode), + tries, + DEPENDS_MAX_TRIES_ON_CONCURRENCY_FAILURE, + wait_time, + ) + time.sleep(wait_time) + else: + break + + @http.route("/queue_job/runjob", type="http", auth="none", save_session=False) + def runjob(self, db, job_uuid, **kw): + http.request.session.db = db + env = http.request.env(user=SUPERUSER_ID) + + def retry_postpone(job, message, seconds=None): + job.env.clear() + with registry(job.env.cr.dbname).cursor() as new_cr: + job.env = api.Environment(new_cr, SUPERUSER_ID, {}) + job.postpone(result=message, seconds=seconds) + job.set_pending(reset_retry=False) + job.store() + + # ensure the job to run is in the correct state and lock the record + env.cr.execute( + "SELECT state FROM queue_job WHERE uuid=%s AND state=%s FOR UPDATE", + (job_uuid, ENQUEUED), + ) + if not env.cr.fetchone(): + _logger.warning( + "was requested to run job %s, but it does not exist, " + "or is not in state %s", + job_uuid, + ENQUEUED, + ) + return "" + + job = Job.load(env, job_uuid) + assert job and job.state == ENQUEUED + + try: + try: + self._try_perform_job(env, job) + except OperationalError as err: + # Automatically retry the typical transaction serialization + # errors + if err.pgcode not in PG_CONCURRENCY_ERRORS_TO_RETRY: + raise + + _logger.debug("%s OperationalError, postponed", job) + raise RetryableJobError(err.pgerror, seconds=PG_RETRY) from err + + except RetryableJobError as err: + # delay the job later, requeue + retry_postpone(job, str(err), seconds=err.seconds) + _logger.debug("%s postponed", job) + # Do not trigger the error up because we don't want an exception + # traceback in the logs we should have the traceback when all + # retries are exhausted + env.cr.rollback() + return "" + + except (FailedJobError, Exception) as orig_exception: + buff = StringIO() + traceback.print_exc(file=buff) + traceback_txt = buff.getvalue() + _logger.error(traceback_txt) + job.env.clear() + with registry(job.env.cr.dbname).cursor() as new_cr: + job.env = job.env(cr=new_cr) + vals = self._get_failure_values(job, traceback_txt, orig_exception) + job.set_failed(**vals) + job.store() + buff.close() + raise + + _logger.debug("%s enqueue depends started", job) + self._enqueue_dependent_jobs(env, job) + _logger.debug("%s enqueue depends done", job) + + return "" + + def _get_failure_values(self, job, traceback_txt, orig_exception): + """Collect relevant data from exception.""" + exception_name = orig_exception.__class__.__name__ + if hasattr(orig_exception, "__module__"): + exception_name = orig_exception.__module__ + "." + exception_name + exc_message = getattr(orig_exception, "name", str(orig_exception)) + return { + "exc_info": traceback_txt, + "exc_name": exception_name, + "exc_message": exc_message, + } + + # flake8: noqa: C901 + @http.route("/queue_job/create_test_job", type="http", auth="user") + def create_test_job( + self, + priority=None, + max_retries=None, + channel=None, + description="Test job", + size=1, + failure_rate=0, + ): + if not http.request.env.user.has_group("base.group_erp_manager"): + raise Forbidden(_("Access Denied")) + + if failure_rate is not None: + try: + failure_rate = float(failure_rate) + except (ValueError, TypeError): + failure_rate = 0 + + if not (0 <= failure_rate <= 1): + raise BadRequest("failure_rate must be between 0 and 1") + + if size is not None: + try: + size = int(size) + except (ValueError, TypeError): + size = 1 + + if priority is not None: + try: + priority = int(priority) + except ValueError: + priority = None + + if max_retries is not None: + try: + max_retries = int(max_retries) + except ValueError: + max_retries = None + + if size == 1: + return self._create_single_test_job( + priority=priority, + max_retries=max_retries, + channel=channel, + description=description, + failure_rate=failure_rate, + ) + + if size > 1: + return self._create_graph_test_jobs( + size, + priority=priority, + max_retries=max_retries, + channel=channel, + description=description, + failure_rate=failure_rate, + ) + return "" + + def _create_single_test_job( + self, + priority=None, + max_retries=None, + channel=None, + description="Test job", + size=1, + failure_rate=0, + ): + delayed = ( + http.request.env["queue.job"] + .with_delay( + priority=priority, + max_retries=max_retries, + channel=channel, + description=description, + ) + ._test_job(failure_rate=failure_rate) + ) + return f"job uuid: {delayed.db_record().uuid}" + + TEST_GRAPH_MAX_PER_GROUP = 5 + + def _create_graph_test_jobs( + self, + size, + priority=None, + max_retries=None, + channel=None, + description="Test job", + failure_rate=0, + ): + model = http.request.env["queue.job"] + current_count = 0 + + possible_grouping_methods = (chain, group) + + tails = [] # we can connect new graph chains/groups to tails + root_delayable = None + while current_count < size: + jobs_count = min( + size - current_count, random.randint(1, self.TEST_GRAPH_MAX_PER_GROUP) + ) + + jobs = [] + for __ in range(jobs_count): + current_count += 1 + jobs.append( + model.delayable( + priority=priority, + max_retries=max_retries, + channel=channel, + description="%s #%d" % (description, current_count), + )._test_job(failure_rate=failure_rate) + ) + + grouping = random.choice(possible_grouping_methods) + delayable = grouping(*jobs) + if not root_delayable: + root_delayable = delayable + else: + tail_delayable = random.choice(tails) + tail_delayable.on_done(delayable) + tails.append(delayable) + + root_delayable.delay() + + return ( + f"graph uuid: {list(root_delayable._head())[0]._generated_job.graph_uuid}" + ) diff --git a/queue_job/data/queue_data.xml b/queue_job/data/queue_data.xml new file mode 100644 index 0000000000..216b6cf016 --- /dev/null +++ b/queue_job/data/queue_data.xml @@ -0,0 +1,34 @@ + + + + + Jobs Garbage Collector + 5 + minutes + + code + model.requeue_stuck_jobs() + + + + Job failed + queue.job + + + + AutoVacuum Job Queue + + + + 1 + days + code + model.autovacuum() + + + + + root + + + diff --git a/queue_job/data/queue_job_function_data.xml b/queue_job/data/queue_job_function_data.xml new file mode 100644 index 0000000000..0105dbc508 --- /dev/null +++ b/queue_job/data/queue_job_function_data.xml @@ -0,0 +1,6 @@ + + + + _test_job + + diff --git a/queue_job/delay.py b/queue_job/delay.py new file mode 100644 index 0000000000..9b596b1665 --- /dev/null +++ b/queue_job/delay.py @@ -0,0 +1,619 @@ +# Copyright 2019 Camptocamp +# Copyright 2019 Guewen Baconnier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +import itertools +import logging +import uuid +from collections import defaultdict, deque + +from .job import Job +from .utils import must_run_without_delay + +_logger = logging.getLogger(__name__) + + +def group(*delayables): + """Return a group of delayable to form a graph + + A group means that jobs can be executed concurrently. + A job or a group of jobs depending on a group can be executed only after + all the jobs of the group are done. + + Shortcut to :class:`~odoo.addons.queue_job.delay.DelayableGroup`. + + Example:: + + g1 = group(delayable1, delayable2) + g2 = group(delayable3, delayable4) + g1.on_done(g2) + g1.delay() + """ + return DelayableGroup(*delayables) + + +def chain(*delayables): + """Return a chain of delayable to form a graph + + A chain means that jobs must be executed sequentially. + A job or a group of jobs depending on a group can be executed only after + the last job of the chain is done. + + Shortcut to :class:`~odoo.addons.queue_job.delay.DelayableChain`. + + Example:: + + chain1 = chain(delayable1, delayable2, delayable3) + chain2 = chain(delayable4, delayable5, delayable6) + chain1.on_done(chain2) + chain1.delay() + """ + return DelayableChain(*delayables) + + +class Graph: + """Acyclic directed graph holding vertices of any hashable type + + This graph is not specifically designed to hold :class:`~Delayable` + instances, although ultimately it is used for this purpose. + """ + + __slots__ = "_graph" + + def __init__(self, graph=None): + if graph: + self._graph = graph + else: + self._graph = {} + + def add_vertex(self, vertex): + """Add a vertex + + Has no effect if called several times with the same vertex + """ + self._graph.setdefault(vertex, set()) + + def add_edge(self, parent, child): + """Add an edge between a parent and a child vertex + + Has no effect if called several times with the same pair of vertices + """ + self.add_vertex(child) + self._graph.setdefault(parent, set()).add(child) + + def vertices(self): + """Return the vertices (nodes) of the graph""" + return set(self._graph) + + def edges(self): + """Return the edges (links) of the graph""" + links = [] + for vertex, neighbours in self._graph.items(): + for neighbour in neighbours: + links.append((vertex, neighbour)) + return links + + # from + # https://codereview.stackexchange.com/questions/55767/finding-all-paths-from-a-given-graph + def paths(self, vertex): + """Generate the maximal cycle-free paths in graph starting at vertex. + + >>> g = {1: [2, 3], 2: [3, 4], 3: [1], 4: []} + >>> sorted(self.paths(1)) + [[1, 2, 3], [1, 2, 4], [1, 3]] + >>> sorted(self.paths(3)) + [[3, 1, 2, 4]] + """ + path = [vertex] # path traversed so far + seen = {vertex} # set of vertices in path + + def search(): + dead_end = True + for neighbour in self._graph[path[-1]]: + if neighbour not in seen: + dead_end = False + seen.add(neighbour) + path.append(neighbour) + yield from search() + path.pop() + seen.remove(neighbour) + if dead_end: + yield list(path) + + yield from search() + + def topological_sort(self): + """Yields a proposed order of nodes to respect dependencies + + The order is not unique, the result may vary, but it is guaranteed + that a node depending on another is not yielded before. + It assumes the graph has no cycle. + """ + depends_per_node = defaultdict(int) + for __, tail in self.edges(): + depends_per_node[tail] += 1 + + # the queue contains only elements for which all dependencies + # are resolved + queue = deque(self.root_vertices()) + while queue: + vertex = queue.popleft() + yield vertex + for node in self._graph[vertex]: + depends_per_node[node] -= 1 + if not depends_per_node[node]: + queue.append(node) + + def root_vertices(self): + """Returns the root vertices + + meaning they do not depend on any other job. + """ + dependency_vertices = set() + for dependencies in self._graph.values(): + dependency_vertices.update(dependencies) + return set(self._graph.keys()) - dependency_vertices + + def __repr__(self): + paths = [path for vertex in self.root_vertices() for path in self.paths(vertex)] + lines = [] + for path in paths: + lines.append(" → ".join(repr(vertex) for vertex in path)) + return "\n".join(lines) + + +class DelayableGraph(Graph): + """Directed Graph for :class:`~Delayable` dependencies + + It connects together the :class:`~Delayable`, :class:`~DelayableGroup` and + :class:`~DelayableChain` graphs, and creates then enqueued the jobs. + """ + + def _merge_graph(self, graph): + """Merge a graph in the current graph + + It takes each vertex, which can be :class:`~Delayable`, + :class:`~DelayableChain` or :class:`~DelayableGroup`, and updates the + current graph with the edges between Delayable objects (connecting + heads and tails of the groups and chains), so that at the end, the + graph contains only Delayable objects and their links. + """ + for vertex, neighbours in graph._graph.items(): + tails = vertex._tail() + for tail in tails: + # connect the tails with the heads of each node + heads = {head for n in neighbours for head in n._head()} + self._graph.setdefault(tail, set()).update(heads) + + def _connect_graphs(self): + """Visit the vertices' graphs and connect them, return the whole graph + + Build a new graph, walk the vertices and their related vertices, merge + their graph in the new one, until we have visited all the vertices + """ + graph = DelayableGraph() + graph._merge_graph(self) + + seen = set() + visit_stack = deque([self]) + while visit_stack: + current = visit_stack.popleft() + if current in seen: + continue + + vertices = current.vertices() + for vertex in vertices: + vertex_graph = vertex._graph + graph._merge_graph(vertex_graph) + visit_stack.append(vertex_graph) + + seen.add(current) + + return graph + + def _has_to_execute_directly(self, vertices): + """Used for tests to run tests directly instead of storing them + + In tests, prefer to use + :func:`odoo.addons.queue_job.tests.common.trap_jobs`. + """ + envs = {vertex.recordset.env for vertex in vertices} + for env in envs: + if must_run_without_delay(env): + return True + return False + + @staticmethod + def _ensure_same_graph_uuid(jobs): + """Set the same graph uuid on all jobs of the same graph""" + jobs_count = len(jobs) + if jobs_count == 0: + raise ValueError("Expecting jobs") + elif jobs_count == 1: + if jobs[0].graph_uuid: + raise ValueError( + f"Job {jobs[0]} is a single job, it should not" " have a graph uuid" + ) + else: + graph_uuids = {job.graph_uuid for job in jobs if job.graph_uuid} + if len(graph_uuids) > 1: + raise ValueError("Jobs cannot have dependencies between several graphs") + elif len(graph_uuids) == 1: + graph_uuid = graph_uuids.pop() + else: + graph_uuid = str(uuid.uuid4()) + for job in jobs: + job.graph_uuid = graph_uuid + + def delay(self): + """Build the whole graph, creates jobs and delay them""" + graph = self._connect_graphs() + + vertices = graph.vertices() + + for vertex in vertices: + vertex._build_job() + + self._ensure_same_graph_uuid([vertex._generated_job for vertex in vertices]) + + if self._has_to_execute_directly(vertices): + self._execute_graph_direct(graph) + return + + for vertex, neighbour in graph.edges(): + neighbour._generated_job.add_depends({vertex._generated_job}) + + # If all the jobs of the graph have another job with the same identity, + # we do not create them. Maybe we should check that the found jobs are + # part of the same graph, but not sure it's really required... + # Also, maybe we want to check only the root jobs. + existing_mapping = {} + for vertex in vertices: + if not vertex.identity_key: + continue + generated_job = vertex._generated_job + existing = generated_job.job_record_with_same_identity_key() + if not existing: + # at least one does not exist yet, we'll delay the whole graph + existing_mapping.clear() + break + existing_mapping[vertex] = existing + + # We'll replace the generated jobs by the existing ones, so callers + # can retrieve the existing job in "_generated_job". + # existing_mapping contains something only if *all* the job with an + # identity have an existing one. + for vertex, existing in existing_mapping.items(): + vertex._generated_job = existing + return + + for vertex in vertices: + vertex._generated_job.store() + + def _execute_graph_direct(self, graph): + for delayable in graph.topological_sort(): + delayable._execute_direct() + + +class DelayableChain: + """Chain of delayables to form a graph + + Delayables can be other :class:`~Delayable`, :class:`~DelayableChain` or + :class:`~DelayableGroup` objects. + + A chain means that jobs must be executed sequentially. + A job or a group of jobs depending on a group can be executed only after + the last job of the chain is done. + + Chains can be connected to other Delayable, DelayableChain or + DelayableGroup objects by using :meth:`~done`. + + A Chain is enqueued by calling :meth:`~delay`, which delays the whole + graph. + Important: :meth:`~delay` must be called on the top-level + delayable/chain/group object of the graph. + """ + + __slots__ = ("_graph", "__head", "__tail") + + def __init__(self, *delayables): + self._graph = DelayableGraph() + iter_delayables = iter(delayables) + head = next(iter_delayables) + self.__head = head + self._graph.add_vertex(head) + for neighbour in iter_delayables: + self._graph.add_edge(head, neighbour) + head = neighbour + self.__tail = head + + def _head(self): + return self.__head._tail() + + def _tail(self): + return self.__tail._head() + + def __repr__(self): + inner_graph = "\n\t".join(repr(self._graph).split("\n")) + return f"DelayableChain(\n\t{inner_graph}\n)" + + def on_done(self, *delayables): + """Connects the current chain to other delayables/chains/groups + + The delayables/chains/groups passed in the parameters will be executed + when the current Chain is done. + """ + for delayable in delayables: + self._graph.add_edge(self.__tail, delayable) + return self + + def delay(self): + """Delay the whole graph""" + self._graph.delay() + + +class DelayableGroup: + """Group of delayables to form a graph + + Delayables can be other :class:`~Delayable`, :class:`~DelayableChain` or + :class:`~DelayableGroup` objects. + + A group means that jobs must be executed sequentially. + A job or a group of jobs depending on a group can be executed only after + the all the jobs of the group are done. + + Groups can be connected to other Delayable, DelayableChain or + DelayableGroup objects by using :meth:`~done`. + + A group is enqueued by calling :meth:`~delay`, which delays the whole + graph. + Important: :meth:`~delay` must be called on the top-level + delayable/chain/group object of the graph. + """ + + __slots__ = ("_graph", "_delayables") + + def __init__(self, *delayables): + self._graph = DelayableGraph() + self._delayables = set(delayables) + for delayable in delayables: + self._graph.add_vertex(delayable) + + def _head(self): + return itertools.chain.from_iterable(node._head() for node in self._delayables) + + def _tail(self): + return itertools.chain.from_iterable(node._tail() for node in self._delayables) + + def __repr__(self): + inner_graph = "\n\t".join(repr(self._graph).split("\n")) + return f"DelayableGroup(\n\t{inner_graph}\n)" + + def on_done(self, *delayables): + """Connects the current group to other delayables/chains/groups + + The delayables/chains/groups passed in the parameters will be executed + when the current Group is done. + """ + for parent in self._delayables: + for child in delayables: + self._graph.add_edge(parent, child) + return self + + def delay(self): + """Delay the whole graph""" + self._graph.delay() + + +class Delayable: + """Unit of a graph, one Delayable will lead to an enqueued job + + Delayables can have dependencies on each others, as well as dependencies on + :class:`~DelayableGroup` or :class:`~DelayableChain` objects. + + This class will generally not be used directly, it is used internally + by :meth:`~odoo.addons.queue_job.models.base.Base.delayable`. Look + in the base model for more details. + + Delayables can be connected to other Delayable, DelayableChain or + DelayableGroup objects by using :meth:`~done`. + + Properties of the future job can be set using the :meth:`~set` method, + which always return ``self``:: + + delayable.set(priority=15).set({"max_retries": 5, "eta": 15}).delay() + + It can be used for example to set properties dynamically. + + A Delayable is enqueued by calling :meth:`delay()`, which delays the whole + graph. + Important: :meth:`delay()` must be called on the top-level + delayable/chain/group object of the graph. + """ + + _properties = ( + "priority", + "eta", + "max_retries", + "description", + "channel", + "identity_key", + ) + __slots__ = _properties + ( + "recordset", + "_graph", + "_job_method", + "_job_args", + "_job_kwargs", + "_generated_job", + ) + + def __init__( + self, + recordset, + priority=None, + eta=None, + max_retries=None, + description=None, + channel=None, + identity_key=None, + ): + self._graph = DelayableGraph() + self._graph.add_vertex(self) + + self.recordset = recordset + + self.priority = priority + self.eta = eta + self.max_retries = max_retries + self.description = description + self.channel = channel + self.identity_key = identity_key + + self._job_method = None + self._job_args = () + self._job_kwargs = {} + + self._generated_job = None + + def _head(self): + return [self] + + def _tail(self): + return [self] + + def __repr__(self): + return "Delayable({}.{}({}, {}))".format( + self.recordset, + self._job_method.__name__ if self._job_method else "", + self._job_args, + self._job_kwargs, + ) + + def __del__(self): + if not self._generated_job: + _logger.warning("Delayable %s was prepared but never delayed", self) + + def _set_from_dict(self, properties): + for key, value in properties.items(): + if key not in self._properties: + raise ValueError(f"No property {key}") + setattr(self, key, value) + + def set(self, *args, **kwargs): + """Set job properties and return self + + The values can be either a dictionary and/or keywork args + """ + if args: + # args must be a dict + self._set_from_dict(*args) + self._set_from_dict(kwargs) + return self + + def on_done(self, *delayables): + """Connects the current Delayable to other delayables/chains/groups + + The delayables/chains/groups passed in the parameters will be executed + when the current Delayable is done. + """ + for child in delayables: + self._graph.add_edge(self, child) + return self + + def delay(self): + """Delay the whole graph""" + self._graph.delay() + + def _build_job(self): + if self._generated_job: + return self._generated_job + self._generated_job = Job( + self._job_method, + args=self._job_args, + kwargs=self._job_kwargs, + priority=self.priority, + max_retries=self.max_retries, + eta=self.eta, + description=self.description, + channel=self.channel, + identity_key=self.identity_key, + ) + return self._generated_job + + def _store_args(self, *args, **kwargs): + self._job_args = args + self._job_kwargs = kwargs + return self + + def __getattr__(self, name): + if name in self.__slots__: + return super().__getattr__(name) + if name in self.recordset: + raise AttributeError( + f"only methods can be delayed ({name} called on {self.recordset})" + ) + recordset_method = getattr(self.recordset, name) + self._job_method = recordset_method + return self._store_args + + def _execute_direct(self): + assert self._generated_job + self._generated_job.perform() + + +class DelayableRecordset: + """Allow to delay a method for a recordset (shortcut way) + + Usage:: + + delayable = DelayableRecordset(recordset, priority=20) + delayable.method(args, kwargs) + + The method call will be processed asynchronously in the job queue, with + the passed arguments. + + This class will generally not be used directly, it is used internally + by :meth:`~odoo.addons.queue_job.models.base.Base.with_delay` + """ + + __slots__ = ("delayable",) + + def __init__( + self, + recordset, + priority=None, + eta=None, + max_retries=None, + description=None, + channel=None, + identity_key=None, + ): + self.delayable = Delayable( + recordset, + priority=priority, + eta=eta, + max_retries=max_retries, + description=description, + channel=channel, + identity_key=identity_key, + ) + + @property + def recordset(self): + return self.delayable.recordset + + def __getattr__(self, name): + def _delay_delayable(*args, **kwargs): + getattr(self.delayable, name)(*args, **kwargs).delay() + return self.delayable._generated_job + + return _delay_delayable + + def __str__(self): + return "DelayableRecordset({}{})".format( + self.delayable.recordset._name, + getattr(self.delayable.recordset, "_ids", ""), + ) + + __repr__ = __str__ diff --git a/queue_job/exception.py b/queue_job/exception.py new file mode 100644 index 0000000000..c04bc8f0cf --- /dev/null +++ b/queue_job/exception.py @@ -0,0 +1,38 @@ +# Copyright 2012-2016 Camptocamp +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + + +class BaseQueueJobError(Exception): + """Base queue job error""" + + +class JobError(BaseQueueJobError): + """A job had an error""" + + +class NoSuchJobError(JobError): + """The job does not exist.""" + + +class FailedJobError(JobError): + """A job had an error having to be resolved.""" + + +class RetryableJobError(JobError): + """A job had an error but can be retried. + + The job will be retried after the given number of seconds. If seconds is + empty, it will be retried according to the ``retry_pattern`` of the job or + by :const:`odoo.addons.queue_job.job.RETRY_INTERVAL` if nothing is defined. + + If ``ignore_retry`` is True, the retry counter will not be increased. + """ + + def __init__(self, msg, seconds=None, ignore_retry=False): + super().__init__(msg) + self.seconds = seconds + self.ignore_retry = ignore_retry + + +class ChannelNotFound(BaseQueueJobError): + """A channel could not be found""" diff --git a/queue_job/fields.py b/queue_job/fields.py new file mode 100644 index 0000000000..8cb45af765 --- /dev/null +++ b/queue_job/fields.py @@ -0,0 +1,133 @@ +# copyright 2016 Camptocamp +# license lgpl-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +import json +from datetime import date, datetime + +import dateutil +import lxml +from psycopg2.extras import Json as PsycopgJson + +from odoo import fields, models +from odoo.tools.func import lazy + + +class JobSerialized(fields.Json): + """Provide the storage for job fields stored as json + + A base_type must be set, it must be dict, list or tuple. + When the field is not set, the json will be the corresponding + json string ("{}" or "[]"). + + Support for some custom types has been added to the json decoder/encoder + (see JobEncoder and JobDecoder). + """ + + type = "job_serialized" + _column_type = ("jsonb", "jsonb") + + _base_type = None + + # these are the default values when we convert an empty value + _default_json_mapping = { + dict: "{}", + list: "[]", + tuple: "[]", + models.BaseModel: lambda env: json.dumps( + {"_type": "odoo_recordset", "model": "base", "ids": [], "uid": env.uid} + ), + } + + def __init__(self, string=fields.SENTINEL, base_type=fields.SENTINEL, **kwargs): + super().__init__(string=string, _base_type=base_type, **kwargs) + + def _setup_attrs(self, model, name): # pylint: disable=missing-return + super()._setup_attrs(model, name) + if self._base_type not in self._default_json_mapping: + raise ValueError(f"{self._base_type} is not a supported base type") + + def _base_type_default_json(self, env): + default_json = self._default_json_mapping.get(self._base_type) + if not isinstance(default_json, str): + default_json = default_json(env) + return default_json + + def convert_to_column(self, value, record, values=None, validate=True): + value = self.convert_to_cache(value, record, validate=validate) + return PsycopgJson(value) + + def convert_to_cache(self, value, record, validate=True): + # cache format: json.dumps(value) or None + if isinstance(value, self._base_type): + return json.dumps(value, cls=JobEncoder) + else: + return value or None + + def convert_to_record(self, value, record): + default = self._base_type_default_json(record.env) + value = value or default + if not isinstance(value, (str | bytes | bytearray)): + value = json.dumps(value, cls=JobEncoder) + return json.loads(value, cls=JobDecoder, env=record.env) + + def convert_to_export(self, value, record): + if not value: + return "" + return json.dumps(value, cls=JobEncoder) + + +class JobEncoder(json.JSONEncoder): + """Encode Odoo recordsets so that we can later recompose them""" + + def _get_record_context(self, obj): + return obj._job_prepare_context_before_enqueue() + + def default(self, obj): + if isinstance(obj, models.BaseModel): + return { + "_type": "odoo_recordset", + "model": obj._name, + "ids": obj.ids, + "uid": obj.env.uid, + "su": obj.env.su, + "context": self._get_record_context(obj), + } + elif isinstance(obj, datetime): + return {"_type": "datetime_isoformat", "value": obj.isoformat()} + elif isinstance(obj, date): + return {"_type": "date_isoformat", "value": obj.isoformat()} + elif isinstance(obj, lxml.etree._Element): + return { + "_type": "etree_element", + "value": lxml.etree.tostring(obj, encoding=str), + } + elif isinstance(obj, lazy): + return obj._value + return json.JSONEncoder.default(self, obj) + + +class JobDecoder(json.JSONDecoder): + """Decode json, recomposing recordsets""" + + def __init__(self, *args, **kwargs): + env = kwargs.pop("env") + super().__init__(*args, object_hook=self.object_hook, **kwargs) + assert env + self.env = env + + def object_hook(self, obj): + if "_type" not in obj: + return obj + type_ = obj["_type"] + if type_ == "odoo_recordset": + model = self.env(user=obj.get("uid"), su=obj.get("su"))[obj["model"]] + if obj.get("context"): + model = model.with_context(**obj.get("context")) + return model.browse(obj["ids"]) + elif type_ == "datetime_isoformat": + return dateutil.parser.parse(obj["value"]) + elif type_ == "date_isoformat": + return dateutil.parser.parse(obj["value"]).date() + elif type_ == "etree_element": + return lxml.etree.fromstring(obj["value"]) + return obj diff --git a/queue_job/i18n/de.po b/queue_job/i18n/de.po new file mode 100644 index 0000000000..209e88f2ee --- /dev/null +++ b/queue_job/i18n/de.po @@ -0,0 +1,955 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * queue_job +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2022-11-04 14:44+0000\n" +"Last-Translator: Maria Sparenberg \n" +"Language-Team: none\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.14.1\n" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "" +"
\n" +" If the max. " +"retries is 0, the number of retries is infinite." +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/controllers/main.py:0 +#, python-format +msgid "Access Denied" +msgstr "Zugriff verweigert" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction +msgid "Action Needed" +msgstr "Aktion notwendig" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_ids +msgid "Activities" +msgstr "Aktivitäten" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "Aussehen von Aktivitätsfehlern" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_state +msgid "Activity State" +msgstr "Aktivitätsstatus" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_icon +msgid "Activity Type Icon" +msgstr "Icon für Aktivitätstyp" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__args +msgid "Args" +msgstr "Argumente" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_attachment_count +msgid "Attachment Count" +msgstr "Anzahl der Anhänge" + +#. module: queue_job +#: model:ir.actions.server,name:queue_job.ir_cron_autovacuum_queue_jobs_ir_actions_server +msgid "AutoVacuum Job Queue" +msgstr "AutoVacuum für Job-Warteschlange" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_base +msgid "Base" +msgstr "Basis" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done +msgid "Cancel" +msgstr "Abbrechen" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_jobs_to_cancelled +msgid "Cancel all selected jobs" +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Cancel job" +msgstr "" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_set_jobs_cancelled +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled +msgid "Cancel jobs" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__cancelled +msgid "Cancelled" +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Cancelled by %s" +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_channel.py:0 +#, python-format +msgid "Cannot change the root channel" +msgstr "Der Root-Kanal kann nicht geändert werden" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_channel.py:0 +#, python-format +msgid "Cannot remove the root channel" +msgstr "Der Root-Kanal kann nicht entfernt werden" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel_id +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Channel" +msgstr "Kanal" + +#. module: queue_job +#: model:ir.model.constraint,message:queue_job.constraint_queue_job_channel_name_uniq +msgid "Channel complete name must be unique" +msgstr "Der vollständige Name des Kanals muss eindeutig sein" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_queue_job_channel +#: model:ir.ui.menu,name:queue_job.menu_queue_job_channel +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_form +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_search +msgid "Channels" +msgstr "Kanäle" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__company_id +msgid "Company" +msgstr "Unternehmen" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel_method_name +msgid "Complete Method Name" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__complete_name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel +msgid "Complete Name" +msgstr "Vollständiger Name" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_created +msgid "Created Date" +msgstr "Erstellt am" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_uid +msgid "Created by" +msgstr "Erstellt von" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_date +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_date +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_date +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_date +msgid "Created on" +msgstr "Erstellt am" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__retry +msgid "Current try" +msgstr "Aktueller Versuch" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Current try / max. retries" +msgstr "Aktueller Versuch / max. Anzahl der Wiederholung" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_cancelled +msgid "Date Cancelled" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_done +msgid "Date Done" +msgstr "Erledigt am" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependencies +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Dependencies" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependency_graph +msgid "Dependency Graph" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__name +msgid "Description" +msgstr "Beschreibung" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__display_name +msgid "Display Name" +msgstr "Anzeigename" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__done +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Done" +msgstr "Erledigt" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_enqueued +msgid "Enqueue Time" +msgstr "Zeit der Einreihung" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__enqueued +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Enqueued" +msgstr "Eingereiht" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_name +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Exception" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_info +msgid "Exception Info" +msgstr "Exception-Info" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Exception Information" +msgstr "Exception-Information" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_message +msgid "Exception Message" +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Exception message" +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Exception:" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__eta +msgid "Execute only after" +msgstr "Erst ausführen nach" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__exec_time +msgid "Execution Time (avg)" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__failed +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Failed" +msgstr "Fehlgeschlagen" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_ir_model_fields__ttype +msgid "Field Type" +msgstr "" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_ir_model_fields +msgid "Fields" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_follower_ids +msgid "Followers" +msgstr "Abonnenten" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_partner_ids +msgid "Followers (Partners)" +msgstr "Abonnenten (Partner)" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "Font Awesome Icon z.B. fa-tasks" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Graph" +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Graph Jobs" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_jobs_count +msgid "Graph Jobs Count" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_uuid +msgid "Graph UUID" +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Group By" +msgstr "Gruppieren nach" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__has_message +msgid "Has Message" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__id +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__id +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__id +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__id +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__id +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__id +msgid "ID" +msgstr "ID" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_icon +msgid "Icon" +msgstr "Icon" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "Dies ist das Icon zur Kennzeichnung eines Aktivitätsfehlers." + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__identity_key +msgid "Identity Key" +msgstr "Identitätsschlüssel" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "If both parameters are 0, ALL jobs will be requeued!" +msgstr "Wenn beide Parameter 0 sind, werden ALLE Jobs neu eingereiht!" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction +msgid "If checked, new messages require your attention." +msgstr "" +"Wenn das Häkchen gesetzt ist, erfordern neue Nachrichten Ihre Aufmerksamkeit." + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error +#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_sms_error +msgid "If checked, some messages have a delivery error." +msgstr "" +"Wenn das Häkchen gesetzt ist, gibt es einige Nachrichten mit einem " +"Übertragungsfehler." + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_function.py:0 +#, python-format +msgid "Invalid job function: {}" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_is_follower +msgid "Is Follower" +msgstr "Ist Abonnent" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_job_channel +msgid "Job Channels" +msgstr "Job-Kanäle" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__job_function_id +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Job Function" +msgstr "Job-Funktion" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_queue_job_function +#: model:ir.model,name:queue_job.model_queue_job_function +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__job_function_ids +#: model:ir.ui.menu,name:queue_job.menu_queue_job_function +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_form +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search +msgid "Job Functions" +msgstr "Job-Funktionen" + +#. module: queue_job +#: model:ir.module.category,name:queue_job.module_category_queue_job +#: model:ir.ui.menu,name:queue_job.menu_queue_job_root +msgid "Job Queue" +msgstr "Job-Warteschlange" + +#. module: queue_job +#: model:res.groups,name:queue_job.group_queue_job_manager +msgid "Job Queue Manager" +msgstr "Job-Warteschlangenverwalter" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__ir_model_fields__ttype__job_serialized +msgid "Job Serialized" +msgstr "Job angeordnet" + +#. module: queue_job +#: model:mail.message.subtype,name:queue_job.mt_job_failed +msgid "Job failed" +msgstr "Job ist fehlgeschlagen" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/controllers/main.py:0 +#, python-format +msgid "Job interrupted and set to Done: nothing to do." +msgstr "Job unterbrochen und als Erledigt markiert: Es ist nicht zu tun." + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__job_ids +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__job_ids +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__job_ids +#: model:ir.ui.menu,name:queue_job.menu_queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_graph +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_pivot +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Jobs" +msgstr "Jobs" + +#. module: queue_job +#: model:ir.actions.server,name:queue_job.ir_cron_queue_job_garbage_collector_ir_actions_server +msgid "Jobs Garbage Collector" +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Jobs for graph %s" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__kwargs +msgid "Kwargs" +msgstr "Kwargs" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_uid +msgid "Last Updated by" +msgstr "Zuletzt aktualisiert von" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_date +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_date +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_date +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_date +msgid "Last Updated on" +msgstr "Zuletzt aktualisiert am" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Manually set to done by %s" +msgstr "Manuell als erledigt markiert von: %s" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__max_retries +msgid "Max. retries" +msgstr "max. Anzahl von Wiederholungen" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error +msgid "Message Delivery error" +msgstr "Nachrichtenübertragungsfehler" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_ids +msgid "Messages" +msgstr "Nachrichten" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__method +msgid "Method" +msgstr "Methode" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__method_name +msgid "Method Name" +msgstr "Methodenname" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__model_name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__model_id +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Model" +msgstr "Modell" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_function.py:0 +#, python-format +msgid "Model {} not found" +msgstr "Modell {} nicht gefunden" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__name +msgid "Name" +msgstr "Bezeichnung" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "Fälligkeit der nächsten Aktivität" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_summary +msgid "Next Activity Summary" +msgstr "Zusammenfassung der nächsten Aktivität" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_id +msgid "Next Activity Type" +msgstr "Typ der nächsten Aktivität" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "No action available for this job" +msgstr "Für diesen Job ist keine Aktion verfügbar" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Not allowed to change field(s): {}" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction_counter +msgid "Number of Actions" +msgstr "Anzahl der Aktionen" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error_counter +msgid "Number of errors" +msgstr "Anzahl der Fehler" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "Das ist die Anzahl von Nachrichten mit Übermittlungsfehler" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__parent_id +msgid "Parent Channel" +msgstr "Übergeordneter Kanal" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_channel.py:0 +#, python-format +msgid "Parent channel required." +msgstr "Es ist ein übergeordneter Kanal notwendig." + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_retry_pattern +msgid "" +"Pattern expressing from the count of retries on retryable errors, the number " +"of of seconds to postpone the next execution. Setting the number of seconds " +"to a 2-element tuple or list will randomize the retry interval between the 2 " +"values.\n" +"Example: {1: 10, 5: 20, 10: 30, 15: 300}.\n" +"Example: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n" +"See the module description for details." +msgstr "" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__pending +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Pending" +msgstr "Ausstehend" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__priority +msgid "Priority" +msgstr "Priorität" + +#. module: queue_job +#: model:ir.ui.menu,name:queue_job.menu_queue +msgid "Queue" +msgstr "Warteschlange" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_job +msgid "Queue Job" +msgstr "Job einreihen" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Queue jobs must be created by calling 'with_delay()'." +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__record_ids +msgid "Record" +msgstr "Datensatz" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__records +#, fuzzy +msgid "Record(s)" +msgstr "Datensatz" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Related" +msgstr "Zugehörige Aktion anzeigen" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_related_action +#, fuzzy +msgid "Related Action" +msgstr "Zugehöriger Datensatz" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__related_action +msgid "Related Action (serialized)" +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Related Record" +msgstr "Zugehöriger Datensatz" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Related Records" +msgstr "Zugehörige Datensätze" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_tree +msgid "Remaining days to execute" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__removal_interval +msgid "Removal Interval" +msgstr "Entfernungsintervall" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job +msgid "Requeue" +msgstr "Erneut einreihen" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Requeue Job" +msgstr "Job erneut einreihen" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_requeue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job +msgid "Requeue Jobs" +msgstr "Jobs erneut einreihen" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_user_id +msgid "Responsible User" +msgstr "Verantwortlicher Benutzer" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__result +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Result" +msgstr "Ergebnis" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Results" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_retry_pattern +msgid "Retry Pattern" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__retry_pattern +msgid "Retry Pattern (serialized)" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_sms_error +msgid "SMS Delivery error" +msgstr "Fehler bei der SMS Nachrichtenübermittlung" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_jobs_to_done +msgid "Set all selected jobs to done" +msgstr "Alle ausgewählten Jobs als Erledigt markieren" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done +msgid "Set jobs done" +msgstr "Jobs als Erledigt markieren" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_set_jobs_done +msgid "Set jobs to done" +msgstr "Jobs als Erledigt markieren" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Set to 'Done'" +msgstr "Als Erledigt markieren" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done +msgid "Set to done" +msgstr "Als Erledigt markieren" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__graph_uuid +msgid "Single shared identifier of a Graph. Empty for a single job." +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "" +"Something bad happened during the execution of the job. More details in the " +"'Exception Information' section." +msgstr "" +"Bei der Ausführung des Jobs ist etwas Ungewöhnliches passiert. Beachten Sie " +"die Details im Abschnitt \"Exception-Information\"." + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_started +msgid "Start Date" +msgstr "Gestartet am" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__started +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Started" +msgstr "Gestartet" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__state +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "State" +msgstr "Status" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" +"Der Status hängt von den Aktivitäten ab.\n" +"Überfällig: Das Fälligkeitsdatum der Aktivität ist überschritten.\n" +"Heute: Die Aktivität findet heute statt.\n" +"Geplant: Die Aktivitäten findet in der Zukunft statt." + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__func_string +msgid "Task" +msgstr "Aufgabe" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_related_action +msgid "" +"The action when the button *Related Action* is used on a job. The default " +"action is to open the view of the record related to the job. Configured as a " +"dictionary with optional keys: enable, func_name, kwargs.\n" +"See the module description for details." +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__max_retries +msgid "" +"The job will fail if the number of tries reach the max. retries.\n" +"Retries are infinite when empty." +msgstr "" +"Der Job wird fehlschlagen, wenn die Anzahl der Versuche gleich der maximalen " +"Anzahl der Wiederholungen ist.\n" +"Wenn Letzteres nicht gesetzt ist, werden unendlich viele Versuche " +"unternommen." + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled +msgid "The selected jobs will be cancelled." +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job +msgid "The selected jobs will be requeued." +msgstr "Die ausgewählten Jobs werden erneut eingereiht." + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done +msgid "The selected jobs will be set to done." +msgstr "Die ausgewählten Jobs werden als Erledigt markiert." + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Time (s)" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__exec_time +msgid "Time required to execute this job in seconds. Average when grouped." +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "Typ der Ausnahmeaktivität im Datensatz." + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__uuid +msgid "UUID" +msgstr "UUID" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_function.py:0 +#, python-format +msgid "" +"Unexpected format of Related Action for {}.\n" +"Example of valid format:\n" +"{{\"enable\": True, \"func_name\": \"related_action_foo\", \"kwargs" +"\" {{\"limit\": 10}}}}" +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_function.py:0 +#, python-format +msgid "" +"Unexpected format of Retry Pattern for {}.\n" +"Example of valid format:\n" +"{{1: 300, 5: 600, 10: 1200, 15: 3000}}" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__user_id +msgid "User ID" +msgstr "Benutzer" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__wait_dependencies +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Wait Dependencies" +msgstr "" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_requeue_job +msgid "Wizard to requeue a selection of jobs" +msgstr "Assistent zur erneuten Einreihung einer Job-Auswahl" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__worker_pid +msgid "Worker Pid" +msgstr "" + +#~ msgid "Last Modified on" +#~ msgstr "Zuletzt geändert am" + +#~ msgid "Main Attachment" +#~ msgstr "Haupt-Anhang" + +#~ msgid "Number of messages which requires an action" +#~ msgstr "Das ist die Anzahl von Nachrichten, die eine Aktion benötigen" + +#~ msgid "" +#~ " If the max. retries is 0, the number " +#~ "of retries is infinite." +#~ msgstr "" +#~ "Wenn die maximale Anzahl der " +#~ "Wiederholung auf 0 gesetzt ist, wird dies als unendlich interpretiert." + +#~ msgid "Override Channel" +#~ msgstr "Kanal überschreiben" + +#~ msgid "Number of unread messages" +#~ msgstr "Das ist die Anzahl von ungelesenen Nachrichten" diff --git a/queue_job/i18n/es.po b/queue_job/i18n/es.po new file mode 100644 index 0000000000..ab30fa0f24 --- /dev/null +++ b/queue_job/i18n/es.po @@ -0,0 +1,975 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * queue_job +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-09-20 21:07+0000\n" +"Last-Translator: Ivorra78 \n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "" +"
\n" +" If the max. " +"retries is 0, the number of retries is infinite." +msgstr "" +"
\n" +" Si el máx. " +"reintentos es 0, el número de reintentos es infinito." + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/controllers/main.py:0 +#, python-format +msgid "Access Denied" +msgstr "Acceso denegado" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction +msgid "Action Needed" +msgstr "Acción requerida" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_ids +msgid "Activities" +msgstr "Actividades" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "Decoración de Actividad de Excepción" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_state +msgid "Activity State" +msgstr "Estado de la actividad" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_icon +msgid "Activity Type Icon" +msgstr "Icono de tipo de actividad" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__args +msgid "Args" +msgstr "Argumentos" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_attachment_count +msgid "Attachment Count" +msgstr "Nº de archivos adjuntos" + +#. module: queue_job +#: model:ir.actions.server,name:queue_job.ir_cron_autovacuum_queue_jobs_ir_actions_server +msgid "AutoVacuum Job Queue" +msgstr "Vaciado automático de la cola de trabajos" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_base +msgid "Base" +msgstr "Base" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done +msgid "Cancel" +msgstr "Cancelar" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_jobs_to_cancelled +msgid "Cancel all selected jobs" +msgstr "Cancelar todos los trabajos seleccionados" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Cancel job" +msgstr "Cancelar trabajo" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_set_jobs_cancelled +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled +msgid "Cancel jobs" +msgstr "Cancelar trabajos" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__cancelled +msgid "Cancelled" +msgstr "Cancelada" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Cancelled by %s" +msgstr "Cancelado por %s" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_channel.py:0 +#, python-format +msgid "Cannot change the root channel" +msgstr "No se puede cambiar el canal raíz" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_channel.py:0 +#, python-format +msgid "Cannot remove the root channel" +msgstr "No se puede eliminar el canal raíz" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel_id +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Channel" +msgstr "Canal" + +#. module: queue_job +#: model:ir.model.constraint,message:queue_job.constraint_queue_job_channel_name_uniq +msgid "Channel complete name must be unique" +msgstr "El nombre completo del canal debe ser único" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_queue_job_channel +#: model:ir.ui.menu,name:queue_job.menu_queue_job_channel +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_form +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_search +msgid "Channels" +msgstr "Canales" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__company_id +msgid "Company" +msgstr "Empresa" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel_method_name +msgid "Complete Method Name" +msgstr "Nombre completo del método" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__complete_name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel +msgid "Complete Name" +msgstr "Nombre completo" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_created +msgid "Created Date" +msgstr "Fecha de creación" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_date +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_date +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_date +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__retry +msgid "Current try" +msgstr "Intento actual" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Current try / max. retries" +msgstr "Intento actual / reintentos máx" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_cancelled +msgid "Date Cancelled" +msgstr "Fecha de cancelación" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_done +msgid "Date Done" +msgstr "Fecha de realización" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependencies +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Dependencies" +msgstr "Dependencias" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependency_graph +msgid "Dependency Graph" +msgstr "Gráfico de dependencias" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__name +msgid "Description" +msgstr "Descripción" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__display_name +msgid "Display Name" +msgstr "Nombre mostrado" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__done +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Done" +msgstr "Hecho" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_enqueued +msgid "Enqueue Time" +msgstr "Hora en que se puso en cola" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__enqueued +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Enqueued" +msgstr "En la cola" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_name +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Exception" +msgstr "Excepción" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_info +msgid "Exception Info" +msgstr "Información de la excepción" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Exception Information" +msgstr "Información de la excepción" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_message +msgid "Exception Message" +msgstr "Mensaje de la excepción" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Exception message" +msgstr "Mensaje de la excepción" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Exception:" +msgstr "Excepción:" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__eta +msgid "Execute only after" +msgstr "Ejecutar solo después de" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__exec_time +msgid "Execution Time (avg)" +msgstr "Duración media de ejecución" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__failed +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Failed" +msgstr "Fallido" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_ir_model_fields__ttype +msgid "Field Type" +msgstr "Tipo de campo" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_ir_model_fields +msgid "Fields" +msgstr "Campos" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_follower_ids +msgid "Followers" +msgstr "Seguidores" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_partner_ids +msgid "Followers (Partners)" +msgstr "Seguidores (Socios)" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "Icono de Font Awesome ej. fa-tasks" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Graph" +msgstr "Gráfico" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Graph Jobs" +msgstr "Gráfico de trabajos" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_jobs_count +msgid "Graph Jobs Count" +msgstr "Nº de gráfico de trabajos" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_uuid +msgid "Graph UUID" +msgstr "UUID del Gráfico" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Group By" +msgstr "Agrupar por" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__has_message +msgid "Has Message" +msgstr "Tiene un mensaje" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__id +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__id +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__id +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__id +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__id +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__id +msgid "ID" +msgstr "ID" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_icon +msgid "Icon" +msgstr "Icono" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "Icono para indicar una actividad de excepción." + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__identity_key +msgid "Identity Key" +msgstr "Clave identificadora" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "If both parameters are 0, ALL jobs will be requeued!" +msgstr "" +"Si ambos parámetros son 0, ¡TODOS los trabajos se volverán a poner en la " +"cola!" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction +msgid "If checked, new messages require your attention." +msgstr "" +"Si se encuentra seleccionado, hay nuevos mensajes que requieren tu atención." + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error +#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_sms_error +msgid "If checked, some messages have a delivery error." +msgstr "Si se encuentra seleccionado, algunos mensajes tienen error de envío." + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_function.py:0 +#, python-format +msgid "Invalid job function: {}" +msgstr "Función del trabajo no válida: {}" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_is_follower +msgid "Is Follower" +msgstr "Es un seguidor" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_job_channel +msgid "Job Channels" +msgstr "Canales de trabajos" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__job_function_id +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Job Function" +msgstr "Función del trabajo" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_queue_job_function +#: model:ir.model,name:queue_job.model_queue_job_function +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__job_function_ids +#: model:ir.ui.menu,name:queue_job.menu_queue_job_function +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_form +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search +msgid "Job Functions" +msgstr "Funciones de los trabajos" + +#. module: queue_job +#: model:ir.module.category,name:queue_job.module_category_queue_job +#: model:ir.ui.menu,name:queue_job.menu_queue_job_root +msgid "Job Queue" +msgstr "Cola de trabajos" + +#. module: queue_job +#: model:res.groups,name:queue_job.group_queue_job_manager +msgid "Job Queue Manager" +msgstr "Gestor de la cola de trabajos" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__ir_model_fields__ttype__job_serialized +msgid "Job Serialized" +msgstr "Trabajo en serie" + +#. module: queue_job +#: model:mail.message.subtype,name:queue_job.mt_job_failed +msgid "Job failed" +msgstr "Trabajo fallido" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/controllers/main.py:0 +#, python-format +msgid "Job interrupted and set to Done: nothing to do." +msgstr "Trabajo interrumpido y marcado como hecho: nada que hacer." + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__job_ids +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__job_ids +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__job_ids +#: model:ir.ui.menu,name:queue_job.menu_queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_graph +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_pivot +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Jobs" +msgstr "Trabajos" + +#. module: queue_job +#: model:ir.actions.server,name:queue_job.ir_cron_queue_job_garbage_collector_ir_actions_server +msgid "Jobs Garbage Collector" +msgstr "Recolector de basura de trabajos" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Jobs for graph %s" +msgstr "Trabajos para gráfico %s" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__kwargs +msgid "Kwargs" +msgstr "Kwargs" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_uid +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_date +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_date +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_date +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_date +msgid "Last Updated on" +msgstr "Última actualización el" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Manually set to done by %s" +msgstr "Marcado como hecho a mano por %s" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__max_retries +msgid "Max. retries" +msgstr "Reintentos máx" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error +msgid "Message Delivery error" +msgstr "Error de Envío de Mensaje" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_ids +msgid "Messages" +msgstr "Mensajes" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__method +msgid "Method" +msgstr "Método" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__method_name +msgid "Method Name" +msgstr "Nombre del método" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__model_name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__model_id +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Model" +msgstr "Modelo" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_function.py:0 +#, python-format +msgid "Model {} not found" +msgstr "No se ha encontrado el modelo {}" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "Mi fecha límite de actividad" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__name +msgid "Name" +msgstr "Nombre" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "Fecha límite de siguiente actividad" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_summary +msgid "Next Activity Summary" +msgstr "Resumen de la siguiente actividad" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_id +msgid "Next Activity Type" +msgstr "Siguiente tipo de actividad" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "No action available for this job" +msgstr "No hay ninguna acción disponible para este trabajo" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Not allowed to change field(s): {}" +msgstr "No se permite cambiar los campos: {}" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction_counter +msgid "Number of Actions" +msgstr "Número de acciones" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error_counter +msgid "Number of errors" +msgstr "Numero de errores" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "Número de mensajes que requieren una acción" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "Número de mensajes con error de envío" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__parent_id +msgid "Parent Channel" +msgstr "Canal padre" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_channel.py:0 +#, python-format +msgid "Parent channel required." +msgstr "Se requiere un canal padre." + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_retry_pattern +msgid "" +"Pattern expressing from the count of retries on retryable errors, the number " +"of of seconds to postpone the next execution. Setting the number of seconds " +"to a 2-element tuple or list will randomize the retry interval between the 2 " +"values.\n" +"Example: {1: 10, 5: 20, 10: 30, 15: 300}.\n" +"Example: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n" +"See the module description for details." +msgstr "" +"Patrón que expresa cuántos segundos se pospondrá la próxima ejecución, " +"basado en el número de reintentos de los errores reintentables. Si se usa " +"una tupla o lista de 2 elementos para expresar el número de segundos, se " +"escogerá un número aleatorio entre ambos valores.\n" +"Ejemplo: {1: 10, 5: 20, 10: 30, 15: 300}.\n" +"Ejemplo: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n" +"Vea la descripción del módulo para más detalles." + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__pending +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Pending" +msgstr "Pendiente" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__priority +msgid "Priority" +msgstr "Prioridad" + +#. module: queue_job +#: model:ir.ui.menu,name:queue_job.menu_queue +msgid "Queue" +msgstr "Cola" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_job +msgid "Queue Job" +msgstr "Cola de trabajos" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Queue jobs must be created by calling 'with_delay()'." +msgstr "Los trabajos en cola deben crearse llamando a 'with_delay()'." + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__record_ids +msgid "Record" +msgstr "Registro" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__records +msgid "Record(s)" +msgstr "Registro(s)" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Related" +msgstr "Relacionado" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_related_action +msgid "Related Action" +msgstr "Acción relacionada" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__related_action +msgid "Related Action (serialized)" +msgstr "Acción relacionada (en serie)" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Related Record" +msgstr "Registro relacionado" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Related Records" +msgstr "Registros relacionados" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_tree +msgid "Remaining days to execute" +msgstr "Días restantes para ejecutar" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__removal_interval +msgid "Removal Interval" +msgstr "Intervalo de eliminación" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job +msgid "Requeue" +msgstr "Volver a poner en la cola" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Requeue Job" +msgstr "Volver a poner el trabajo en la cola" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_requeue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job +msgid "Requeue Jobs" +msgstr "Volver a poner los trabajos en la cola" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_user_id +msgid "Responsible User" +msgstr "Usuario responsable" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__result +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Result" +msgstr "Resultado" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Results" +msgstr "Resultados" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_retry_pattern +msgid "Retry Pattern" +msgstr "Patrón de reintentos" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__retry_pattern +msgid "Retry Pattern (serialized)" +msgstr "Patrón de reintentos (en serie)" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_sms_error +msgid "SMS Delivery error" +msgstr "Error de entrega del SMS" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_jobs_to_done +msgid "Set all selected jobs to done" +msgstr "Marcar como hechos todos los trabajos seleccionados" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done +msgid "Set jobs done" +msgstr "Marcar trabajos como hechos" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_set_jobs_done +msgid "Set jobs to done" +msgstr "Marcar trabajos como hechos" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Set to 'Done'" +msgstr "Marcar como 'Hecho'" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done +msgid "Set to done" +msgstr "Marcar como hecho" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__graph_uuid +msgid "Single shared identifier of a Graph. Empty for a single job." +msgstr "" +"Identificador único compartido de un gráfico. Vacío para un solo trabajo." + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "" +"Something bad happened during the execution of the job. More details in the " +"'Exception Information' section." +msgstr "" +"Algo malo pasó durante la ejecución del trabajo. Más detalles en la sección " +"'Información de la excepción'." + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_started +msgid "Start Date" +msgstr "Fecha de inicio" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__started +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Started" +msgstr "Iniciado" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__state +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "State" +msgstr "Estado" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" +"Estado basado en actividades\n" +"Vencida: la fecha tope ya ha pasado\n" +"Hoy: La fecha tope es hoy\n" +"Planificada: futuras actividades." + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__func_string +msgid "Task" +msgstr "Tarea" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_related_action +msgid "" +"The action when the button *Related Action* is used on a job. The default " +"action is to open the view of the record related to the job. Configured as a " +"dictionary with optional keys: enable, func_name, kwargs.\n" +"See the module description for details." +msgstr "" +"Acción cuando se usa el botón *Acción relacionada* en un trabajo. La acción " +"por defecto es abrir la vista del registro relacionado con el trabajo. Se " +"configura como un diccionario con estas claves opcionales: enable, " +"func_name, kwargs.\n" +"Vea la descripción del módulo para más detalles." + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__max_retries +msgid "" +"The job will fail if the number of tries reach the max. retries.\n" +"Retries are infinite when empty." +msgstr "" +"El trabajo fallará si alcanza el máx. de re intentos.\n" +"Los reintentos son infinitos si se deja vacío." + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled +msgid "The selected jobs will be cancelled." +msgstr "Los trabajos seleccionados serán cancelados." + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job +msgid "The selected jobs will be requeued." +msgstr "Los trabajos seleccionados volverán a ponerse en la cola." + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done +msgid "The selected jobs will be set to done." +msgstr "Los trabajos seleccionados se marcarán como hechos." + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Time (s)" +msgstr "Tiempo (s)" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__exec_time +msgid "Time required to execute this job in seconds. Average when grouped." +msgstr "" +"Tiempo requerido para ejecutar este trabajo en segundos. Promedio cuando se " +"agrupa." + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "Tipo de actividad de excepción registrada." + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__uuid +msgid "UUID" +msgstr "UUID" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_function.py:0 +#, python-format +msgid "" +"Unexpected format of Related Action for {}.\n" +"Example of valid format:\n" +"{{\"enable\": True, \"func_name\": \"related_action_foo\", \"kwargs" +"\" {{\"limit\": 10}}}}" +msgstr "" +"Formato inesperado en la acción relacionada con {}.\n" +"Ejemplo de un formato válido:\n" +"{{\"enable\": True, \"func_name\": \"related_action_foo\", \"kwargs" +"\" {{\"limit\": 10}}}}" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_function.py:0 +#, python-format +msgid "" +"Unexpected format of Retry Pattern for {}.\n" +"Example of valid format:\n" +"{{1: 300, 5: 600, 10: 1200, 15: 3000}}" +msgstr "" +"Formato inesperado en el patrón de reintentos de {}.\n" +"Ejemplo de un formato válido:\n" +"{{1: 300, 5: 600, 10: 1200, 15: 3000}}" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__user_id +msgid "User ID" +msgstr "ID de usuario" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__wait_dependencies +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Wait Dependencies" +msgstr "Esperando dependencias" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_requeue_job +msgid "Wizard to requeue a selection of jobs" +msgstr "Asistente para volver a poner en cola una selección de trabajos" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__worker_pid +msgid "Worker Pid" +msgstr "Pid del trabajador" + +#~ msgid "Last Modified on" +#~ msgstr "Última modificación el" + +#~ msgid "Main Attachment" +#~ msgstr "Adjuntos principales" + +#~ msgid "Number of messages which requires an action" +#~ msgstr "Número de mensajes que requieren una acción" + +#~ msgid "" +#~ " If the max. retries is 0, the number " +#~ "of retries is infinite." +#~ msgstr "" +#~ " Si los reintentos máximos son 0, el " +#~ "número de reintentos es infinito." + +#~ msgid "Override Channel" +#~ msgstr "Sobreescribir canal" + +#~ msgid "Number of unread messages" +#~ msgstr "Número de mensajes no leidos" diff --git a/queue_job/i18n/it.po b/queue_job/i18n/it.po new file mode 100644 index 0000000000..255d6ec93d --- /dev/null +++ b/queue_job/i18n/it.po @@ -0,0 +1,941 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * queue_job +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-02-19 14:37+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "" +"
\n" +" If the max. retries is 0, the number of retries is infinite." +msgstr "" +"
\n" +" Se il massimo " +"di tentativi è 0, il numero di tentativi è infinito." + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/controllers/main.py:0 +#, python-format +msgid "Access Denied" +msgstr "Accesso negato" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction +msgid "Action Needed" +msgstr "Azione richiesta" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_ids +msgid "Activities" +msgstr "Attività" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "Decorazione eccezione attività" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_state +msgid "Activity State" +msgstr "Stato attività" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_icon +msgid "Activity Type Icon" +msgstr "Icona tipo attività" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__args +msgid "Args" +msgstr "Argomenti" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_attachment_count +msgid "Attachment Count" +msgstr "Conteggio allegati" + +#. module: queue_job +#: model:ir.actions.server,name:queue_job.ir_cron_autovacuum_queue_jobs_ir_actions_server +msgid "AutoVacuum Job Queue" +msgstr "Auto pulizia coda lavoro" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_base +msgid "Base" +msgstr "Base" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done +msgid "Cancel" +msgstr "Annulla" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_jobs_to_cancelled +msgid "Cancel all selected jobs" +msgstr "Annulla tutti i lavori selezionati" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Cancel job" +msgstr "Annulla lavoro" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_set_jobs_cancelled +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled +msgid "Cancel jobs" +msgstr "Annulla lavori" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__cancelled +msgid "Cancelled" +msgstr "Annullata" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Cancelled by %s" +msgstr "Annullata da %s" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_channel.py:0 +#, python-format +msgid "Cannot change the root channel" +msgstr "Non si può cambiare il canale radice" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_channel.py:0 +#, python-format +msgid "Cannot remove the root channel" +msgstr "Non si può rimuovere il canale radice" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel_id +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Channel" +msgstr "Canale" + +#. module: queue_job +#: model:ir.model.constraint,message:queue_job.constraint_queue_job_channel_name_uniq +msgid "Channel complete name must be unique" +msgstr "Il nome completo del canale deve essere univoco" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_queue_job_channel +#: model:ir.ui.menu,name:queue_job.menu_queue_job_channel +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_form +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_search +msgid "Channels" +msgstr "Canali" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__company_id +msgid "Company" +msgstr "Azienda" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel_method_name +msgid "Complete Method Name" +msgstr "Nome completo metodo" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__complete_name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel +msgid "Complete Name" +msgstr "Nome completo" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_created +msgid "Created Date" +msgstr "Data creazione" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_date +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_date +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_date +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__retry +msgid "Current try" +msgstr "Tentativo attuale" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Current try / max. retries" +msgstr "Tentativo attuale / massimo tentativi" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_cancelled +msgid "Date Cancelled" +msgstr "Data annullamento" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_done +msgid "Date Done" +msgstr "Data completamento" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependencies +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Dependencies" +msgstr "Dipendenze" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependency_graph +msgid "Dependency Graph" +msgstr "Grafico dipendenza" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__name +msgid "Description" +msgstr "Descrizione" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__done +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Done" +msgstr "Completata" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_enqueued +msgid "Enqueue Time" +msgstr "Ora accodamento" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__enqueued +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Enqueued" +msgstr "In coda" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_name +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Exception" +msgstr "Eccezione" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_info +msgid "Exception Info" +msgstr "Informazioni eccezione" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Exception Information" +msgstr "Informazioni eccezione" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_message +msgid "Exception Message" +msgstr "Messaggio eccezione" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Exception message" +msgstr "Messaggio eccezione" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Exception:" +msgstr "Eccezione:" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__eta +msgid "Execute only after" +msgstr "Eseguire solo dopo" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__exec_time +msgid "Execution Time (avg)" +msgstr "Tempo esecuzione (medio)" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__failed +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Failed" +msgstr "Fallito" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_ir_model_fields__ttype +msgid "Field Type" +msgstr "Tipo campo" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_ir_model_fields +msgid "Fields" +msgstr "Campi" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_follower_ids +msgid "Followers" +msgstr "Seguito da" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_partner_ids +msgid "Followers (Partners)" +msgstr "Seguito da (partner)" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "Icona Font Awesome es. fa-tasks" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Graph" +msgstr "Grafico" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Graph Jobs" +msgstr "Grafico lavori" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_jobs_count +msgid "Graph Jobs Count" +msgstr "Grafico conteggio lavori" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_uuid +msgid "Graph UUID" +msgstr "Grafico UUID" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Group By" +msgstr "Raggruppa per" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__has_message +msgid "Has Message" +msgstr "Ha un messaggio" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__id +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__id +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__id +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__id +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__id +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__id +msgid "ID" +msgstr "ID" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_icon +msgid "Icon" +msgstr "Icona" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "Icona per indicare un'attività eccezione." + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__identity_key +msgid "Identity Key" +msgstr "Chiave identità" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "If both parameters are 0, ALL jobs will be requeued!" +msgstr "Se entrambi i parametri sono 0, tutti i lavori verranno riaccodati!" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction +msgid "If checked, new messages require your attention." +msgstr "Se selezionata, nuovi messaggi richiedono attenzione." + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error +#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_sms_error +msgid "If checked, some messages have a delivery error." +msgstr "Se selezionata, alcuni messaggi hanno un errore di consegna." + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_function.py:0 +#, python-format +msgid "Invalid job function: {}" +msgstr "Funzione lavoro non valida: {}" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_is_follower +msgid "Is Follower" +msgstr "Segue" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_job_channel +msgid "Job Channels" +msgstr "Canali lavoro" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__job_function_id +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Job Function" +msgstr "Funzione lavoro" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_queue_job_function +#: model:ir.model,name:queue_job.model_queue_job_function +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__job_function_ids +#: model:ir.ui.menu,name:queue_job.menu_queue_job_function +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_form +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search +msgid "Job Functions" +msgstr "Funzioni lavoro" + +#. module: queue_job +#: model:ir.module.category,name:queue_job.module_category_queue_job +#: model:ir.ui.menu,name:queue_job.menu_queue_job_root +msgid "Job Queue" +msgstr "Coda lavoro" + +#. module: queue_job +#: model:res.groups,name:queue_job.group_queue_job_manager +msgid "Job Queue Manager" +msgstr "Gestore coda lavoro" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__ir_model_fields__ttype__job_serialized +msgid "Job Serialized" +msgstr "Lavoro serializzato" + +#. module: queue_job +#: model:mail.message.subtype,name:queue_job.mt_job_failed +msgid "Job failed" +msgstr "Lavro fallito" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/controllers/main.py:0 +#, python-format +msgid "Job interrupted and set to Done: nothing to do." +msgstr "Lavoro interrotto e impostato a completato: nulla da fare." + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__job_ids +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__job_ids +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__job_ids +#: model:ir.ui.menu,name:queue_job.menu_queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_graph +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_pivot +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Jobs" +msgstr "Lavori" + +#. module: queue_job +#: model:ir.actions.server,name:queue_job.ir_cron_queue_job_garbage_collector_ir_actions_server +msgid "Jobs Garbage Collector" +msgstr "Garbage collector lavori" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Jobs for graph %s" +msgstr "Lavori per grafico %s" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__kwargs +msgid "Kwargs" +msgstr "Kwargs" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_date +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_date +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_date +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Manually set to done by %s" +msgstr "Impostato manualmente a completato da %s" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__max_retries +msgid "Max. retries" +msgstr "Massimo tentativi" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error +msgid "Message Delivery error" +msgstr "Errore di consegna messaggio" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_ids +msgid "Messages" +msgstr "Messaggi" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__method +msgid "Method" +msgstr "Metodo" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__method_name +msgid "Method Name" +msgstr "Nome metodo" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__model_name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__model_id +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Model" +msgstr "Modello" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_function.py:0 +#, python-format +msgid "Model {} not found" +msgstr "Modello {} non trovato" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "Scadenza mia attività" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__name +msgid "Name" +msgstr "Nome" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "Scadenza prossima attività" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_summary +msgid "Next Activity Summary" +msgstr "Riepilogo prossima attività" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_id +msgid "Next Activity Type" +msgstr "Tipo prossima attività" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "No action available for this job" +msgstr "Nessuna azione disponibile per questo lavoro" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Not allowed to change field(s): {}" +msgstr "Non autorizzato a modificare i campi: {}" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction_counter +msgid "Number of Actions" +msgstr "Numero di azioni" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error_counter +msgid "Number of errors" +msgstr "Numero di errori" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "Numero di messaggi che richiedono un'azione" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "Numero di messaggi con errore di consegna" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__parent_id +msgid "Parent Channel" +msgstr "Canale padre" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_channel.py:0 +#, python-format +msgid "Parent channel required." +msgstr "Richiesto canale padre." + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_retry_pattern +msgid "" +"Pattern expressing from the count of retries on retryable errors, the number of of seconds to postpone the next execution. Setting the number of seconds to a 2-element tuple or list will randomize the retry interval between the 2 values.\n" +"Example: {1: 10, 5: 20, 10: 30, 15: 300}.\n" +"Example: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n" +"See the module description for details." +msgstr "" +"Schema derivante dal conteggio dei tentativi degli errori ripetibili, numero " +"di secondi per ritardare l'esecuzione successiva. Impostando il numero di " +"secondi ad una tupla di due elementi o un elenco renderà causale " +"l'intervallo tra i tentativi tra i due valori.\n" +"Esempio: {1: 10, 5: 20, 10: 30, 15: 300}.\n" +"Esempio: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n" +"Vedere la descrizione del modulo per i dettagli." + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__pending +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Pending" +msgstr "In attesa" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__priority +msgid "Priority" +msgstr "Priorità" + +#. module: queue_job +#: model:ir.ui.menu,name:queue_job.menu_queue +msgid "Queue" +msgstr "Coda" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_job +msgid "Queue Job" +msgstr "Lavoro in coda" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Queue jobs must be created by calling 'with_delay()'." +msgstr "Il lavoro in coda deve essere creato chiamando 'with_delay()'." + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__record_ids +msgid "Record" +msgstr "Record" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__records +msgid "Record(s)" +msgstr "Record" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Related" +msgstr "Collegato" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_related_action +msgid "Related Action" +msgstr "Azione collegata" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__related_action +msgid "Related Action (serialized)" +msgstr "Azione collegata (serializzata)" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Related Record" +msgstr "Record collegato" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Related Records" +msgstr "Record collegati" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_tree +msgid "Remaining days to execute" +msgstr "Giorni rimanenti all'esecuzione" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__removal_interval +msgid "Removal Interval" +msgstr "Intervallo rimozione" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job +msgid "Requeue" +msgstr "Rimetti in coda" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Requeue Job" +msgstr "Riaccoda lavoro" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_requeue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job +msgid "Requeue Jobs" +msgstr "Riaccoda lavori" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_user_id +msgid "Responsible User" +msgstr "Utente responsabile" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__result +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Result" +msgstr "Risultato" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Results" +msgstr "Risultati" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_retry_pattern +msgid "Retry Pattern" +msgstr "Riprova schema" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__retry_pattern +msgid "Retry Pattern (serialized)" +msgstr "Riprova schema (serializzato)" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_sms_error +msgid "SMS Delivery error" +msgstr "Errore consegna SMS" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_jobs_to_done +msgid "Set all selected jobs to done" +msgstr "Imposta a completati tutti i lavori selezionati" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done +msgid "Set jobs done" +msgstr "Imposta i lavori a completato" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_set_jobs_done +msgid "Set jobs to done" +msgstr "Imposta i lavori a completato" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Set to 'Done'" +msgstr "Imposta come 'Completato'" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done +msgid "Set to done" +msgstr "Imposta a completato" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__graph_uuid +msgid "Single shared identifier of a Graph. Empty for a single job." +msgstr "" +"Singolo identificatore condiviso di un grafico. Vuoto per un lavoro singolo." + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "" +"Something bad happened during the execution of the job. More details in the " +"'Exception Information' section." +msgstr "" +"Qualcosa è andato male durante l'esecuzione del lavoro. Maggiori dettagli " +"nella sezione 'informazioni eccezione'." + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_started +msgid "Start Date" +msgstr "Data inizio" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__started +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Started" +msgstr "Iniziato" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__state +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "State" +msgstr "Stato" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" +"Stato in base alle attività\n" +"Scaduto: la data richiesta è trascorsa\n" +"Oggi: la data attività è oggi\n" +"Pianificato: attività future." + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__func_string +msgid "Task" +msgstr "Lavoro" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_related_action +msgid "" +"The action when the button *Related Action* is used on a job. The default action is to open the view of the record related to the job. Configured as a dictionary with optional keys: enable, func_name, kwargs.\n" +"See the module description for details." +msgstr "" +"L'azione quando si usa il pulsante 'Azione collegata' in un lavoro. L'azione " +"predefinita è di aprire la vista del record collegato al lavoro. Configrata " +"come dizionari con chiavi opzionali: enable, func_name, kwargs.\n" +"Vedere la descrizione del modulo per i dettagli." + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__max_retries +msgid "" +"The job will fail if the number of tries reach the max. retries.\n" +"Retries are infinite when empty." +msgstr "" +"Il lavoro fallirà se il numero di tentativi raggiunge il massimo.\n" +"I tentativi sono infiniti quando vuoto." + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled +msgid "The selected jobs will be cancelled." +msgstr "I lavori selezionati verranno annullati." + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job +msgid "The selected jobs will be requeued." +msgstr "I lavori selezionati verranno riaccodati." + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done +msgid "The selected jobs will be set to done." +msgstr "I lavori selezionati verranno impostati a completato." + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Time (s)" +msgstr "Ora (e)" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__exec_time +msgid "Time required to execute this job in seconds. Average when grouped." +msgstr "" +"Tempo in secondi richiesto per eseguire il lavoro. Medio quando raggruppati." + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "Tipo di attività eccezione sul record." + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__uuid +msgid "UUID" +msgstr "UUID" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_function.py:0 +#, python-format +msgid "" +"Unexpected format of Related Action for {}.\n" +"Example of valid format:\n" +"{{\"enable\": True, \"func_name\": \"related_action_foo\", \"kwargs\" {{\"limit\": 10}}}}" +msgstr "" +"Formato inaspettato di azione colegata per {}.\n" +"Esempio di formato valido:\n" +"{{\"enable\": True, \"func_name\": \"related_action_foo\", \"kwargs\" {{" +"\"limit\": 10}}}}" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_function.py:0 +#, python-format +msgid "" +"Unexpected format of Retry Pattern for {}.\n" +"Example of valid format:\n" +"{{1: 300, 5: 600, 10: 1200, 15: 3000}}" +msgstr "" +"Formato inaspettato di schema tentativo per {}.\n" +"Esempio di formato valido:\n" +"{{1: 300, 5: 600, 10: 1200, 15: 3000}}" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__user_id +msgid "User ID" +msgstr "ID utente" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__wait_dependencies +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Wait Dependencies" +msgstr "Attesa dipendenze" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_requeue_job +msgid "Wizard to requeue a selection of jobs" +msgstr "Procedura guidata per riaccodare una selezione di lavori" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__worker_pid +msgid "Worker Pid" +msgstr "PID worker" diff --git a/queue_job/i18n/queue_job.pot b/queue_job/i18n/queue_job.pot new file mode 100644 index 0000000000..30d5b245fe --- /dev/null +++ b/queue_job/i18n/queue_job.pot @@ -0,0 +1,907 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * queue_job +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.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: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "" +"
\n" +" If the max. retries is 0, the number of retries is infinite." +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/controllers/main.py:0 +#, python-format +msgid "Access Denied" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction +msgid "Action Needed" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_ids +msgid "Activities" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_state +msgid "Activity State" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_icon +msgid "Activity Type Icon" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__args +msgid "Args" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_attachment_count +msgid "Attachment Count" +msgstr "" + +#. module: queue_job +#: model:ir.actions.server,name:queue_job.ir_cron_autovacuum_queue_jobs_ir_actions_server +msgid "AutoVacuum Job Queue" +msgstr "" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_base +msgid "Base" +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done +msgid "Cancel" +msgstr "" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_jobs_to_cancelled +msgid "Cancel all selected jobs" +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Cancel job" +msgstr "" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_set_jobs_cancelled +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled +msgid "Cancel jobs" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__cancelled +msgid "Cancelled" +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Cancelled by %s" +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_channel.py:0 +#, python-format +msgid "Cannot change the root channel" +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_channel.py:0 +#, python-format +msgid "Cannot remove the root channel" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel_id +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Channel" +msgstr "" + +#. module: queue_job +#: model:ir.model.constraint,message:queue_job.constraint_queue_job_channel_name_uniq +msgid "Channel complete name must be unique" +msgstr "" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_queue_job_channel +#: model:ir.ui.menu,name:queue_job.menu_queue_job_channel +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_form +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_search +msgid "Channels" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__company_id +msgid "Company" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel_method_name +msgid "Complete Method Name" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__complete_name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel +msgid "Complete Name" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_created +msgid "Created Date" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_uid +msgid "Created by" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_date +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_date +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_date +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_date +msgid "Created on" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__retry +msgid "Current try" +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Current try / max. retries" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_cancelled +msgid "Date Cancelled" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_done +msgid "Date Done" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependencies +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Dependencies" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependency_graph +msgid "Dependency Graph" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__name +msgid "Description" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__display_name +msgid "Display Name" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__done +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Done" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_enqueued +msgid "Enqueue Time" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__enqueued +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Enqueued" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_name +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Exception" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_info +msgid "Exception Info" +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Exception Information" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_message +msgid "Exception Message" +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Exception message" +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Exception:" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__eta +msgid "Execute only after" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__exec_time +msgid "Execution Time (avg)" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__failed +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Failed" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_ir_model_fields__ttype +msgid "Field Type" +msgstr "" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_ir_model_fields +msgid "Fields" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_follower_ids +msgid "Followers" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_partner_ids +msgid "Followers (Partners)" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Graph" +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Graph Jobs" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_jobs_count +msgid "Graph Jobs Count" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_uuid +msgid "Graph UUID" +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Group By" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__has_message +msgid "Has Message" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__id +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__id +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__id +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__id +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__id +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__id +msgid "ID" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_icon +msgid "Icon" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__identity_key +msgid "Identity Key" +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "If both parameters are 0, ALL jobs will be requeued!" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction +msgid "If checked, new messages require your attention." +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error +#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_sms_error +msgid "If checked, some messages have a delivery error." +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_function.py:0 +#, python-format +msgid "Invalid job function: {}" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_is_follower +msgid "Is Follower" +msgstr "" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_job_channel +msgid "Job Channels" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__job_function_id +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Job Function" +msgstr "" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_queue_job_function +#: model:ir.model,name:queue_job.model_queue_job_function +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__job_function_ids +#: model:ir.ui.menu,name:queue_job.menu_queue_job_function +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_form +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search +msgid "Job Functions" +msgstr "" + +#. module: queue_job +#: model:ir.module.category,name:queue_job.module_category_queue_job +#: model:ir.ui.menu,name:queue_job.menu_queue_job_root +msgid "Job Queue" +msgstr "" + +#. module: queue_job +#: model:res.groups,name:queue_job.group_queue_job_manager +msgid "Job Queue Manager" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__ir_model_fields__ttype__job_serialized +msgid "Job Serialized" +msgstr "" + +#. module: queue_job +#: model:mail.message.subtype,name:queue_job.mt_job_failed +msgid "Job failed" +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/controllers/main.py:0 +#, python-format +msgid "Job interrupted and set to Done: nothing to do." +msgstr "" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__job_ids +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__job_ids +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__job_ids +#: model:ir.ui.menu,name:queue_job.menu_queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_graph +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_pivot +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Jobs" +msgstr "" + +#. module: queue_job +#: model:ir.actions.server,name:queue_job.ir_cron_queue_job_garbage_collector_ir_actions_server +msgid "Jobs Garbage Collector" +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Jobs for graph %s" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__kwargs +msgid "Kwargs" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_date +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_date +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_date +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_date +msgid "Last Updated on" +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Manually set to done by %s" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__max_retries +msgid "Max. retries" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error +msgid "Message Delivery error" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_ids +msgid "Messages" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__method +msgid "Method" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__method_name +msgid "Method Name" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__model_name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__model_id +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Model" +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_function.py:0 +#, python-format +msgid "Model {} not found" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__name +msgid "Name" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_summary +msgid "Next Activity Summary" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_id +msgid "Next Activity Type" +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "No action available for this job" +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Not allowed to change field(s): {}" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction_counter +msgid "Number of Actions" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error_counter +msgid "Number of errors" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__parent_id +msgid "Parent Channel" +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_channel.py:0 +#, python-format +msgid "Parent channel required." +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_retry_pattern +msgid "" +"Pattern expressing from the count of retries on retryable errors, the number of of seconds to postpone the next execution. Setting the number of seconds to a 2-element tuple or list will randomize the retry interval between the 2 values.\n" +"Example: {1: 10, 5: 20, 10: 30, 15: 300}.\n" +"Example: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n" +"See the module description for details." +msgstr "" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__pending +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Pending" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__priority +msgid "Priority" +msgstr "" + +#. module: queue_job +#: model:ir.ui.menu,name:queue_job.menu_queue +msgid "Queue" +msgstr "" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_job +msgid "Queue Job" +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Queue jobs must be created by calling 'with_delay()'." +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__record_ids +msgid "Record" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__records +msgid "Record(s)" +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Related" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_related_action +msgid "Related Action" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__related_action +msgid "Related Action (serialized)" +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Related Record" +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Related Records" +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_tree +msgid "Remaining days to execute" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__removal_interval +msgid "Removal Interval" +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job +msgid "Requeue" +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Requeue Job" +msgstr "" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_requeue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job +msgid "Requeue Jobs" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_user_id +msgid "Responsible User" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__result +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Result" +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Results" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_retry_pattern +msgid "Retry Pattern" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__retry_pattern +msgid "Retry Pattern (serialized)" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_sms_error +msgid "SMS Delivery error" +msgstr "" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_jobs_to_done +msgid "Set all selected jobs to done" +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done +msgid "Set jobs done" +msgstr "" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_set_jobs_done +msgid "Set jobs to done" +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Set to 'Done'" +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done +msgid "Set to done" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__graph_uuid +msgid "Single shared identifier of a Graph. Empty for a single job." +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "" +"Something bad happened during the execution of the job. More details in the " +"'Exception Information' section." +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_started +msgid "Start Date" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__started +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Started" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__state +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "State" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__func_string +msgid "Task" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_related_action +msgid "" +"The action when the button *Related Action* is used on a job. The default action is to open the view of the record related to the job. Configured as a dictionary with optional keys: enable, func_name, kwargs.\n" +"See the module description for details." +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__max_retries +msgid "" +"The job will fail if the number of tries reach the max. retries.\n" +"Retries are infinite when empty." +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled +msgid "The selected jobs will be cancelled." +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job +msgid "The selected jobs will be requeued." +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done +msgid "The selected jobs will be set to done." +msgstr "" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Time (s)" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__exec_time +msgid "Time required to execute this job in seconds. Average when grouped." +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__uuid +msgid "UUID" +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_function.py:0 +#, python-format +msgid "" +"Unexpected format of Related Action for {}.\n" +"Example of valid format:\n" +"{{\"enable\": True, \"func_name\": \"related_action_foo\", \"kwargs\" {{\"limit\": 10}}}}" +msgstr "" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_function.py:0 +#, python-format +msgid "" +"Unexpected format of Retry Pattern for {}.\n" +"Example of valid format:\n" +"{{1: 300, 5: 600, 10: 1200, 15: 3000}}" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__user_id +msgid "User ID" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__wait_dependencies +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Wait Dependencies" +msgstr "" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_requeue_job +msgid "Wizard to requeue a selection of jobs" +msgstr "" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__worker_pid +msgid "Worker Pid" +msgstr "" diff --git a/queue_job/i18n/zh_CN.po b/queue_job/i18n/zh_CN.po new file mode 100644 index 0000000000..2075f872c8 --- /dev/null +++ b/queue_job/i18n/zh_CN.po @@ -0,0 +1,963 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * queue_job +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-07-02 09:47+0000\n" +"Last-Translator: xtanuiha \n" +"Language-Team: none\n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.17\n" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "" +"
\n" +" If the max. " +"retries is 0, the number of retries is infinite." +msgstr "" +"
\n" +" " +"如果最大重试次数设置为0,则表示重试次数是无限的。" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/controllers/main.py:0 +#, python-format +msgid "Access Denied" +msgstr "拒绝访问" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction +msgid "Action Needed" +msgstr "前置操作" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_ids +msgid "Activities" +msgstr "活动" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "活动异常装饰" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_state +msgid "Activity State" +msgstr "活动状态" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_icon +msgid "Activity Type Icon" +msgstr "活动类型图标" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__args +msgid "Args" +msgstr "位置参数" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_attachment_count +msgid "Attachment Count" +msgstr "附件数量" + +#. module: queue_job +#: model:ir.actions.server,name:queue_job.ir_cron_autovacuum_queue_jobs_ir_actions_server +msgid "AutoVacuum Job Queue" +msgstr "自动清空作业队列" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_base +msgid "Base" +msgstr "基础" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done +msgid "Cancel" +msgstr "取消" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_jobs_to_cancelled +msgid "Cancel all selected jobs" +msgstr "取消所有选中作业" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Cancel job" +msgstr "取消作业" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_set_jobs_cancelled +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled +msgid "Cancel jobs" +msgstr "取消作业" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__cancelled +msgid "Cancelled" +msgstr "已取消" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Cancelled by %s" +msgstr "被%s取消" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_channel.py:0 +#, python-format +msgid "Cannot change the root channel" +msgstr "无法更改root频道" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_channel.py:0 +#, python-format +msgid "Cannot remove the root channel" +msgstr "无法删除root频道" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel_id +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Channel" +msgstr "频道" + +#. module: queue_job +#: model:ir.model.constraint,message:queue_job.constraint_queue_job_channel_name_uniq +msgid "Channel complete name must be unique" +msgstr "频道完整名称必须是唯一的" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_queue_job_channel +#: model:ir.ui.menu,name:queue_job.menu_queue_job_channel +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_form +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_search +msgid "Channels" +msgstr "频道" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__company_id +msgid "Company" +msgstr "公司" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel_method_name +msgid "Complete Method Name" +msgstr "完整方法名" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__complete_name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel +msgid "Complete Name" +msgstr "完整名称" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_created +msgid "Created Date" +msgstr "创建日期" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_uid +msgid "Created by" +msgstr "创建者" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_date +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_date +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_date +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_date +msgid "Created on" +msgstr "创建时间" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__retry +msgid "Current try" +msgstr "当前尝试" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Current try / max. retries" +msgstr "当前尝试/最大重试次数" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_cancelled +msgid "Date Cancelled" +msgstr "取消日期" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_done +msgid "Date Done" +msgstr "完成日期" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependencies +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Dependencies" +msgstr "依赖项" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependency_graph +msgid "Dependency Graph" +msgstr "依赖关系图" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__name +msgid "Description" +msgstr "说明" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__display_name +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__display_name +msgid "Display Name" +msgstr "显示名称" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__done +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Done" +msgstr "完成" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_enqueued +msgid "Enqueue Time" +msgstr "排队时间" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__enqueued +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Enqueued" +msgstr "排队" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_name +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Exception" +msgstr "异常" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_info +msgid "Exception Info" +msgstr "异常信息" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Exception Information" +msgstr "异常信息" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_message +msgid "Exception Message" +msgstr "异常信息" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Exception message" +msgstr "异常信息" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Exception:" +msgstr "异常:" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__eta +msgid "Execute only after" +msgstr "仅在此之后执行" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__exec_time +msgid "Execution Time (avg)" +msgstr "平均执行时间" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__failed +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Failed" +msgstr "失败" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_ir_model_fields__ttype +msgid "Field Type" +msgstr "字段类型" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_ir_model_fields +msgid "Fields" +msgstr "字段" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_follower_ids +msgid "Followers" +msgstr "关注者" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_partner_ids +msgid "Followers (Partners)" +msgstr "关注者(业务伙伴)" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "Font Awesome 图标,例如:fa-tasks" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Graph" +msgstr "图表" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Graph Jobs" +msgstr "图表作业" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_jobs_count +msgid "Graph Jobs Count" +msgstr "图表作业数量" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_uuid +msgid "Graph UUID" +msgstr "图表唯一标识符(UUID)" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Group By" +msgstr "分组" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__has_message +msgid "Has Message" +msgstr "有消息" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__id +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__id +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__id +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__id +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__id +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__id +msgid "ID" +msgstr "ID" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_icon +msgid "Icon" +msgstr "图标" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "指示异常活动的图标。" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__identity_key +msgid "Identity Key" +msgstr "身份密钥" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "If both parameters are 0, ALL jobs will be requeued!" +msgstr "如果两个参数都为0,所有任务都将被重新排队!" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction +msgid "If checked, new messages require your attention." +msgstr "确认后, 出现提示消息。" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error +#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_sms_error +msgid "If checked, some messages have a delivery error." +msgstr "如果勾选此项, 某些消息将会产生传递错误。" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_function.py:0 +#, python-format +msgid "Invalid job function: {}" +msgstr "无效的作业函数:{}" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_is_follower +msgid "Is Follower" +msgstr "关注者" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_job_channel +msgid "Job Channels" +msgstr "作业频道" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__job_function_id +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Job Function" +msgstr "作业函数" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_queue_job_function +#: model:ir.model,name:queue_job.model_queue_job_function +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__job_function_ids +#: model:ir.ui.menu,name:queue_job.menu_queue_job_function +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_form +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search +msgid "Job Functions" +msgstr "作业函数" + +#. module: queue_job +#: model:ir.module.category,name:queue_job.module_category_queue_job +#: model:ir.ui.menu,name:queue_job.menu_queue_job_root +msgid "Job Queue" +msgstr "作业队列" + +#. module: queue_job +#: model:res.groups,name:queue_job.group_queue_job_manager +msgid "Job Queue Manager" +msgstr "作业队列管理员" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__ir_model_fields__ttype__job_serialized +msgid "Job Serialized" +msgstr "任务序列化" + +#. module: queue_job +#: model:mail.message.subtype,name:queue_job.mt_job_failed +msgid "Job failed" +msgstr "作业失败" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/controllers/main.py:0 +#, python-format +msgid "Job interrupted and set to Done: nothing to do." +msgstr "作业中断并设置为已完成:无需执行任何操作。" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__job_ids +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__job_ids +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__job_ids +#: model:ir.ui.menu,name:queue_job.menu_queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_graph +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_pivot +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Jobs" +msgstr "作业" + +#. module: queue_job +#: model:ir.actions.server,name:queue_job.ir_cron_queue_job_garbage_collector_ir_actions_server +msgid "Jobs Garbage Collector" +msgstr "作业垃圾收集器" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Jobs for graph %s" +msgstr "图表 %s 的作业" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__kwargs +msgid "Kwargs" +msgstr "关键字参数" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_uid +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_uid +msgid "Last Updated by" +msgstr "最后更新者" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_date +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_date +#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_date +#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_date +msgid "Last Updated on" +msgstr "最后更新时间" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Manually set to done by %s" +msgstr "由%s手动设置为完成" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__max_retries +msgid "Max. retries" +msgstr "最大重试次数" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error +msgid "Message Delivery error" +msgstr "消息递送错误" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_ids +msgid "Messages" +msgstr "消息" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__method +msgid "Method" +msgstr "方法名称" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__method_name +msgid "Method Name" +msgstr "方法名称" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__model_name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__model_id +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Model" +msgstr "模型" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_function.py:0 +#, python-format +msgid "Model {} not found" +msgstr "模型 {} 未找到" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "我的活动截止日期" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__name +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__name +msgid "Name" +msgstr "名称" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "下一活动截止日期" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_summary +msgid "Next Activity Summary" +msgstr "下一活动摘要" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_id +msgid "Next Activity Type" +msgstr "下一活动类型" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "No action available for this job" +msgstr "此作业无法执行任何操作" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Not allowed to change field(s): {}" +msgstr "不允许更改字段:{}" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction_counter +msgid "Number of Actions" +msgstr "操作次数" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error_counter +msgid "Number of errors" +msgstr "错误数量" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "需要处理的消息数量" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "递送错误消息数量" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__parent_id +msgid "Parent Channel" +msgstr "父频道" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_channel.py:0 +#, python-format +msgid "Parent channel required." +msgstr "父频道必填。" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_retry_pattern +msgid "" +"Pattern expressing from the count of retries on retryable errors, the number " +"of of seconds to postpone the next execution. Setting the number of seconds " +"to a 2-element tuple or list will randomize the retry interval between the 2 " +"values.\n" +"Example: {1: 10, 5: 20, 10: 30, 15: 300}.\n" +"Example: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n" +"See the module description for details." +msgstr "" +"表达式用于根据重试可重试错误的次数,指定下一次执行推迟的秒数。将秒数设置为一" +"个两个元素的元组或列表将会在两个值之间随机化重试间隔。 \n" +"示例:{1: 10, 5: 20, 10: 30, 15: 300}。 \n" +"示例:{1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}。 \n" +"详情请参阅模块描述。" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__pending +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Pending" +msgstr "等待" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__priority +msgid "Priority" +msgstr "优先级" + +#. module: queue_job +#: model:ir.ui.menu,name:queue_job.menu_queue +msgid "Queue" +msgstr "队列" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_job +msgid "Queue Job" +msgstr "队列作业" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Queue jobs must be created by calling 'with_delay()'." +msgstr "队列任务必须通过调用'with_delay()'方法来创建。" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__record_ids +msgid "Record" +msgstr "记录" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__records +msgid "Record(s)" +msgstr "记录" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Related" +msgstr "相关的" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_related_action +msgid "Related Action" +msgstr "相关动作" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__related_action +msgid "Related Action (serialized)" +msgstr "相关操作(已序列化)" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Related Record" +msgstr "相关记录" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "Related Records" +msgstr "相关记录" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_tree +msgid "Remaining days to execute" +msgstr "剩余执行天数" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__removal_interval +msgid "Removal Interval" +msgstr "清除间隔" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job +msgid "Requeue" +msgstr "重新排队" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Requeue Job" +msgstr "重新排队作业" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_requeue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job +msgid "Requeue Jobs" +msgstr "重新排队作业" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_user_id +msgid "Responsible User" +msgstr "负责的用户" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__result +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Result" +msgstr "结果" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Results" +msgstr "结果" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_retry_pattern +msgid "Retry Pattern" +msgstr "重试模式" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__retry_pattern +msgid "Retry Pattern (serialized)" +msgstr "重试模式(已序列化)" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_sms_error +msgid "SMS Delivery error" +msgstr "短信传递错误" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_jobs_to_done +msgid "Set all selected jobs to done" +msgstr "将所有选定的作业设置为完成" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done +msgid "Set jobs done" +msgstr "设置作业完成" + +#. module: queue_job +#: model:ir.actions.act_window,name:queue_job.action_set_jobs_done +msgid "Set jobs to done" +msgstr "将作业设置为完成" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Set to 'Done'" +msgstr "设置为“完成”" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done +msgid "Set to done" +msgstr "设置为完成" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__graph_uuid +msgid "Single shared identifier of a Graph. Empty for a single job." +msgstr "图的唯一共享标识符。对于单个任务,则为空。" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job.py:0 +#, python-format +msgid "" +"Something bad happened during the execution of the job. More details in the " +"'Exception Information' section." +msgstr "" +"在执行作业期间发生了一些不好的事情。有关详细信息,请参见“异常信息”部分。" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_started +msgid "Start Date" +msgstr "开始日期" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__started +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Started" +msgstr "开始" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__state +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "State" +msgstr "状态" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" +"基于活动的状态\n" +"逾期:已经超过截止日期\n" +"现今:活动日期是当天\n" +"计划:未来的活动。" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__func_string +msgid "Task" +msgstr "任务" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_related_action +msgid "" +"The action when the button *Related Action* is used on a job. The default " +"action is to open the view of the record related to the job. Configured as a " +"dictionary with optional keys: enable, func_name, kwargs.\n" +"See the module description for details." +msgstr "" +"当在作业上使用“相关操作”按钮时所执行的动作。默认动作是打开与该作业相关的记录" +"视图。此配置作为一个字典,包含可选键:enable, func_name, kwargs。 " +"详细配置请参阅模块说明。" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__max_retries +msgid "" +"The job will fail if the number of tries reach the max. retries.\n" +"Retries are infinite when empty." +msgstr "" +"如果尝试次数达到最大重试次数,作业将失败。\n" +"空的时候重试是无限的。" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled +msgid "The selected jobs will be cancelled." +msgstr "被选中的任务将被取消。" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job +msgid "The selected jobs will be requeued." +msgstr "所选作业将重新排队。" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done +msgid "The selected jobs will be set to done." +msgstr "所选作业将设置为完成。" + +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form +msgid "Time (s)" +msgstr "时间(秒)" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__exec_time +msgid "Time required to execute this job in seconds. Average when grouped." +msgstr "以秒为单位执行此任务所需的时间。分组时为平均值。" + +#. module: queue_job +#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "记录的异常活动的类型。" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__uuid +msgid "UUID" +msgstr "UUID" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_function.py:0 +#, python-format +msgid "" +"Unexpected format of Related Action for {}.\n" +"Example of valid format:\n" +"{{\"enable\": True, \"func_name\": \"related_action_foo\", \"kwargs" +"\" {{\"limit\": 10}}}}" +msgstr "" +"对于 {},关联操作的格式不符合预期。 \n" +"有效格式的示例:\n" +"{{\"enable\": True, \"func_name\": \"related_action_foo\", \"kwargs\" {{" +"\"limit\": 10}}}}" + +#. module: queue_job +#. odoo-python +#: code:addons/queue_job/models/queue_job_function.py:0 +#, python-format +msgid "" +"Unexpected format of Retry Pattern for {}.\n" +"Example of valid format:\n" +"{{1: 300, 5: 600, 10: 1200, 15: 3000}}" +msgstr "" +"对于 {},重试模式的格式不符合预期。 有效格式的示例:\n" +"{{1: 300, 5: 600, 10: 1200, 15: 3000}}" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__user_id +msgid "User ID" +msgstr "用户" + +#. module: queue_job +#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__wait_dependencies +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Wait Dependencies" +msgstr "等待依赖项" + +#. module: queue_job +#: model:ir.model,name:queue_job.model_queue_requeue_job +msgid "Wizard to requeue a selection of jobs" +msgstr "重新排队向导所选的作业" + +#. module: queue_job +#: model:ir.model.fields,field_description:queue_job.field_queue_job__worker_pid +msgid "Worker Pid" +msgstr "工作进程PID" + +#~ msgid "Last Modified on" +#~ msgstr "最后修改日" + +#~ msgid "Main Attachment" +#~ msgstr "附件" + +#~ msgid "Number of messages which requires an action" +#~ msgstr "需要操作消息数量" + +#~ msgid "" +#~ " If the max. retries is 0, the number " +#~ "of retries is infinite." +#~ msgstr "" +#~ "如果最大重试次数是0,则重试次数是无限" +#~ "的。" + +#~ msgid "Override Channel" +#~ msgstr "覆盖频道" + +#~ msgid "Number of unread messages" +#~ msgstr "未读消息数量" diff --git a/queue_job/job.py b/queue_job/job.py new file mode 100644 index 0000000000..31226303c7 --- /dev/null +++ b/queue_job/job.py @@ -0,0 +1,885 @@ +# Copyright 2013-2020 Camptocamp +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +import hashlib +import inspect +import logging +import os +import sys +import uuid +import weakref +from datetime import datetime, timedelta +from functools import total_ordering +from random import randint + +import odoo + +from .exception import FailedJobError, NoSuchJobError, RetryableJobError + +WAIT_DEPENDENCIES = "wait_dependencies" +PENDING = "pending" +ENQUEUED = "enqueued" +CANCELLED = "cancelled" +DONE = "done" +STARTED = "started" +FAILED = "failed" + +STATES = [ + (WAIT_DEPENDENCIES, "Wait Dependencies"), + (PENDING, "Pending"), + (ENQUEUED, "Enqueued"), + (STARTED, "Started"), + (DONE, "Done"), + (CANCELLED, "Cancelled"), + (FAILED, "Failed"), +] + +DEFAULT_PRIORITY = 10 # used by the PriorityQueue to sort the jobs +DEFAULT_MAX_RETRIES = 5 +RETRY_INTERVAL = 10 * 60 # seconds + +_logger = logging.getLogger(__name__) + + +def identity_exact(job_): + """Identity function using the model, method and all arguments as key + + When used, this identity key will have the effect that when a job should be + created and a pending job with the exact same recordset and arguments, the + second will not be created. + + It should be used with the ``identity_key`` argument: + + .. python:: + + from odoo.addons.queue_job.job import identity_exact + + # [...] + delayable = self.with_delay(identity_key=identity_exact) + delayable.export_record(force=True) + + Alternative identity keys can be built using the various fields of the job. + For example, you could compute a hash using only some arguments of + the job. + + .. python:: + + def identity_example(job_): + hasher = hashlib.sha1() + hasher.update(job_.model_name) + hasher.update(job_.method_name) + hasher.update(str(sorted(job_.recordset.ids))) + hasher.update(str(job_.args[1])) + hasher.update(str(job_.kwargs.get('foo', ''))) + return hasher.hexdigest() + + Usually you will probably always want to include at least the name of the + model and method. + """ + hasher = identity_exact_hasher(job_) + return hasher.hexdigest() + + +def identity_exact_hasher(job_): + """Prepare hasher object for identity_exact.""" + hasher = hashlib.sha1() + hasher.update(job_.model_name.encode("utf-8")) + hasher.update(job_.method_name.encode("utf-8")) + hasher.update(str(sorted(job_.recordset.ids)).encode("utf-8")) + hasher.update(str(job_.args).encode("utf-8")) + hasher.update(str(sorted(job_.kwargs.items())).encode("utf-8")) + return hasher + + +@total_ordering +class Job: + """A Job is a task to execute. It is the in-memory representation of a job. + + Jobs are stored in the ``queue.job`` Odoo Model, but they are handled + through this class. + + .. attribute:: uuid + + Id (UUID) of the job. + + .. attribute:: graph_uuid + + Shared UUID of the job's graph. Empty if the job is a single job. + + .. attribute:: state + + State of the job, can pending, enqueued, started, done or failed. + The start state is pending and the final state is done. + + .. attribute:: retry + + The current try, starts at 0 and each time the job is executed, + it increases by 1. + + .. attribute:: max_retries + + The maximum number of retries allowed before the job is + considered as failed. + + .. attribute:: args + + Arguments passed to the function when executed. + + .. attribute:: kwargs + + Keyword arguments passed to the function when executed. + + .. attribute:: description + + Human description of the job. + + .. attribute:: func + + The python function itself. + + .. attribute:: model_name + + Odoo model on which the job will run. + + .. attribute:: priority + + Priority of the job, 0 being the higher priority. + + .. attribute:: date_created + + Date and time when the job was created. + + .. attribute:: date_enqueued + + Date and time when the job was enqueued. + + .. attribute:: date_started + + Date and time when the job was started. + + .. attribute:: date_done + + Date and time when the job was done. + + .. attribute:: result + + A description of the result (for humans). + + .. attribute:: exc_name + + Exception error name when the job failed. + + .. attribute:: exc_message + + Exception error message when the job failed. + + .. attribute:: exc_info + + Exception information (traceback) when the job failed. + + .. attribute:: user_id + + Odoo user id which created the job + + .. attribute:: eta + + Estimated Time of Arrival of the job. It will not be executed + before this date/time. + + .. attribute:: recordset + + Model recordset when we are on a delayed Model method + + .. attribute::channel + + The complete name of the channel to use to process the job. If + provided it overrides the one defined on the job's function. + + .. attribute::identity_key + + A key referencing the job, multiple job with the same key will not + be added to a channel if the existing job with the same key is not yet + started or executed. + + """ + + @classmethod + def load(cls, env, job_uuid): + """Read a single job from the Database + + Raise an error if the job is not found. + """ + stored = cls.db_records_from_uuids(env, [job_uuid]) + if not stored: + raise NoSuchJobError(f"Job {job_uuid} does no longer exist in the storage.") + return cls._load_from_db_record(stored) + + @classmethod + def load_many(cls, env, job_uuids): + """Read jobs in batch from the Database + + Jobs not found are ignored. + """ + recordset = cls.db_records_from_uuids(env, job_uuids) + return {cls._load_from_db_record(record) for record in recordset} + + @classmethod + def _load_from_db_record(cls, job_db_record): + stored = job_db_record + + args = stored.args + kwargs = stored.kwargs + method_name = stored.method_name + + recordset = stored.records + method = getattr(recordset, method_name) + + eta = None + if stored.eta: + eta = stored.eta + + job_ = cls( + method, + args=args, + kwargs=kwargs, + priority=stored.priority, + eta=eta, + job_uuid=stored.uuid, + description=stored.name, + channel=stored.channel, + identity_key=stored.identity_key, + ) + + if stored.date_created: + job_.date_created = stored.date_created + + if stored.date_enqueued: + job_.date_enqueued = stored.date_enqueued + + if stored.date_started: + job_.date_started = stored.date_started + + if stored.date_done: + job_.date_done = stored.date_done + + if stored.date_cancelled: + job_.date_cancelled = stored.date_cancelled + + job_.state = stored.state + job_.graph_uuid = stored.graph_uuid if stored.graph_uuid else None + job_.result = stored.result if stored.result else None + job_.exc_info = stored.exc_info if stored.exc_info else None + job_.retry = stored.retry + job_.max_retries = stored.max_retries + if stored.company_id: + job_.company_id = stored.company_id.id + job_.identity_key = stored.identity_key + job_.worker_pid = stored.worker_pid + + job_.__depends_on_uuids.update(stored.dependencies.get("depends_on", [])) + job_.__reverse_depends_on_uuids.update( + stored.dependencies.get("reverse_depends_on", []) + ) + return job_ + + def job_record_with_same_identity_key(self): + """Check if a job to be executed with the same key exists.""" + existing = ( + self.env["queue.job"] + .sudo() + .search( + [ + ("identity_key", "=", self.identity_key), + ("state", "in", [WAIT_DEPENDENCIES, PENDING, ENQUEUED]), + ], + limit=1, + ) + ) + return existing + + # TODO to deprecate (not called anymore) + @classmethod + def enqueue( + cls, + func, + args=None, + kwargs=None, + priority=None, + eta=None, + max_retries=None, + description=None, + channel=None, + identity_key=None, + ): + """Create a Job and enqueue it in the queue. Return the job uuid. + + This expects the arguments specific to the job to be already extracted + from the ones to pass to the job function. + + If the identity key is the same than the one in a pending job, + no job is created and the existing job is returned + + """ + new_job = cls( + func=func, + args=args, + kwargs=kwargs, + priority=priority, + eta=eta, + max_retries=max_retries, + description=description, + channel=channel, + identity_key=identity_key, + ) + return new_job._enqueue_job() + + # TODO to deprecate (not called anymore) + def _enqueue_job(self): + if self.identity_key: + existing = self.job_record_with_same_identity_key() + if existing: + _logger.debug( + "a job has not been enqueued due to having " + "the same identity key (%s) than job %s", + self.identity_key, + existing.uuid, + ) + return Job._load_from_db_record(existing) + self.store() + _logger.debug( + "enqueued %s:%s(*%r, **%r) with uuid: %s", + self.recordset, + self.method_name, + self.args, + self.kwargs, + self.uuid, + ) + return self + + @staticmethod + def db_records_from_uuids(env, job_uuids): + model = env["queue.job"].sudo() + record = model.search([("uuid", "in", tuple(job_uuids))]) + return record.with_env(env).sudo() + + def __init__( + self, + func, + args=None, + kwargs=None, + priority=None, + eta=None, + job_uuid=None, + max_retries=None, + description=None, + channel=None, + identity_key=None, + ): + """Create a Job + + :param func: function to execute + :type func: function + :param args: arguments for func + :type args: tuple + :param kwargs: keyworkd arguments for func + :type kwargs: dict + :param priority: priority of the job, + the smaller is the higher priority + :type priority: int + :param eta: the job can be executed only after this datetime + (or now + timedelta) + :type eta: datetime or timedelta + :param job_uuid: UUID of the job + :param max_retries: maximum number of retries before giving up and set + the job state to 'failed'. A value of 0 means infinite retries. + :param description: human description of the job. If None, description + is computed from the function doc or name + :param channel: The complete channel name to use to process the job. + :param identity_key: A hash to uniquely identify a job, or a function + that returns this hash (the function takes the job + as argument) + """ + if args is None: + args = () + if isinstance(args, list): + args = tuple(args) + assert isinstance(args, tuple), f"{args}: args are not a tuple" + if kwargs is None: + kwargs = {} + + assert isinstance(kwargs, dict), f"{kwargs}: kwargs are not a dict" + + if not _is_model_method(func): + raise TypeError("Job accepts only methods of Models") + + recordset = func.__self__ + env = recordset.env + self.method_name = func.__name__ + self.recordset = recordset + + self.env = env + self.job_model = self.env["queue.job"] + self.job_model_name = "queue.job" + + self.job_config = ( + self.env["queue.job.function"].sudo().job_config(self.job_function_name) + ) + + self.state = PENDING + + self.retry = 0 + if max_retries is None: + self.max_retries = DEFAULT_MAX_RETRIES + else: + self.max_retries = max_retries + + self._uuid = job_uuid + self.graph_uuid = None + + self.args = args + self.kwargs = kwargs + + self.__depends_on_uuids = set() + self.__reverse_depends_on_uuids = set() + self._depends_on = set() + self._reverse_depends_on = weakref.WeakSet() + + self.priority = priority + if self.priority is None: + self.priority = DEFAULT_PRIORITY + + self.date_created = datetime.now() + self._description = description + + if isinstance(identity_key, str): + self._identity_key = identity_key + self._identity_key_func = None + else: + # we'll compute the key on the fly when called + # from the function + self._identity_key = None + self._identity_key_func = identity_key + + self.date_enqueued = None + self.date_started = None + self.date_done = None + self.date_cancelled = None + + self.result = None + self.exc_name = None + self.exc_message = None + self.exc_info = None + + if "company_id" in env.context: + company_id = env.context["company_id"] + else: + company_id = env.company.id + self.company_id = company_id + self._eta = None + self.eta = eta + self.channel = channel + self.worker_pid = None + + def add_depends(self, jobs): + if self in jobs: + raise ValueError("job cannot depend on itself") + self.__depends_on_uuids |= {j.uuid for j in jobs} + self._depends_on.update(jobs) + for parent in jobs: + parent.__reverse_depends_on_uuids.add(self.uuid) + parent._reverse_depends_on.add(self) + if any(j.state != DONE for j in jobs): + self.state = WAIT_DEPENDENCIES + + def perform(self): + """Execute the job. + + The job is executed with the user which has initiated it. + """ + self.retry += 1 + try: + self.result = self.func(*tuple(self.args), **self.kwargs) + except RetryableJobError as err: + if err.ignore_retry: + self.retry -= 1 + raise + elif not self.max_retries: # infinite retries + raise + elif self.retry >= self.max_retries: + type_, value, traceback = sys.exc_info() + # change the exception type but keep the original + # traceback and message: + # http://blog.ianbicking.org/2007/09/12/re-raising-exceptions/ + new_exc = FailedJobError( + "Max. retries (%d) reached: %s" % (self.max_retries, value or type_) + ) + raise new_exc from err + raise + + return self.result + + def _get_common_dependent_jobs_query(self): + return """ + UPDATE queue_job + SET state = %s + FROM ( + SELECT child.id, array_agg(parent.state) as parent_states + FROM queue_job job + JOIN LATERAL + json_array_elements_text( + job.dependencies::json->'reverse_depends_on' + ) child_deps ON true + JOIN queue_job child + ON child.graph_uuid = job.graph_uuid + AND child.uuid = child_deps + JOIN LATERAL + json_array_elements_text( + child.dependencies::json->'depends_on' + ) parent_deps ON true + JOIN queue_job parent + ON parent.graph_uuid = job.graph_uuid + AND parent.uuid = parent_deps + WHERE job.uuid = %s + GROUP BY child.id + ) jobs + WHERE + queue_job.id = jobs.id + AND %s = ALL(jobs.parent_states) + AND state = %s; + """ + + def enqueue_waiting(self): + sql = self._get_common_dependent_jobs_query() + self.env.cr.execute(sql, (PENDING, self.uuid, DONE, WAIT_DEPENDENCIES)) + self.env["queue.job"].invalidate_model(["state"]) + + def cancel_dependent_jobs(self): + sql = self._get_common_dependent_jobs_query() + self.env.cr.execute(sql, (CANCELLED, self.uuid, CANCELLED, WAIT_DEPENDENCIES)) + self.env["queue.job"].invalidate_model(["state"]) + + def store(self): + """Store the Job""" + job_model = self.env["queue.job"] + # The sentinel is used to prevent edition sensitive fields (such as + # method_name) from RPC methods. + edit_sentinel = job_model.EDIT_SENTINEL + + db_record = self.db_record() + if db_record: + db_record.with_context(_job_edit_sentinel=edit_sentinel).write( + self._store_values() + ) + else: + job_model.with_context(_job_edit_sentinel=edit_sentinel).sudo().create( + self._store_values(create=True) + ) + + def _store_values(self, create=False): + vals = { + "state": self.state, + "priority": self.priority, + "retry": self.retry, + "max_retries": self.max_retries, + "exc_name": self.exc_name, + "exc_message": self.exc_message, + "exc_info": self.exc_info, + "company_id": self.company_id, + "result": str(self.result) if self.result else False, + "date_enqueued": False, + "date_started": False, + "date_done": False, + "exec_time": False, + "date_cancelled": False, + "eta": False, + "identity_key": False, + "worker_pid": self.worker_pid, + "graph_uuid": self.graph_uuid, + } + + if self.date_enqueued: + vals["date_enqueued"] = self.date_enqueued + if self.date_started: + vals["date_started"] = self.date_started + if self.date_done: + vals["date_done"] = self.date_done + if self.exec_time: + vals["exec_time"] = self.exec_time + if self.date_cancelled: + vals["date_cancelled"] = self.date_cancelled + if self.eta: + vals["eta"] = self.eta + if self.identity_key: + vals["identity_key"] = self.identity_key + + dependencies = { + "depends_on": [parent.uuid for parent in self.depends_on], + "reverse_depends_on": [ + children.uuid for children in self.reverse_depends_on + ], + } + vals["dependencies"] = dependencies + + if create: + vals.update( + { + "user_id": self.env.uid, + "channel": self.channel, + # The following values must never be modified after the + # creation of the job + "uuid": self.uuid, + "name": self.description, + "func_string": self.func_string, + "date_created": self.date_created, + "model_name": self.recordset._name, + "method_name": self.method_name, + "job_function_id": self.job_config.job_function_id, + "channel_method_name": self.job_function_name, + "records": self.recordset, + "args": self.args, + "kwargs": self.kwargs, + } + ) + + vals_from_model = self._store_values_from_model() + # Sanitize values: make sure you cannot screw core values + vals_from_model = {k: v for k, v in vals_from_model.items() if k not in vals} + vals.update(vals_from_model) + return vals + + def _store_values_from_model(self): + vals = {} + value_handlers_candidates = ( + "_job_store_values_for_" + self.method_name, + "_job_store_values", + ) + for candidate in value_handlers_candidates: + handler = getattr(self.recordset, candidate, None) + if handler is not None: + vals = handler(self) + return vals + + @property + def func_string(self): + model = repr(self.recordset) + args = [repr(arg) for arg in self.args] + kwargs = [f"{key}={val!r}" for key, val in self.kwargs.items()] + all_args = ", ".join(args + kwargs) + return f"{model}.{self.method_name}({all_args})" + + def __eq__(self, other): + return self.uuid == other.uuid + + def __hash__(self): + return self.uuid.__hash__() + + def sorting_key(self): + return self.eta, self.priority, self.date_created, self.seq + + def __lt__(self, other): + if self.eta and not other.eta: + return True + elif not self.eta and other.eta: + return False + return self.sorting_key() < other.sorting_key() + + def db_record(self): + return self.db_records_from_uuids(self.env, [self.uuid]) + + @property + def func(self): + recordset = self.recordset.with_context(job_uuid=self.uuid) + return getattr(recordset, self.method_name) + + @property + def job_function_name(self): + func_model = self.env["queue.job.function"].sudo() + return func_model.job_function_name(self.recordset._name, self.method_name) + + @property + def identity_key(self): + if self._identity_key is None: + if self._identity_key_func: + self._identity_key = self._identity_key_func(self) + return self._identity_key + + @identity_key.setter + def identity_key(self, value): + if isinstance(value, str): + self._identity_key = value + self._identity_key_func = None + else: + # we'll compute the key on the fly when called + # from the function + self._identity_key = None + self._identity_key_func = value + + @property + def depends_on(self): + if not self._depends_on: + self._depends_on = Job.load_many(self.env, self.__depends_on_uuids) + return self._depends_on + + @property + def reverse_depends_on(self): + if not self._reverse_depends_on: + self._reverse_depends_on = Job.load_many( + self.env, self.__reverse_depends_on_uuids + ) + return set(self._reverse_depends_on) + + @property + def description(self): + if self._description: + return self._description + elif self.func.__doc__: + return self.func.__doc__.splitlines()[0].strip() + else: + return f"{self.model_name}.{self.func.__name__}" + + @property + def uuid(self): + """Job ID, this is an UUID""" + if self._uuid is None: + self._uuid = str(uuid.uuid4()) + return self._uuid + + @property + def model_name(self): + return self.recordset._name + + @property + def user_id(self): + return self.recordset.env.uid + + @property + def eta(self): + return self._eta + + @eta.setter + def eta(self, value): + if not value: + self._eta = None + elif isinstance(value, timedelta): + self._eta = datetime.now() + value + elif isinstance(value, int): + self._eta = datetime.now() + timedelta(seconds=value) + else: + self._eta = value + + @property + def channel(self): + return self._channel or self.job_config.channel + + @channel.setter + def channel(self, value): + self._channel = value + + @property + def exec_time(self): + if self.date_done and self.date_started: + return (self.date_done - self.date_started).total_seconds() + return None + + def set_pending(self, result=None, reset_retry=True): + if any(j.state != DONE for j in self.depends_on): + self.state = WAIT_DEPENDENCIES + else: + self.state = PENDING + self.date_enqueued = None + self.date_started = None + self.date_done = None + self.worker_pid = None + self.date_cancelled = None + if reset_retry: + self.retry = 0 + if result is not None: + self.result = result + + def set_enqueued(self): + self.state = ENQUEUED + self.date_enqueued = datetime.now() + self.date_started = None + self.worker_pid = None + + def set_started(self): + self.state = STARTED + self.date_started = datetime.now() + self.worker_pid = os.getpid() + + def set_done(self, result=None): + self.state = DONE + self.exc_name = None + self.exc_info = None + self.date_done = datetime.now() + if result is not None: + self.result = result + + def set_cancelled(self, result=None): + self.state = CANCELLED + self.date_cancelled = datetime.now() + if result is not None: + self.result = result + + def set_failed(self, **kw): + self.state = FAILED + for k, v in kw.items(): + if v is not None: + setattr(self, k, v) + + def __repr__(self): + return "" % (self.uuid, self.priority) + + def _get_retry_seconds(self, seconds=None): + retry_pattern = self.job_config.retry_pattern + if not seconds and retry_pattern: + # ordered from higher to lower count of retries + patt = sorted(retry_pattern.items(), key=lambda t: t[0]) + seconds = RETRY_INTERVAL + for retry_count, postpone_seconds in patt: + if self.retry >= retry_count: + seconds = postpone_seconds + else: + break + elif not seconds: + seconds = RETRY_INTERVAL + if isinstance(seconds, (list | tuple)): + seconds = randint(seconds[0], seconds[1]) + return seconds + + def postpone(self, result=None, seconds=None): + """Postpone the job + + Write an estimated time arrival to n seconds + later than now. Used when an retryable exception + want to retry a job later. + """ + eta_seconds = self._get_retry_seconds(seconds) + self.eta = timedelta(seconds=eta_seconds) + self.exc_name = None + self.exc_info = None + if result is not None: + self.result = result + + def related_action(self): + record = self.db_record() + if not self.job_config.related_action_enable: + return None + + funcname = self.job_config.related_action_func_name + if not funcname: + funcname = record._default_related_action + if not isinstance(funcname, str): + raise ValueError( + "related_action must be the name of the " + "method on queue.job as string" + ) + action = getattr(record, funcname) + action_kwargs = self.job_config.related_action_kwargs + return action(**action_kwargs) + + +def _is_model_method(func): + return inspect.ismethod(func) and isinstance( + func.__self__.__class__, odoo.models.MetaModel + ) diff --git a/queue_job/jobrunner/__init__.py b/queue_job/jobrunner/__init__.py new file mode 100644 index 0000000000..e2561b0e74 --- /dev/null +++ b/queue_job/jobrunner/__init__.py @@ -0,0 +1,163 @@ +# Copyright (c) 2015-2016 ACSONE SA/NV () +# Copyright 2016 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +import logging +from threading import Thread +import time + +from odoo.service import server +from odoo.tools import config + +try: + from odoo.addons.server_environment import serv_config + + if serv_config.has_section("queue_job"): + queue_job_config = serv_config["queue_job"] + else: + queue_job_config = {} +except ImportError: + queue_job_config = config.misc.get("queue_job", {}) + + +from .runner import QueueJobRunner, _channels + +_logger = logging.getLogger(__name__) + +START_DELAY = 5 + + +# Here we monkey patch the Odoo server to start the job runner thread +# in the main server process (and not in forked workers). This is +# very easy to deploy as we don't need another startup script. + + +class QueueJobRunnerThread(Thread): + def __init__(self): + Thread.__init__(self) + self.daemon = True + self.runner = QueueJobRunner.from_environ_or_config() + + def run(self): + # sleep a bit to let the workers start at ease + time.sleep(START_DELAY) + self.runner.run() + + def stop(self): + self.runner.stop() + + +class WorkerJobRunner(server.Worker): + """Jobrunner workers""" + + def __init__(self, multi): + super().__init__(multi) + self.watchdog_timeout = None + self.runner = QueueJobRunner.from_environ_or_config() + self._recover = False + + def sleep(self): + pass + + def signal_handler(self, sig, frame): # pylint: disable=missing-return + _logger.debug("WorkerJobRunner (%s) received signal %s", self.pid, sig) + super().signal_handler(sig, frame) + self.runner.stop() + + def process_work(self): + if self._recover: + _logger.info("WorkerJobRunner (%s) runner is reinitialized", self.pid) + self.runner = QueueJobRunner.from_environ_or_config() + self._recover = False + _logger.debug("WorkerJobRunner (%s) starting up", self.pid) + time.sleep(START_DELAY) + self.runner.run() + + def signal_time_expired_handler(self, n, stack): + _logger.info( + "Worker (%d) CPU time limit (%s) reached.Stop gracefully and recover", + self.pid, + config["limit_time_cpu"], + ) + self._recover = True + self.runner.stop() + + +runner_thread = None + + +def _is_runner_enabled(): + return not _channels().strip().startswith("root:0") + + +def _start_runner_thread(server_type): + global runner_thread + if not config["stop_after_init"]: + if _is_runner_enabled(): + _logger.info("starting jobrunner thread (in %s)", server_type) + runner_thread = QueueJobRunnerThread() + runner_thread.start() + else: + _logger.info( + "jobrunner thread (in %s) NOT started, " + "because the root channel's capacity is set to 0", + server_type, + ) + + +orig_prefork__init__ = server.PreforkServer.__init__ +orig_prefork_process_spawn = server.PreforkServer.process_spawn +orig_prefork_worker_pop = server.PreforkServer.worker_pop +orig_threaded_start = server.ThreadedServer.start +orig_threaded_stop = server.ThreadedServer.stop + + +def prefork__init__(server, app): + res = orig_prefork__init__(server, app) + server.jobrunner = {} + return res + + +def prefork_process_spawn(server): + orig_prefork_process_spawn(server) + if not hasattr(server, "jobrunner"): + # if 'queue_job' is not in server wide modules, PreforkServer is + # not initialized with a 'jobrunner' attribute, skip this + return + if not server.jobrunner and _is_runner_enabled(): + server.worker_spawn(WorkerJobRunner, server.jobrunner) + + +def prefork_worker_pop(server, pid): + res = orig_prefork_worker_pop(server, pid) + if not hasattr(server, "jobrunner"): + # if 'queue_job' is not in server wide modules, PreforkServer is + # not initialized with a 'jobrunner' attribute, skip this + return res + if pid in server.jobrunner: + server.jobrunner.pop(pid) + return res + + +def threaded_start(server, *args, **kwargs): + res = orig_threaded_start(server, *args, **kwargs) + _start_runner_thread("threaded server") + return res + + +def threaded_stop(server): + global runner_thread + if runner_thread: + runner_thread.stop() + res = orig_threaded_stop(server) + if runner_thread: + runner_thread.join() + runner_thread = None + return res + + +server.PreforkServer.__init__ = prefork__init__ +server.PreforkServer.process_spawn = prefork_process_spawn +server.PreforkServer.worker_pop = prefork_worker_pop +server.ThreadedServer.start = threaded_start +server.ThreadedServer.stop = threaded_stop diff --git a/queue_job/jobrunner/__main__.py b/queue_job/jobrunner/__main__.py new file mode 100644 index 0000000000..8f9628c5db --- /dev/null +++ b/queue_job/jobrunner/__main__.py @@ -0,0 +1,13 @@ +import odoo + +from .runner import QueueJobRunner + + +def main(): + odoo.tools.config.parse_config() + runner = QueueJobRunner.from_environ_or_config() + runner.run() + + +if __name__ == "__main__": + main() diff --git a/queue_job/jobrunner/channels.py b/queue_job/jobrunner/channels.py new file mode 100644 index 0000000000..e0d6f9acbd --- /dev/null +++ b/queue_job/jobrunner/channels.py @@ -0,0 +1,1079 @@ +# Copyright (c) 2015-2016 ACSONE SA/NV () +# Copyright 2015-2016 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) +import logging +from functools import total_ordering +from heapq import heappop, heappush +from weakref import WeakValueDictionary + +from ..exception import ChannelNotFound +from ..job import CANCELLED, DONE, ENQUEUED, FAILED, PENDING, STARTED, WAIT_DEPENDENCIES + +NOT_DONE = (WAIT_DEPENDENCIES, PENDING, ENQUEUED, STARTED, FAILED) + +_logger = logging.getLogger(__name__) + + +class PriorityQueue: + """A priority queue that supports removing arbitrary objects. + + Adding an object already in the queue is a no op. + Popping an empty queue returns None. + + >>> q = PriorityQueue() + >>> q.add(2) + >>> q.add(3) + >>> q.add(3) + >>> q.add(1) + >>> q[0] + 1 + >>> len(q) + 3 + >>> q.pop() + 1 + >>> q.remove(2) + >>> len(q) + 1 + >>> q[0] + 3 + >>> q.pop() + 3 + >>> q.pop() + >>> q.add(2) + >>> q.remove(2) + >>> q.add(2) + >>> q.pop() + 2 + """ + + def __init__(self): + self._heap = [] + self._known = set() # all objects in the heap (including removed) + self._removed = set() # all objects that have been removed + + def __len__(self): + return len(self._known) - len(self._removed) + + def __getitem__(self, i): + if i != 0: + raise IndexError() + while True: + if not self._heap: + raise IndexError() + o = self._heap[0] + if o in self._removed: + o2 = heappop(self._heap) + assert o2 == o + self._removed.remove(o) + self._known.remove(o) + else: + return o + + def __contains__(self, o): + return o in self._known and o not in self._removed + + def add(self, o): + if o is None: + raise ValueError() + if o in self._removed: + self._removed.remove(o) + if o in self._known: + return + self._known.add(o) + heappush(self._heap, o) + + def remove(self, o): + if o is None: + raise ValueError() + if o not in self._known: + return + if o not in self._removed: + self._removed.add(o) + + def pop(self): + while True: + try: + o = heappop(self._heap) + except IndexError: + # queue is empty + return None + self._known.remove(o) + if o in self._removed: + self._removed.remove(o) + else: + return o + + +class SafeSet(set): + """A set that does not raise KeyError when removing non-existent items. + + >>> s = SafeSet() + >>> s.remove(1) + >>> len(s) + 0 + >>> s.remove(1) + """ + + def remove(self, o): + # pylint: disable=missing-return,except-pass + try: + super().remove(o) + except KeyError: + pass + + +@total_ordering +class ChannelJob: + """A channel job is attached to a channel and holds the properties of a + job that are necessary to prioritise them. + + Channel jobs are comparable according to the following rules: + * jobs with an eta come before all other jobs + * then jobs with a smaller eta come first + * then jobs with a smaller priority come first + * then jobs with a smaller creation time come first + * then jobs with a smaller sequence come first + + Here are some examples. + + j1 comes before j2 because it has an earlier date_created + + >>> j1 = ChannelJob(None, None, 1, + ... seq=0, date_created=1, priority=9, eta=None) + >>> j1 + + >>> j2 = ChannelJob(None, None, 2, + ... seq=0, date_created=2, priority=9, eta=None) + >>> j1 < j2 + True + + j3 comes first because it has lower priority, + despite having a creation date after j1 and j2 + + >>> j3 = ChannelJob(None, None, 3, + ... seq=0, date_created=3, priority=2, eta=None) + >>> j3 < j1 + True + + j4 and j5 comes even before j3, because they have an eta + + >>> j4 = ChannelJob(None, None, 4, + ... seq=0, date_created=4, priority=9, eta=9) + >>> j5 = ChannelJob(None, None, 5, + ... seq=0, date_created=5, priority=9, eta=9) + >>> j4 < j5 < j3 + True + + j6 has same date_created and priority as j5 but a smaller eta + + >>> j6 = ChannelJob(None, None, 6, + ... seq=0, date_created=5, priority=9, eta=2) + >>> j6 < j4 < j5 + True + + Here is the complete suite: + + >>> j6 < j4 < j5 < j3 < j1 < j2 + True + + j0 has the same properties as j1 but they are not considered + equal as they are different instances + + >>> j0 = ChannelJob(None, None, 1, + ... seq=0, date_created=1, priority=9, eta=None) + >>> j0 == j1 + False + >>> j0 == j0 + True + + Comparison excluding eta: + + >>> j1.sorting_key_ignoring_eta() < j2.sorting_key_ignoring_eta() + True + + """ + + def __init__(self, db_name, channel, uuid, seq, date_created, priority, eta): + self.db_name = db_name + self.channel = channel + self.uuid = uuid + self.seq = seq + self.date_created = date_created + self.priority = priority + self.eta = eta + + def __repr__(self): + return f"" + + def __eq__(self, other): + return id(self) == id(other) + + def __hash__(self): + return id(self) + + def sorting_key(self): + return self.eta, self.priority, self.date_created, self.seq + + def sorting_key_ignoring_eta(self): + return self.priority, self.date_created, self.seq + + def __lt__(self, other): + if self.eta and not other.eta: + return True + elif not self.eta and other.eta: + return False + return self.sorting_key() < other.sorting_key() + + +class ChannelQueue: + """A channel queue is a priority queue for jobs. + + Jobs with an eta are set aside until their eta is past due, at + which point they start competing normally with other jobs. + + >>> q = ChannelQueue() + >>> j1 = ChannelJob(None, None, 1, + ... seq=0, date_created=1, priority=1, eta=10) + >>> j2 = ChannelJob(None, None, 2, + ... seq=0, date_created=2, priority=1, eta=None) + >>> j3 = ChannelJob(None, None, 3, + ... seq=0, date_created=3, priority=1, eta=None) + >>> q.add(j1) + >>> q.add(j2) + >>> q.add(j3) + + Wakeup time is the eta of job 1. + + >>> q.get_wakeup_time() + 10 + + We have not reached the eta of job 1, so we get job 2. + + >>> q.pop(now=1) + + + Wakeup time is still the eta of job 1, and we get job 1 when we are past + it's eta. + + >>> q.get_wakeup_time() + 10 + >>> q.pop(now=11) + + + Now there is no wakeup time anymore, because no job have an eta. + + >>> q.get_wakeup_time() + 0 + >>> q.pop(now=12) + + >>> q.get_wakeup_time() + 0 + >>> q.pop(now=13) + + Observe that job with past eta still run after jobs with higher priority. + + >>> j4 = ChannelJob(None, None, 4, + ... seq=0, date_created=4, priority=10, eta=20) + >>> j5 = ChannelJob(None, None, 5, + ... seq=0, date_created=5, priority=1, eta=None) + >>> q.add(j4) + >>> q.add(j5) + >>> q.get_wakeup_time() + 20 + >>> q.pop(21) + + >>> q.get_wakeup_time() + 0 + >>> q.pop(22) + + + Test a sequential queue. + + >>> sq = ChannelQueue(sequential=True) + >>> j6 = ChannelJob(None, None, 6, + ... seq=0, date_created=6, priority=1, eta=None) + >>> j7 = ChannelJob(None, None, 7, + ... seq=0, date_created=7, priority=1, eta=20) + >>> j8 = ChannelJob(None, None, 8, + ... seq=0, date_created=8, priority=1, eta=None) + >>> sq.add(j6) + >>> sq.add(j7) + >>> sq.add(j8) + >>> sq.pop(10) + + >>> sq.pop(15) + >>> sq.pop(20) + + >>> sq.pop(30) + + """ + + def __init__(self, sequential=False): + self._queue = PriorityQueue() + self._eta_queue = PriorityQueue() + self.sequential = sequential + + def __len__(self): + return len(self._eta_queue) + len(self._queue) + + def __contains__(self, o): + return o in self._eta_queue or o in self._queue + + def add(self, job): + if job.eta: + self._eta_queue.add(job) + else: + self._queue.add(job) + + def remove(self, job): + self._eta_queue.remove(job) + self._queue.remove(job) + + def pop(self, now): + while self._eta_queue and self._eta_queue[0].eta <= now: + eta_job = self._eta_queue.pop() + eta_job.eta = None + self._queue.add(eta_job) + if self.sequential and self._eta_queue and self._queue: + eta_job = self._eta_queue[0] + job = self._queue[0] + + if eta_job.sorting_key_ignoring_eta() < job.sorting_key_ignoring_eta(): + # eta ignored, the job with eta has higher priority + # than the job without eta; since it's a sequential + # queue we wait until eta + return None + return self._queue.pop() + + def get_wakeup_time(self, wakeup_time=0): + if self._eta_queue: + if not wakeup_time: + wakeup_time = self._eta_queue[0].eta + else: + wakeup_time = min(wakeup_time, self._eta_queue[0].eta) + return wakeup_time + + +class Channel: + """A channel for jobs, with a maximum capacity. + + When jobs are created by queue_job modules, they may be associated + to a job channel. Jobs with no channel are inserted into the root channel. + + Job channels are joined in a hierarchy down to the root channel. + When a job channel has available capacity, jobs are dequeued, marked + as running in the channel and are inserted into the queue of the + parent channel where they wait for available capacity and so on. + + Job channels can be visualized as water channels with a given flow + limit (= capacity). Channels are joined together in a downstream channel + and the flow limit of the downstream channel limits upstream channels.:: + + ---------------------+ + | + | + Ch. A C:4,Q:12,R:4 +----------------------- + + ---------------------+ Ch. root C:5,Q:0,R:4 + | + ---------------------+ + Ch. B C:1,Q:0,R:0 + ---------------------+----------------------- + + The above diagram illustrates two channels joining in the root channel. + The root channel has a capacity of 5, and 4 running jobs coming from + Channel A. Channel A has a capacity of 4, all in use (passed down to the + root channel), and 12 jobs enqueued. Channel B has a capacity of 1, + none in use. This means that whenever a new job comes in channel B, + there will be available room for it to run in the root channel. + + Note that from the point of view of a channel, 'running' means enqueued + in the downstream channel. Only jobs marked running in the root channel + are actually sent to Odoo for execution. + + Should a downstream channel have less capacity than its upstream channels, + jobs going downstream will be enqueued in the downstream channel, + and compete normally according to their properties (priority, etc). + + Using this technique, it is possible to enforce sequence in a channel + with a capacity of 1. It is also possible to dedicate a channel with a + limited capacity for application-autocreated subchannels + without risking to overflow the system. + """ + + def __init__(self, name, parent, capacity=None, sequential=False, throttle=0): + self.name = name + self.parent = parent + if self.parent: + self.parent.children[name] = self + self.children = {} + self._queue = ChannelQueue() + self._running = SafeSet() + self._failed = SafeSet() + self._pause_until = 0 # utc seconds since the epoch + self.capacity = capacity + self.throttle = throttle # seconds + self.sequential = sequential + + @property + def sequential(self): + return self._queue.sequential + + @sequential.setter + def sequential(self, val): + self._queue.sequential = val + + def configure(self, config): + """Configure a channel from a dictionary. + + Supported keys are: + + * capacity + * sequential + * throttle + """ + assert self.fullname.endswith(config["name"]) + self.capacity = config.get("capacity", None) + self.sequential = bool(config.get("sequential", False)) + self.throttle = int(config.get("throttle", 0)) + if self.sequential and self.capacity != 1: + raise ValueError("A sequential channel must have a capacity of 1") + + @property + def fullname(self): + """The full name of the channel, in dot separated notation.""" + if self.parent: + return self.parent.fullname + "." + self.name + else: + return self.name + + def get_subchannel_by_name(self, subchannel_name): + return self.children.get(subchannel_name) + + def __str__(self): + capacity = "∞" if self.capacity is None else str(self.capacity) + return "%s(C:%s,Q:%d,R:%d,F:%d)" % ( + self.fullname, + capacity, + len(self._queue), + len(self._running), + len(self._failed), + ) + + def remove(self, job): + """Remove a job from the channel.""" + self._queue.remove(job) + self._running.remove(job) + self._failed.remove(job) + if self.parent: + self.parent.remove(job) + + def set_done(self, job): + """Mark a job as done. + + This removes it from the channel queue. + """ + self.remove(job) + _logger.debug("job %s marked done in channel %s", job.uuid, self) + + def set_pending(self, job): + """Mark a job as pending. + + This puts the job in the channel queue and remove it + from parent channels queues. + """ + if job not in self._queue: + self._queue.add(job) + self._running.remove(job) + self._failed.remove(job) + if self.parent: + self.parent.remove(job) + _logger.debug("job %s marked pending in channel %s", job.uuid, self) + + def set_running(self, job): + """Mark a job as running. + + This also marks the job as running in parent channels. + """ + if job not in self._running: + self._queue.remove(job) + self._running.add(job) + self._failed.remove(job) + if self.parent: + self.parent.set_running(job) + _logger.debug("job %s marked running in channel %s", job.uuid, self) + + def set_failed(self, job): + """Mark the job as failed.""" + if job not in self._failed: + self._queue.remove(job) + self._running.remove(job) + self._failed.add(job) + if self.parent: + self.parent.remove(job) + _logger.debug("job %s marked failed in channel %s", job.uuid, self) + + def has_capacity(self): + if self.sequential and self._failed: + # a sequential queue blocks on failed jobs + return False + if not self.capacity: + # unlimited capacity + return True + return len(self._running) < self.capacity + + def get_jobs_to_run(self, now): + """Get jobs that are ready to run in channel. + + This works by enqueuing jobs that are ready to run in children + channels, then yielding jobs from the channel queue until + ``capacity`` jobs are marked running in the channel. + + If the ``throttle`` option is set on the channel, then it yields + no job until at least throttle seconds have elapsed since the previous + yield. + + :param now: the current datetime in seconds + + :return: iterator of + :class:`odoo.addons.queue_job.jobrunner.ChannelJob` + """ + # enqueue jobs of children channels + for child in self.children.values(): + for job in child.get_jobs_to_run(now): + self._queue.add(job) + # is this channel paused? + if self.throttle and self._pause_until: + if now < self._pause_until: + if self.has_capacity(): + _logger.debug( + "channel %s paused until %s because " + "of throttle delay between jobs", + self, + self._pause_until, + ) + return + else: + # unpause, this is important to avoid perpetual wakeup + # while the channel is at full capacity + self._pause_until = 0 + _logger.debug("channel %s unpaused at %s", self, now) + # yield jobs that are ready to run, while we have capacity + while self.has_capacity(): + job = self._queue.pop(now) + if not job: + return + self._running.add(job) + _logger.debug("job %s marked running in channel %s", job.uuid, self) + yield job + if self.throttle: + self._pause_until = now + self.throttle + _logger.debug("pausing channel %s until %s", self, self._pause_until) + return + + def get_wakeup_time(self, wakeup_time=0): + if not self.has_capacity(): + # this channel is full, do not request timed wakeup, as + # a notification will wakeup the runner when a job finishes + return wakeup_time + if self._pause_until: + # this channel is paused, request wakeup at the end of the pause + if not wakeup_time: + wakeup_time = self._pause_until + else: + wakeup_time = min(wakeup_time, self._pause_until) + # since this channel is paused, no need to look at the + # wakeup time of children nor eta jobs, as such jobs would not + # run anyway because they would end up in this paused channel + return wakeup_time + wakeup_time = self._queue.get_wakeup_time(wakeup_time) + for child in self.children.values(): + wakeup_time = child.get_wakeup_time(wakeup_time) + return wakeup_time + + +def split_strip(s, sep, maxsplit=-1): + """Split string and strip each component. + + >>> split_strip("foo: bar baz\\n: fred:", ":") + ['foo', 'bar baz', 'fred', ''] + """ + return [x.strip() for x in s.split(sep, maxsplit)] + + +class ChannelManager: + """High level interface for channels + + This class handles: + + * configuration of channels + * high level api to create and remove jobs (notify, remove_job, remove_db) + * get jobs to run + + Here is how the runner will use it. + + Let's create a channel manager and configure it. + + >>> from pprint import pprint as pp + >>> cm = ChannelManager() + >>> cm.simple_configure('root:4,A:4,B:1') + >>> db = 'db' + + Add a few jobs in channel A with priority 10 + + >>> cm.notify(db, 'A', 'A1', 1, 0, 10, None, 'pending') + >>> cm.notify(db, 'A', 'A2', 2, 0, 10, None, 'pending') + >>> cm.notify(db, 'A', 'A3', 3, 0, 10, None, 'pending') + >>> cm.notify(db, 'A', 'A4', 4, 0, 10, None, 'pending') + >>> cm.notify(db, 'A', 'A5', 5, 0, 10, None, 'pending') + >>> cm.notify(db, 'A', 'A6', 6, 0, 10, None, 'pending') + + Add a few jobs in channel B with priority 5 + + >>> cm.notify(db, 'B', 'B1', 1, 0, 5, None, 'pending') + >>> cm.notify(db, 'B', 'B2', 2, 0, 5, None, 'pending') + + We must now run one job from queue B which has a capacity of 1 + and 3 jobs from queue A so the root channel capacity of 4 is filled. + + >>> pp(list(cm.get_jobs_to_run(now=100))) + [, , , ] + + Job A2 is done. Next job to run is A5, even if we have + higher priority job in channel B, because channel B has a capacity of 1. + + >>> cm.notify(db, 'A', 'A2', 2, 0, 10, None, 'done') + >>> pp(list(cm.get_jobs_to_run(now=100))) + [] + + Job B1 is done. Next job to run is B2 because it has higher priority. + + >>> cm.notify(db, 'B', 'B1', 1, 0, 5, None, 'done') + >>> pp(list(cm.get_jobs_to_run(now=100))) + [] + + Let's say A1 is done and A6 gets a higher priority. A6 will run next. + + >>> cm.notify(db, 'A', 'A1', 1, 0, 10, None, 'done') + >>> cm.notify(db, 'A', 'A6', 6, 0, 5, None, 'pending') + >>> pp(list(cm.get_jobs_to_run(now=100))) + [] + + Let's test the throttling mechanism. Configure a 2 seconds delay + on channel A, end enqueue two jobs. + + >>> cm = ChannelManager() + >>> cm.simple_configure('root:4,A:4:throttle=2') + >>> cm.notify(db, 'A', 'A1', 1, 0, 10, None, 'pending') + >>> cm.notify(db, 'A', 'A2', 2, 0, 10, None, 'pending') + + We have only one job to run, because of the throttle. + + >>> pp(list(cm.get_jobs_to_run(now=100))) + [] + >>> cm.get_wakeup_time() + 102 + + We have no job to run, because of the throttle. + + >>> pp(list(cm.get_jobs_to_run(now=101))) + [] + >>> cm.get_wakeup_time() + 102 + + 2 seconds later, we can run the other job (even though the first one + is still running, because we have enough capacity). + + >>> pp(list(cm.get_jobs_to_run(now=102))) + [] + >>> cm.get_wakeup_time() + 104 + + Let's test throttling in combination with a queue reaching full capacity. + + >>> cm = ChannelManager() + >>> cm.simple_configure('root:4,T:2:throttle=2') + >>> cm.notify(db, 'T', 'T1', 1, 0, 10, None, 'pending') + >>> cm.notify(db, 'T', 'T2', 2, 0, 10, None, 'pending') + >>> cm.notify(db, 'T', 'T3', 3, 0, 10, None, 'pending') + + >>> pp(list(cm.get_jobs_to_run(now=100))) + [] + >>> pp(list(cm.get_jobs_to_run(now=102))) + [] + + Channel is now full, so no job to run even though throttling + delay is over. + + >>> pp(list(cm.get_jobs_to_run(now=103))) + [] + >>> cm.get_wakeup_time() # no wakeup time, since queue is full + 0 + >>> pp(list(cm.get_jobs_to_run(now=104))) + [] + >>> cm.get_wakeup_time() # queue is still full + 0 + + >>> cm.notify(db, 'T', 'T1', 1, 0, 10, None, 'done') + >>> pp(list(cm.get_jobs_to_run(now=105))) + [] + >>> cm.get_wakeup_time() # queue is full + 0 + >>> cm.notify(db, 'T', 'T2', 1, 0, 10, None, 'done') + >>> cm.get_wakeup_time() + 107 + + Test wakeup time behaviour in presence of eta. + + >>> cm = ChannelManager() + >>> cm.simple_configure('root:4,E:1') + >>> cm.notify(db, 'E', 'E1', 1, 0, 10, None, 'pending') + >>> cm.notify(db, 'E', 'E2', 2, 0, 10, None, 'pending') + >>> cm.notify(db, 'E', 'E3', 3, 0, 10, None, 'pending') + + >>> pp(list(cm.get_jobs_to_run(now=100))) + [] + >>> pp(list(cm.get_jobs_to_run(now=101))) + [] + >>> cm.notify(db, 'E', 'E1', 1, 0, 10, 105, 'pending') + >>> cm.get_wakeup_time() # wakeup at eta + 105 + >>> pp(list(cm.get_jobs_to_run(now=102))) # but there is capacity + [] + >>> pp(list(cm.get_jobs_to_run(now=106))) # no capacity anymore + [] + >>> cm.get_wakeup_time() # no timed wakeup because no capacity + 0 + >>> cm.notify(db, 'E', 'E2', 1, 0, 10, None, 'done') + >>> cm.get_wakeup_time() + 105 + >>> pp(list(cm.get_jobs_to_run(now=107))) # no capacity anymore + [] + >>> cm.get_wakeup_time() + 0 + + Test wakeup time behaviour in a sequential queue. + + >>> cm = ChannelManager() + >>> cm.simple_configure('root:4,S:1:sequential') + >>> cm.notify(db, 'S', 'S1', 1, 0, 10, None, 'pending') + >>> cm.notify(db, 'S', 'S2', 2, 0, 10, None, 'pending') + >>> cm.notify(db, 'S', 'S3', 3, 0, 10, None, 'pending') + + >>> pp(list(cm.get_jobs_to_run(now=100))) + [] + >>> cm.notify(db, 'S', 'S1', 1, 0, 10, None, 'failed') + >>> pp(list(cm.get_jobs_to_run(now=101))) + [] + >>> cm.notify(db, 'S', 'S2', 2, 0, 10, 105, 'pending') + >>> pp(list(cm.get_jobs_to_run(now=102))) + [] + + No wakeup time because due to eta, because the sequential queue + is waiting for a failed job. + + >>> cm.get_wakeup_time() + 0 + >>> cm.notify(db, 'S', 'S1', 1, 0, 10, None, 'pending') + >>> cm.get_wakeup_time() + 105 + >>> pp(list(cm.get_jobs_to_run(now=102))) + [] + >>> pp(list(cm.get_jobs_to_run(now=103))) + [] + >>> cm.notify(db, 'S', 'S1', 1, 0, 10, None, 'done') + + At this stage, we have S2 with an eta of 105 and since the + queue is sequential, we wait for it. + + >>> pp(list(cm.get_jobs_to_run(now=103))) + [] + >>> pp(list(cm.get_jobs_to_run(now=105))) + [] + >>> cm.notify(db, 'S', 'S2', 2, 0, 10, 105, 'done') + >>> pp(list(cm.get_jobs_to_run(now=105))) + [] + >>> cm.notify(db, 'S', 'S3', 3, 0, 10, None, 'done') + >>> pp(list(cm.get_jobs_to_run(now=105))) + [] + """ + + def __init__(self): + self._jobs_by_uuid = WeakValueDictionary() + self._root_channel = Channel(name="root", parent=None, capacity=1) + self._channels_by_name = WeakValueDictionary(root=self._root_channel) + + @classmethod + def parse_simple_config(cls, config_string): + """Parse a simple channels configuration string. + + The general form is as follow: + channel(.subchannel)*(:capacity(:key(=value)?)*)? [, ...] + + If capacity is absent, it defaults to 1. + If a key is present without value, it gets True as value. + When declaring subchannels, the root channel may be omitted + (ie sub:4 is the same as root.sub:4). + + Returns a list of channel configuration dictionaries. + + >>> from pprint import pprint as pp + >>> pp(ChannelManager.parse_simple_config('root:4')) + [{'capacity': 4, 'name': 'root'}] + >>> pp(ChannelManager.parse_simple_config('root:4,root.sub:2')) + [{'capacity': 4, 'name': 'root'}, {'capacity': 2, 'name': 'root.sub'}] + >>> pp(ChannelManager.parse_simple_config('root:4,root.sub:2:' + ... 'sequential:k=v')) + [{'capacity': 4, 'name': 'root'}, + {'capacity': 2, 'k': 'v', 'name': 'root.sub', 'sequential': True}] + >>> pp(ChannelManager.parse_simple_config('root')) + [{'capacity': 1, 'name': 'root'}] + >>> pp(ChannelManager.parse_simple_config('sub:2')) + [{'capacity': 2, 'name': 'sub'}] + + It ignores whitespace around values, and drops empty entries which + would be generated by trailing commas, or commented lines on the Odoo + config file. + + >>> pp(ChannelManager.parse_simple_config(''' + ... root : 4, + ... , + ... foo bar:1: k=va lue, + ... ''')) + [{'capacity': 4, 'name': 'root'}, + {'capacity': 1, 'k': 'va lue', 'name': 'foo bar'}] + + It's also possible to replace commas with line breaks, which is more + readable if the channel configuration comes from the odoo config file. + + >>> pp(ChannelManager.parse_simple_config(''' + ... root : 4 + ... foo bar:1: k=va lue + ... baz + ... ''')) + [{'capacity': 4, 'name': 'root'}, + {'capacity': 1, 'k': 'va lue', 'name': 'foo bar'}, + {'capacity': 1, 'name': 'baz'}] + """ + res = [] + config_string = config_string.replace("\n", ",") + for channel_config_string in split_strip(config_string, ","): + if not channel_config_string: + # ignore empty entries (commented lines, trailing commas) + continue + config = {} + config_items = split_strip(channel_config_string, ":") + name = config_items[0] + if not name: + raise ValueError( + f"Invalid channel config {config_string}: missing channel name" + ) + config["name"] = name + if len(config_items) > 1: + capacity = config_items[1] + try: + config["capacity"] = int(capacity) + except Exception as ex: + raise ValueError( + f"Invalid channel config {config_string}: " + f"invalid capacity {capacity}" + ) from ex + for config_item in config_items[2:]: + kv = split_strip(config_item, "=") + if len(kv) == 1: + k, v = kv[0], True + elif len(kv) == 2: + k, v = kv + else: + raise ValueError( + f"Invalid channel config {config_string}: " + f"incorrect config item {config_item}" + ) + if k in config: + raise ValueError( + f"Invalid channel config {config_string}: " + f"duplicate key {k}" + ) + config[k] = v + else: + config["capacity"] = 1 + res.append(config) + return res + + def simple_configure(self, config_string): + """Configure the channel manager from a simple configuration string + + >>> cm = ChannelManager() + >>> c = cm.get_channel_by_name('root') + >>> c.capacity + 1 + >>> cm.simple_configure('root:4,autosub.sub:2,seq:1:sequential') + >>> cm.get_channel_by_name('root').capacity + 4 + >>> cm.get_channel_by_name('root').sequential + False + >>> cm.get_channel_by_name('root.autosub').capacity + >>> cm.get_channel_by_name('root.autosub.sub').capacity + 2 + >>> cm.get_channel_by_name('root.autosub.sub').sequential + False + >>> cm.get_channel_by_name('autosub.sub').capacity + 2 + >>> cm.get_channel_by_name('seq').capacity + 1 + >>> cm.get_channel_by_name('seq').sequential + True + """ + for config in ChannelManager.parse_simple_config(config_string): + self.get_channel_from_config(config) + + def get_channel_from_config(self, config): + """Return a Channel object from a parsed configuration. + + If the channel does not exist it is created. + The configuration is applied on the channel before returning it. + If some of the parent channels are missing when creating a subchannel, + the parent channels are auto created with an infinite capacity + (except for the root channel, which defaults to a capacity of 1 + when not configured explicity). + """ + channel = self.get_channel_by_name(config["name"], autocreate=True) + channel.configure(config) + _logger.info("Configured channel: %s", channel) + return channel + + def get_channel_by_name( + self, channel_name, autocreate=False, parent_fallback=False + ): + """Return a Channel object by its name. + + If it does not exist and autocreate is True, it is created + with a default configuration and inserted in the Channels structure. + If autocreate is False and the channel does not exist, an exception + is raised. + + >>> cm = ChannelManager() + >>> c = cm.get_channel_by_name('root', autocreate=False) + >>> c.name + 'root' + >>> c.fullname + 'root' + >>> c = cm.get_channel_by_name('root.sub', autocreate=True) + >>> c.name + 'sub' + >>> c.fullname + 'root.sub' + >>> c = cm.get_channel_by_name('sub', autocreate=True) + >>> c.name + 'sub' + >>> c.fullname + 'root.sub' + >>> c = cm.get_channel_by_name('autosub.sub', autocreate=True) + >>> c.name + 'sub' + >>> c.fullname + 'root.autosub.sub' + >>> c = cm.get_channel_by_name(None) + >>> c.fullname + 'root' + >>> c = cm.get_channel_by_name('root.sub') + >>> c.fullname + 'root.sub' + >>> c = cm.get_channel_by_name('sub') + >>> c.fullname + 'root.sub' + >>> c = cm.get_channel_by_name('root.sub.not.configured', parent_fallback=True) + >>> c.fullname + 'root.sub.sub.not.configured' + """ + if not channel_name or channel_name == self._root_channel.name: + return self._root_channel + if not channel_name.startswith(self._root_channel.name + "."): + channel_name = self._root_channel.name + "." + channel_name + if channel_name in self._channels_by_name: + return self._channels_by_name[channel_name] + if not autocreate and not parent_fallback: + raise ChannelNotFound(f"Channel {channel_name} not found") + parent = self._root_channel + if parent_fallback: + # Look for first direct parent w/ config. + # Eg: `root.edi.foo.baz` will falback on `root.edi.foo` + # or `root.edi` or `root` in sequence + parent_name = channel_name + while True: + parent_name = parent_name.rsplit(".", 1)[:-1][0] + if parent_name == self._root_channel.name: + break + if parent_name in self._channels_by_name: + parent = self._channels_by_name[parent_name] + _logger.debug( + "%s has no specific configuration: using %s", + channel_name, + parent_name, + ) + break + for subchannel_name in channel_name.split(".")[1:]: + subchannel = parent.get_subchannel_by_name(subchannel_name) + if not subchannel: + subchannel = Channel(subchannel_name, parent, capacity=None) + self._channels_by_name[subchannel.fullname] = subchannel + parent = subchannel + return parent + + def notify( + self, db_name, channel_name, uuid, seq, date_created, priority, eta, state + ): + channel = self.get_channel_by_name(channel_name, parent_fallback=True) + job = self._jobs_by_uuid.get(uuid) + if job: + # db_name is invariant + assert job.db_name == db_name + # date_created is invariant + assert job.date_created == date_created + # if one of the job properties that influence + # scheduling order has changed, we remove the job + # from the queues and create a new job object + if ( + seq != job.seq + or priority != job.priority + or eta != job.eta + or channel != job.channel + ): + _logger.debug("job %s properties changed, rescheduling it", uuid) + self.remove_job(uuid) + job = None + if not job: + job = ChannelJob(db_name, channel, uuid, seq, date_created, priority, eta) + self._jobs_by_uuid[uuid] = job + # state transitions + if not state or state in (DONE, CANCELLED): + job.channel.set_done(job) + elif state == PENDING: + job.channel.set_pending(job) + elif state in (ENQUEUED, STARTED): + job.channel.set_running(job) + elif state == FAILED: + job.channel.set_failed(job) + elif state == WAIT_DEPENDENCIES: + # wait until all parent jobs are done + pass + else: + _logger.error("unexpected state %s for job %s", state, job) + + def remove_job(self, uuid): + job = self._jobs_by_uuid.get(uuid) + if job: + job.channel.remove(job) + del self._jobs_by_uuid[job.uuid] + + def remove_db(self, db_name): + for job in list(self._jobs_by_uuid.values()): + if job.db_name == db_name: + job.channel.remove(job) + del self._jobs_by_uuid[job.uuid] + + def get_jobs_to_run(self, now): + return self._root_channel.get_jobs_to_run(now) + + def get_wakeup_time(self): + return self._root_channel.get_wakeup_time() diff --git a/queue_job/jobrunner/runner.py b/queue_job/jobrunner/runner.py new file mode 100644 index 0000000000..c1565c1767 --- /dev/null +++ b/queue_job/jobrunner/runner.py @@ -0,0 +1,532 @@ +# Copyright (c) 2015-2016 ACSONE SA/NV () +# Copyright 2015-2016 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) +""" +What is the job runner? +----------------------- +The job runner is the main process managing the dispatch of delayed jobs to +available Odoo workers + +How does it work? +----------------- + +* It starts as a thread in the Odoo main process or as a new worker +* It receives postgres NOTIFY messages each time jobs are + added or updated in the queue_job table. +* It maintains an in-memory priority queue of jobs that + is populated from the queue_job tables in all databases. +* It does not run jobs itself, but asks Odoo to run them through an + anonymous ``/queue_job/runjob`` HTTP request. [1]_ + +How to use it? +-------------- + +* Optionally adjust your configuration through environment variables: + + - ``ODOO_QUEUE_JOB_CHANNELS=root:4`` (or any other channels + configuration), default ``root:1``. + - ``ODOO_QUEUE_JOB_SCHEME=https``, default ``http``. + - ``ODOO_QUEUE_JOB_HOST=load-balancer``, default ``http_interface`` + or ``localhost`` if unset. + - ``ODOO_QUEUE_JOB_PORT=443``, default ``http_port`` or 8069 if unset. + - ``ODOO_QUEUE_JOB_HTTP_AUTH_USER=jobrunner``, default empty. + - ``ODOO_QUEUE_JOB_HTTP_AUTH_PASSWORD=s3cr3t``, default empty. + - ``ODOO_QUEUE_JOB_JOBRUNNER_DB_HOST=master-db``, default ``db_host`` + or ``False`` if unset. + - ``ODOO_QUEUE_JOB_JOBRUNNER_DB_PORT=5432``, default ``db_port`` + or ``False`` if unset. + - ``ODOO_QUEUE_JOB_JOBRUNNER_DB_USER=userdb``, default ``db_user`` + or ``False`` if unset. + - ``ODOO_QUEUE_JOB_JOBRUNNER_DB_PASSWORD=passdb``, default ``db_password`` + or ``False`` if unset. + +* Alternatively, configure the channels through the Odoo configuration + file, like: + +.. code-block:: ini + + [queue_job] + channels = root:4 + scheme = https + host = load-balancer + port = 443 + http_auth_user = jobrunner + http_auth_password = s3cr3t + jobrunner_db_host = master-db + jobrunner_db_port = 5432 + jobrunner_db_user = userdb + jobrunner_db_password = passdb + +* Or, if using ``anybox.recipe.odoo``, add this to your buildout configuration: + +.. code-block:: ini + + [odoo] + recipe = anybox.recipe.odoo + (...) + queue_job.channels = root:4 + queue_job.scheme = https + queue_job.host = load-balancer + queue_job.port = 443 + queue_job.http_auth_user = jobrunner + queue_job.http_auth_password = s3cr3t + +* Start Odoo with ``--load=web,web_kanban,queue_job`` + and ``--workers`` greater than 1 [2]_, or set the ``server_wide_modules`` + option in The Odoo configuration file: + +.. code-block:: ini + + [options] + (...) + workers = 4 + server_wide_modules = web,web_kanban,queue_job + (...) + +* Or, if using ``anybox.recipe.odoo``: + +.. code-block:: ini + + [odoo] + recipe = anybox.recipe.odoo + (...) + options.workers = 4 + options.server_wide_modules = web,web_kanban,queue_job + +* Confirm the runner is starting correctly by checking the odoo log file: + +.. code-block:: none + + ...INFO...queue_job.jobrunner.runner: starting + ...INFO...queue_job.jobrunner.runner: initializing database connections + ...INFO...queue_job.jobrunner.runner: queue job runner ready for db + ...INFO...queue_job.jobrunner.runner: database connections ready + +* Create jobs (eg using base_import_async) and observe they + start immediately and in parallel. + +* Tip: to enable debug logging for the queue job, use + ``--log-handler=odoo.addons.queue_job:DEBUG`` + +Caveat +------ + +* After creating a new database or installing queue_job on an + existing database, Odoo must be restarted for the runner to detect it. + +* When Odoo shuts down normally, it waits for running jobs to finish. + However, when the Odoo server crashes or is otherwise force-stopped, + running jobs are interrupted while the runner has no chance to know + they have been aborted. In such situations, jobs may remain in + ``started`` or ``enqueued`` state after the Odoo server is halted. + Since the runner has no way to know if they are actually running or + not, and does not know for sure if it is safe to restart the jobs, + it does not attempt to restart them automatically. Such stale jobs + therefore fill the running queue and prevent other jobs to start. + You must therefore requeue them manually, either from the Jobs view, + or by running the following SQL statement *before starting Odoo*: + +.. code-block:: sql + + update queue_job set state='pending' where state in ('started', 'enqueued') + +.. rubric:: Footnotes + +.. [1] From a security standpoint, it is safe to have an anonymous HTTP + request because this request only accepts to run jobs that are + enqueued. +.. [2] It works with the threaded Odoo server too, although this way + of running Odoo is obviously not for production purposes. +""" + +import datetime +import logging +import os +import selectors +import threading +import time +from contextlib import closing, contextmanager + +import psycopg2 +import requests +from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT + +import odoo +from odoo.tools import config + +from . import queue_job_config +from .channels import ENQUEUED, NOT_DONE, PENDING, ChannelManager + +SELECT_TIMEOUT = 60 +ERROR_RECOVERY_DELAY = 5 + +_logger = logging.getLogger(__name__) + +select = selectors.DefaultSelector + + +# Unfortunately, it is not possible to extend the Odoo +# server command line arguments, so we resort to environment variables +# to configure the runner (channels mostly). +# +# On the other hand, the odoo configuration file can be extended at will, +# so we check it in addition to the environment variables. + + +def _channels(): + return ( + os.environ.get("ODOO_QUEUE_JOB_CHANNELS") + or queue_job_config.get("channels") + or "root:1" + ) + + +def _datetime_to_epoch(dt): + # important: this must return the same as postgresql + # EXTRACT(EPOCH FROM TIMESTAMP dt) + return (dt - datetime.datetime(1970, 1, 1)).total_seconds() + + +def _odoo_now(): + dt = datetime.datetime.utcnow() + return _datetime_to_epoch(dt) + + +def _connection_info_for(db_name): + db_or_uri, connection_info = odoo.sql_db.connection_info_for(db_name) + + for p in ("host", "port", "user", "password"): + cfg = os.environ.get( + f"ODOO_QUEUE_JOB_JOBRUNNER_DB_{p.upper()}" + ) or queue_job_config.get("jobrunner_db_" + p) + + if cfg: + connection_info[p] = cfg + + return connection_info + + +def _async_http_get(scheme, host, port, user, password, db_name, job_uuid): + # Method to set failed job (due to timeout, etc) as pending, + # to avoid keeping it as enqueued. + def set_job_pending(): + connection_info = _connection_info_for(db_name) + conn = psycopg2.connect(**connection_info) + conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) + with closing(conn.cursor()) as cr: + cr.execute( + "UPDATE queue_job SET state=%s, " + "date_enqueued=NULL, date_started=NULL " + "WHERE uuid=%s and state=%s " + "RETURNING uuid", + (PENDING, job_uuid, ENQUEUED), + ) + if cr.fetchone(): + _logger.warning( + "state of job %s was reset from %s to %s", + job_uuid, + ENQUEUED, + PENDING, + ) + + # TODO: better way to HTTP GET asynchronously (grequest, ...)? + # if this was python3 I would be doing this with + # asyncio, aiohttp and aiopg + def urlopen(): + url = f"{scheme}://{host}:{port}/queue_job/runjob?db={db_name}&job_uuid={job_uuid}" + try: + auth = None + if user: + auth = (user, password) + # we are not interested in the result, so we set a short timeout + # but not too short so we trap and log hard configuration errors + response = requests.get(url, timeout=1, auth=auth) + + # raise_for_status will result in either nothing, a Client Error + # for HTTP Response codes between 400 and 500 or a Server Error + # for codes between 500 and 600 + response.raise_for_status() + except requests.Timeout: + set_job_pending() + except Exception: + _logger.exception("exception in GET %s", url) + set_job_pending() + + thread = threading.Thread(target=urlopen) + thread.daemon = True + thread.start() + + +class Database: + def __init__(self, db_name): + self.db_name = db_name + connection_info = _connection_info_for(db_name) + self.conn = psycopg2.connect(**connection_info) + self.conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) + self.has_queue_job = self._has_queue_job() + if self.has_queue_job: + self._initialize() + + def close(self): + # pylint: disable=except-pass + # if close fail for any reason, it's either because it's already closed + # and we don't care, or for any reason but anyway it will be closed on + # del + try: + self.conn.close() + except Exception: + pass + self.conn = None + + def _has_queue_job(self): + with closing(self.conn.cursor()) as cr: + cr.execute( + "SELECT 1 FROM pg_tables WHERE tablename=%s", ("ir_module_module",) + ) + if not cr.fetchone(): + _logger.debug("%s doesn't seem to be an odoo db", self.db_name) + return False + cr.execute( + "SELECT 1 FROM ir_module_module WHERE name=%s AND state=%s", + ("queue_job", "installed"), + ) + if not cr.fetchone(): + _logger.debug("queue_job is not installed for db %s", self.db_name) + return False + cr.execute( + """SELECT COUNT(1) + FROM information_schema.triggers + WHERE event_object_table = %s + AND trigger_name = %s""", + ("queue_job", "queue_job_notify"), + ) + if cr.fetchone()[0] != 3: # INSERT, DELETE, UPDATE + _logger.error( + "queue_job_notify trigger is missing in db %s", self.db_name + ) + return False + return True + + def _initialize(self): + with closing(self.conn.cursor()) as cr: + cr.execute("LISTEN queue_job") + + @contextmanager + def select_jobs(self, where, args): + # pylint: disable=sql-injection + # the checker thinks we are injecting values but we are not, we are + # adding the where conditions, values are added later properly with + # parameters + query = ( + "SELECT channel, uuid, id as seq, date_created, " + "priority, EXTRACT(EPOCH FROM eta), state " + f"FROM queue_job WHERE {where}" + ) + with closing(self.conn.cursor("select_jobs", withhold=True)) as cr: + cr.execute(query, args) + yield cr + + def keep_alive(self): + query = "SELECT 1" + with closing(self.conn.cursor()) as cr: + cr.execute(query) + + def set_job_enqueued(self, uuid): + with closing(self.conn.cursor()) as cr: + cr.execute( + "UPDATE queue_job SET state=%s, " + "date_enqueued=date_trunc('seconds', " + " now() at time zone 'utc') " + "WHERE uuid=%s", + (ENQUEUED, uuid), + ) + + +class QueueJobRunner: + def __init__( + self, + scheme="http", + host="localhost", + port=8069, + user=None, + password=None, + channel_config_string=None, + ): + self.scheme = scheme + self.host = host + self.port = port + self.user = user + self.password = password + self.channel_manager = ChannelManager() + if channel_config_string is None: + channel_config_string = _channels() + self.channel_manager.simple_configure(channel_config_string) + self.db_by_name = {} + self._stop = False + self._stop_pipe = os.pipe() + + @classmethod + def from_environ_or_config(cls): + scheme = os.environ.get("ODOO_QUEUE_JOB_SCHEME") or queue_job_config.get( + "scheme" + ) + host = ( + os.environ.get("ODOO_QUEUE_JOB_HOST") + or queue_job_config.get("host") + or config["http_interface"] + ) + port = ( + os.environ.get("ODOO_QUEUE_JOB_PORT") + or queue_job_config.get("port") + or config["http_port"] + ) + user = os.environ.get("ODOO_QUEUE_JOB_HTTP_AUTH_USER") or queue_job_config.get( + "http_auth_user" + ) + password = os.environ.get( + "ODOO_QUEUE_JOB_HTTP_AUTH_PASSWORD" + ) or queue_job_config.get("http_auth_password") + runner = cls( + scheme=scheme or "http", + host=host or "localhost", + port=port or 8069, + user=user, + password=password, + ) + return runner + + def get_db_names(self): + if config["db_name"]: + db_names = config["db_name"].split(",") + else: + db_names = odoo.service.db.list_dbs(True) + return db_names + + def close_databases(self, remove_jobs=True): + for db_name, db in self.db_by_name.items(): + try: + if remove_jobs: + self.channel_manager.remove_db(db_name) + db.close() + except Exception: + _logger.warning("error closing database %s", db_name, exc_info=True) + self.db_by_name = {} + + def initialize_databases(self): + for db_name in self.get_db_names(): + db = Database(db_name) + if db.has_queue_job: + self.db_by_name[db_name] = db + with db.select_jobs("state in %s", (NOT_DONE,)) as cr: + for job_data in cr: + self.channel_manager.notify(db_name, *job_data) + _logger.info("queue job runner ready for db %s", db_name) + + def run_jobs(self): + now = _odoo_now() + for job in self.channel_manager.get_jobs_to_run(now): + if self._stop: + break + _logger.info("asking Odoo to run job %s on db %s", job.uuid, job.db_name) + self.db_by_name[job.db_name].set_job_enqueued(job.uuid) + _async_http_get( + self.scheme, + self.host, + self.port, + self.user, + self.password, + job.db_name, + job.uuid, + ) + + def process_notifications(self): + for db in self.db_by_name.values(): + if not db.conn.notifies: + # If there are no activity in the queue_job table it seems that + # tcp keepalives are not sent (in that very specific scenario), + # causing some intermediaries (such as haproxy) to close the + # connection, making the jobrunner to restart on a socket error + db.keep_alive() + while db.conn.notifies: + if self._stop: + break + notification = db.conn.notifies.pop() + uuid = notification.payload + with db.select_jobs("uuid = %s", (uuid,)) as cr: + job_datas = cr.fetchone() + if job_datas: + self.channel_manager.notify(db.db_name, *job_datas) + else: + self.channel_manager.remove_job(uuid) + + def wait_notification(self): + for db in self.db_by_name.values(): + if db.conn.notifies: + # something is going on in the queue, no need to wait + return + # wait for something to happen in the queue_job tables + # we'll select() on database connections and the stop pipe + conns = [db.conn for db in self.db_by_name.values()] + conns.append(self._stop_pipe[0]) + # look if the channels specify a wakeup time + wakeup_time = self.channel_manager.get_wakeup_time() + if not wakeup_time: + # this could very well be no timeout at all, because + # any activity in the job queue will wake us up, but + # let's have a timeout anyway, just to be safe + timeout = SELECT_TIMEOUT + else: + timeout = wakeup_time - _odoo_now() + # wait for a notification or a timeout; + # if timeout is negative (ie wakeup time in the past), + # do not wait; this should rarely happen + # because of how get_wakeup_time is designed; actually + # if timeout remains a large negative number, it is most + # probably a bug + _logger.debug("select() timeout: %.2f sec", timeout) + if timeout > 0: + if conns and not self._stop: + with select() as sel: + for conn in conns: + sel.register(conn, selectors.EVENT_READ) + events = sel.select(timeout=timeout) + for key, _mask in events: + if key.fileobj == self._stop_pipe[0]: + # stop-pipe is not a conn so doesn't need poll() + continue + key.fileobj.poll() + + def stop(self): + _logger.info("graceful stop requested") + self._stop = True + # wakeup the select() in wait_notification + os.write(self._stop_pipe[1], b".") + + def run(self): + _logger.info("starting") + while not self._stop: + # outer loop does exception recovery + try: + _logger.info("initializing database connections") + # TODO: how to detect new databases or databases + # on which queue_job is installed after server start? + self.initialize_databases() + _logger.info("database connections ready") + # inner loop does the normal processing + while not self._stop: + self.process_notifications() + self.run_jobs() + self.wait_notification() + except KeyboardInterrupt: + self.stop() + except InterruptedError: + # Interrupted system call, i.e. KeyboardInterrupt during select + self.stop() + except Exception: + _logger.exception( + "exception: sleeping %ds and retrying", ERROR_RECOVERY_DELAY + ) + self.close_databases() + time.sleep(ERROR_RECOVERY_DELAY) + self.close_databases(remove_jobs=False) + _logger.info("stopped") diff --git a/queue_job/migrations/18.0.1.0.0/pre-migrate.py b/queue_job/migrations/18.0.1.0.0/pre-migrate.py new file mode 100644 index 0000000000..3f02e7bc75 --- /dev/null +++ b/queue_job/migrations/18.0.1.0.0/pre-migrate.py @@ -0,0 +1,21 @@ +from openupgradelib import openupgrade + + +def migrate(cr, version): + if not version: + return + + # List of tables and their corresponding columns + table_column_map = { + "queue.job.function": ["retry_pattern", "related_action"], + "queue.job": ["records", "args", "kwargs"], + } + + for table, columns in table_column_map.items(): + for column in columns: + if openupgrade.column_exists(cr, table, column): + cr.execute(f""" + UPDATE {table} + SET {column} = {column}::jsonb + WHERE {column} IS NOT NULL + """) diff --git a/queue_job/models/__init__.py b/queue_job/models/__init__.py new file mode 100644 index 0000000000..4744e7ab46 --- /dev/null +++ b/queue_job/models/__init__.py @@ -0,0 +1,5 @@ +from . import base +from . import ir_model_fields +from . import queue_job +from . import queue_job_channel +from . import queue_job_function diff --git a/queue_job/models/base.py b/queue_job/models/base.py new file mode 100644 index 0000000000..3a68ffd11b --- /dev/null +++ b/queue_job/models/base.py @@ -0,0 +1,270 @@ +# Copyright 2016 Camptocamp +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +import functools + +from odoo import api, models + +from ..delay import Delayable, DelayableRecordset +from ..utils import must_run_without_delay + + +class Base(models.AbstractModel): + """The base model, which is implicitly inherited by all models. + + A new :meth:`~with_delay` method is added on all Odoo Models, allowing to + postpone the execution of a job method in an asynchronous process. + """ + + _inherit = "base" + + def with_delay( + self, + priority=None, + eta=None, + max_retries=None, + description=None, + channel=None, + identity_key=None, + ): + """Return a ``DelayableRecordset`` + + It is a shortcut for the longer form as shown below:: + + self.with_delay(priority=20).action_done() + # is equivalent to: + self.delayable().set(priority=20).action_done().delay() + + ``with_delay()`` accepts job properties which specify how the job will + be executed. + + Usage with job properties:: + + env['a.model'].with_delay(priority=30, eta=60*60*5).action_done() + delayable.export_one_thing(the_thing_to_export) + # => the job will be executed with a low priority and not before a + # delay of 5 hours from now + + When using :meth:``with_delay``, the final ``delay()`` is implicit. + See the documentation of :meth:``delayable`` for more details. + + :return: instance of a DelayableRecordset + :rtype: :class:`odoo.addons.queue_job.job.DelayableRecordset` + """ + return DelayableRecordset( + self, + priority=priority, + eta=eta, + max_retries=max_retries, + description=description, + channel=channel, + identity_key=identity_key, + ) + + def delayable( + self, + priority=None, + eta=None, + max_retries=None, + description=None, + channel=None, + identity_key=None, + ): + """Return a ``Delayable`` + + The returned instance allows to enqueue any method of the recordset's + Model. + + Usage:: + + delayable = self.env["res.users"].browse(10).delayable(priority=20) + delayable.do_work(name="test"}).delay() + + In this example, the ``do_work`` method will not be executed directly. + It will be executed in an asynchronous job. + + Method calls on a Delayable generally return themselves, so calls can + be chained together:: + + delayable.set(priority=15).do_work(name="test"}).delay() + + The order of the calls that build the job is not relevant, beside + the call to ``delay()`` that must happen at the very end. This is + equivalent to the example above:: + + delayable.do_work(name="test"}).set(priority=15).delay() + + Very importantly, ``delay()`` must be called on the top-most parent + of a chain of jobs, so if you have this:: + + job1 = record1.delayable().do_work() + job2 = record2.delayable().do_work() + job1.on_done(job2) + + The ``delay()`` call must be made on ``job1``, otherwise ``job2`` will + be delayed, but ``job1`` will never be. When done on ``job1``, the + ``delay()`` call will traverse the graph of jobs and delay all of + them:: + + job1.delay() + + For more details on the graph dependencies, read the documentation of + :module:`~odoo.addons.queue_job.delay`. + + :param priority: Priority of the job, 0 being the higher priority. + Default is 10. + :param eta: Estimated Time of Arrival of the job. It will not be + executed before this date/time. + :param max_retries: maximum number of retries before giving up and set + the job state to 'failed'. A value of 0 means + infinite retries. Default is 5. + :param description: human description of the job. If None, description + is computed from the function doc or name + :param channel: the complete name of the channel to use to process + the function. If specified it overrides the one + defined on the function + :param identity_key: key uniquely identifying the job, if specified + and a job with the same key has not yet been run, + the new job will not be added. It is either a + string, either a function that takes the job as + argument (see :py:func:`..job.identity_exact`). + the new job will not be added. + :return: instance of a Delayable + :rtype: :class:`odoo.addons.queue_job.job.Delayable` + """ + return Delayable( + self, + priority=priority, + eta=eta, + max_retries=max_retries, + description=description, + channel=channel, + identity_key=identity_key, + ) + + def _patch_job_auto_delay(self, method_name, context_key=None): + """Patch a method to be automatically delayed as job method when called + + This patch method has to be called in ``_register_hook`` (example + below). + + When a method is patched, any call to the method will not directly + execute the method's body, but will instead enqueue a job. + + When a ``context_key`` is set when calling ``_patch_job_auto_delay``, + the patched method is automatically delayed only when this key is + ``True`` in the caller's context. It is advised to patch the method + with a ``context_key``, because making the automatic delay *in any + case* can produce nasty and unexpected side effects (e.g. another + module calls the method and expects it to be computed before doing + something else, expecting a result, ...). + + A typical use case is when a method in a module we don't control is + called synchronously in the middle of another method, and we'd like all + the calls to this method become asynchronous. + + The options of the job usually passed to ``with_delay()`` (priority, + description, identity_key, ...) can be returned in a dictionary by a + method named after the name of the method suffixed by ``_job_options`` + which takes the same parameters as the initial method. + + Example patching the "foo" method to be automatically delayed as job + (the job options method is optional): + + .. code-block:: python + + # original method: + def foo(self, arg1): + print("hello", arg1) + + def large_method(self): + # doing a lot of things + self.foo("world) + # doing a lot of other things + + def button_x(self): + self.with_context(auto_delay_foo=True).large_method() + + # auto delay patch: + def foo_job_options(self, arg1): + return { + "priority": 100, + "description": "Saying hello to {}".format(arg1) + } + + def _register_hook(self): + self._patch_method( + "foo", + self._patch_job_auto_delay("foo", context_key="auto_delay_foo") + ) + return super()._register_hook() + + The result when ``button_x`` is called, is that a new job for ``foo`` + is delayed. + """ + + def auto_delay_wrapper(self, *args, **kwargs): + # when no context_key is set, we delay in any case (warning, can be + # dangerous) + context_delay = self.env.context.get(context_key) if context_key else True + if ( + self.env.context.get("job_uuid") + or not context_delay + or must_run_without_delay(self.env) + ): + # we are in the job execution + return auto_delay_wrapper.origin(self, *args, **kwargs) + else: + # replace the synchronous call by a job on itself + method_name = auto_delay_wrapper.origin.__name__ + job_options_method = getattr(self, f"{method_name}_job_options", None) + job_options = {} + if job_options_method: + job_options.update(job_options_method(*args, **kwargs)) + delayed = self.with_delay(**job_options) + return getattr(delayed, method_name)(*args, **kwargs) + + origin = getattr(self, method_name) + return functools.update_wrapper(auto_delay_wrapper, origin) + + @api.model + def _job_store_values(self, job): + """Hook for manipulating job stored values. + + You can define a more specific hook for a job function + by defining a method name with this pattern: + + `_queue_job_store_values_${func_name}` + + NOTE: values will be stored only if they match stored fields on `queue.job`. + + :param job: current queue_job.job.Job instance. + :return: dictionary for setting job values. + """ + return {} + + @api.model + def _job_prepare_context_before_enqueue_keys(self): + """Keys to keep in context of stored jobs + Empty by default for backward compatibility. + """ + return ("tz", "lang", "allowed_company_ids", "force_company", "active_test") + + def _job_prepare_context_before_enqueue(self): + """Return the context to store in the jobs + Can be used to keep only safe keys. + """ + return { + key: value + for key, value in self.env.context.items() + if key in self._job_prepare_context_before_enqueue_keys() + } + + @classmethod + def _patch_method(cls, name, method): + origin = getattr(cls, name) + method.origin = origin + # propagate decorators from origin to method, and apply api decorator + wrapped = api.propagate(origin, method) + wrapped.origin = origin + setattr(cls, name, wrapped) diff --git a/queue_job/models/ir_model_fields.py b/queue_job/models/ir_model_fields.py new file mode 100644 index 0000000000..5a31fcdc5f --- /dev/null +++ b/queue_job/models/ir_model_fields.py @@ -0,0 +1,13 @@ +# Copyright 2020 Camptocamp +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +from odoo import fields, models + + +class IrModelFields(models.Model): + _inherit = "ir.model.fields" + + ttype = fields.Selection( + selection_add=[("job_serialized", "Job Serialized")], + ondelete={"job_serialized": "cascade"}, + ) diff --git a/queue_job/models/queue_job.py b/queue_job/models/queue_job.py new file mode 100644 index 0000000000..c4f1faaef5 --- /dev/null +++ b/queue_job/models/queue_job.py @@ -0,0 +1,497 @@ +# Copyright 2013-2020 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +import logging +import random +from datetime import datetime, timedelta + +from odoo import _, api, exceptions, fields, models +from odoo.osv import expression +from odoo.tools import config, html_escape + +from odoo.addons.base_sparse_field.models.fields import Serialized + +from ..delay import Graph +from ..exception import JobError +from ..fields import JobSerialized +from ..job import ( + CANCELLED, + DONE, + FAILED, + PENDING, + STARTED, + STATES, + WAIT_DEPENDENCIES, + Job, +) + +_logger = logging.getLogger(__name__) + + +class QueueJob(models.Model): + """Model storing the jobs to be executed.""" + + _name = "queue.job" + _description = "Queue Job" + _inherit = ["mail.thread", "mail.activity.mixin"] + _log_access = False + + _order = "date_created DESC, date_done DESC" + + _removal_interval = 30 # days + _default_related_action = "related_action_open_record" + + # This must be passed in a context key "_job_edit_sentinel" to write on + # protected fields. It protects against crafting "queue.job" records from + # RPC (e.g. on internal methods). When ``with_delay`` is used, the sentinel + # is set. + EDIT_SENTINEL = object() + _protected_fields = ( + "uuid", + "name", + "date_created", + "model_name", + "method_name", + "func_string", + "channel_method_name", + "job_function_id", + "records", + "args", + "kwargs", + ) + + uuid = fields.Char(string="UUID", readonly=True, index=True, required=True) + graph_uuid = fields.Char( + string="Graph UUID", + readonly=True, + index=True, + help="Single shared identifier of a Graph. Empty for a single job.", + ) + user_id = fields.Many2one(comodel_name="res.users", string="User ID") + company_id = fields.Many2one( + comodel_name="res.company", string="Company", index=True + ) + name = fields.Char(string="Description", readonly=True) + + model_name = fields.Char(string="Model", readonly=True) + method_name = fields.Char(readonly=True) + records = JobSerialized( + string="Record(s)", + readonly=True, + base_type=models.BaseModel, + ) + dependencies = Serialized(readonly=True) + # dependency graph as expected by the field widget + dependency_graph = Serialized(compute="_compute_dependency_graph") + graph_jobs_count = fields.Integer(compute="_compute_graph_jobs_count") + args = JobSerialized(readonly=True, base_type=tuple) + kwargs = JobSerialized(readonly=True, base_type=dict) + func_string = fields.Char(string="Task", readonly=True) + + state = fields.Selection(STATES, readonly=True, required=True, index=True) + priority = fields.Integer() + exc_name = fields.Char(string="Exception", readonly=True) + exc_message = fields.Char(string="Exception Message", readonly=True, tracking=True) + exc_info = fields.Text(string="Exception Info", readonly=True) + result = fields.Text(readonly=True) + + date_created = fields.Datetime(string="Created Date", readonly=True) + date_started = fields.Datetime(string="Start Date", readonly=True) + date_enqueued = fields.Datetime(string="Enqueue Time", readonly=True) + date_done = fields.Datetime(readonly=True) + exec_time = fields.Float( + string="Execution Time (avg)", + aggregator="avg", + help="Time required to execute this job in seconds. Average when grouped.", + ) + date_cancelled = fields.Datetime(readonly=True) + + eta = fields.Datetime(string="Execute only after") + retry = fields.Integer(string="Current try") + max_retries = fields.Integer( + string="Max. retries", + help="The job will fail if the number of tries reach the " + "max. retries.\n" + "Retries are infinite when empty.", + ) + # FIXME the name of this field is very confusing + channel_method_name = fields.Char(string="Complete Method Name", readonly=True) + job_function_id = fields.Many2one( + comodel_name="queue.job.function", + string="Job Function", + readonly=True, + ) + + channel = fields.Char(index=True) + + identity_key = fields.Char(readonly=True) + worker_pid = fields.Integer(readonly=True) + + def init(self): + self._cr.execute( + "SELECT indexname FROM pg_indexes WHERE indexname = %s ", + ("queue_job_identity_key_state_partial_index",), + ) + if not self._cr.fetchone(): + self._cr.execute( + "CREATE INDEX queue_job_identity_key_state_partial_index " + "ON queue_job (identity_key) WHERE state in ('pending', " + "'enqueued', 'wait_dependencies') AND identity_key IS NOT NULL;" + ) + + @api.depends("dependencies") + def _compute_dependency_graph(self): + jobs_groups = self.env["queue.job"].read_group( + [ + ( + "graph_uuid", + "in", + [uuid for uuid in self.mapped("graph_uuid") if uuid], + ) + ], + ["graph_uuid", "ids:array_agg(id)"], + ["graph_uuid"], + ) + ids_per_graph_uuid = { + group["graph_uuid"]: group["ids"] for group in jobs_groups + } + for record in self: + if not record.graph_uuid: + record.dependency_graph = {} + continue + + graph_jobs = self.browse(ids_per_graph_uuid.get(record.graph_uuid) or []) + if not graph_jobs: + record.dependency_graph = {} + continue + + graph_ids = {graph_job.uuid: graph_job.id for graph_job in graph_jobs} + graph_jobs_by_ids = {graph_job.id: graph_job for graph_job in graph_jobs} + + graph = Graph() + for graph_job in graph_jobs: + graph.add_vertex(graph_job.id) + for parent_uuid in graph_job.dependencies["depends_on"]: + parent_id = graph_ids.get(parent_uuid) + if not parent_id: + continue + graph.add_edge(parent_id, graph_job.id) + for child_uuid in graph_job.dependencies["reverse_depends_on"]: + child_id = graph_ids.get(child_uuid) + if not child_id: + continue + graph.add_edge(graph_job.id, child_id) + + record.dependency_graph = { + # list of ids + "nodes": [ + graph_jobs_by_ids[graph_id]._dependency_graph_vis_node() + for graph_id in graph.vertices() + ], + # list of tuples (from, to) + "edges": graph.edges(), + } + + def _dependency_graph_vis_node(self): + """Return the node as expected by the JobDirectedGraph widget""" + default = ("#D2E5FF", "#2B7CE9") + colors = { + DONE: ("#C2FABC", "#4AD63A"), + FAILED: ("#FB7E81", "#FA0A10"), + STARTED: ("#FFFF00", "#FFA500"), + } + return { + "id": self.id, + "title": ( + f"{html_escape(self.display_name)}
" + f"{html_escape(self.func_string)}" + ), + "color": colors.get(self.state, default)[0], + "border": colors.get(self.state, default)[1], + "shadow": True, + } + + def _compute_graph_jobs_count(self): + jobs_groups = self.env["queue.job"].read_group( + [ + ( + "graph_uuid", + "in", + [uuid for uuid in self.mapped("graph_uuid") if uuid], + ) + ], + ["graph_uuid"], + ["graph_uuid"], + ) + count_per_graph_uuid = { + group["graph_uuid"]: group["graph_uuid_count"] for group in jobs_groups + } + for record in self: + record.graph_jobs_count = count_per_graph_uuid.get(record.graph_uuid) or 0 + + @api.model_create_multi + def create(self, vals_list): + if self.env.context.get("_job_edit_sentinel") is not self.EDIT_SENTINEL: + # Prevent to create a queue.job record "raw" from RPC. + # ``with_delay()`` must be used. + raise exceptions.AccessError( + _("Queue jobs must be created by calling 'with_delay()'.") + ) + return super( + QueueJob, + self.with_context(mail_create_nolog=True, mail_create_nosubscribe=True), + ).create(vals_list) + + def write(self, vals): + if self.env.context.get("_job_edit_sentinel") is not self.EDIT_SENTINEL: + write_on_protected_fields = [ + fieldname for fieldname in vals if fieldname in self._protected_fields + ] + if write_on_protected_fields: + raise exceptions.AccessError( + _("Not allowed to change field(s): {}").format( + write_on_protected_fields + ) + ) + + different_user_jobs = self.browse() + if vals.get("user_id"): + different_user_jobs = self.filtered( + lambda records: records.env.user.id != vals["user_id"] + ) + + if vals.get("state") == "failed": + self._message_post_on_failure() + + result = super().write(vals) + + for record in different_user_jobs: + # the user is stored in the env of the record, but we still want to + # have a stored user_id field to be able to search/groupby, so + # synchronize the env of records with user_id + super(QueueJob, record).write( + {"records": record.records.with_user(vals["user_id"])} + ) + return result + + def open_related_action(self): + """Open the related action associated to the job""" + self.ensure_one() + job = Job.load(self.env, self.uuid) + action = job.related_action() + if action is None: + raise exceptions.UserError(_("No action available for this job")) + return action + + def open_graph_jobs(self): + """Return action that opens all jobs of the same graph""" + self.ensure_one() + jobs = self.env["queue.job"].search([("graph_uuid", "=", self.graph_uuid)]) + + action = self.env["ir.actions.act_window"]._for_xml_id( + "queue_job.action_queue_job" + ) + action.update( + { + "name": _("Jobs for graph %s") % (self.graph_uuid), + "context": {}, + "domain": [("id", "in", jobs.ids)], + } + ) + return action + + def _change_job_state(self, state, result=None): + """Change the state of the `Job` object + + Changing the state of the Job will automatically change some fields + (date, result, ...). + """ + for record in self: + job_ = Job.load(record.env, record.uuid) + if state == DONE: + job_.set_done(result=result) + job_.store() + record.env["queue.job"].flush_model() + job_.enqueue_waiting() + elif state == PENDING: + job_.set_pending(result=result) + job_.store() + elif state == CANCELLED: + job_.set_cancelled(result=result) + job_.store() + record.env["queue.job"].flush_model() + job_.cancel_dependent_jobs() + else: + raise ValueError(f"State not supported: {state}") + + def button_done(self): + result = _("Manually set to done by {}").format(self.env.user.name) + self._change_job_state(DONE, result=result) + return True + + def button_cancelled(self): + result = _("Cancelled by {}").format(self.env.user.name) + self._change_job_state(CANCELLED, result=result) + return True + + def requeue(self): + jobs_to_requeue = self.filtered(lambda job_: job_.state != WAIT_DEPENDENCIES) + jobs_to_requeue._change_job_state(PENDING) + return True + + def _message_post_on_failure(self): + # subscribe the users now to avoid to subscribe them + # at every job creation + domain = self._subscribe_users_domain() + base_users = self.env["res.users"].search(domain) + for record in self: + users = base_users | record.user_id + record.message_subscribe(partner_ids=users.mapped("partner_id").ids) + msg = record._message_failed_job() + if msg: + record.message_post(body=msg, subtype_xmlid="queue_job.mt_job_failed") + + def _subscribe_users_domain(self): + """Subscribe all users having the 'Queue Job Manager' group""" + group = self.env.ref("queue_job.group_queue_job_manager") + if not group: + return None + companies = self.mapped("company_id") + domain = [("groups_id", "=", group.id)] + if companies: + domain.append(("company_id", "in", companies.ids)) + return domain + + def _message_failed_job(self): + """Return a message which will be posted on the job when it is failed. + + It can be inherited to allow more precise messages based on the + exception informations. + + If nothing is returned, no message will be posted. + """ + self.ensure_one() + return _( + "Something bad happened during the execution of the job. " + "More details in the 'Exception Information' section." + ) + + def _needaction_domain_get(self): + """Returns the domain to filter records that require an action + + :return: domain or False is no action + """ + return [("state", "=", "failed")] + + def autovacuum(self): + """Delete all jobs done based on the removal interval defined on the + channel + + Called from a cron. + """ + for channel in self.env["queue.job.channel"].search([]): + deadline = datetime.now() - timedelta(days=int(channel.removal_interval)) + while True: + jobs = self.search( + [ + "|", + ("date_done", "<=", deadline), + ("date_cancelled", "<=", deadline), + ("channel", "=", channel.complete_name), + ], + limit=1000, + ) + if jobs: + jobs.unlink() + if not config["test_enable"]: + self.env.cr.commit() # pylint: disable=E8102 + else: + break + return True + + def requeue_stuck_jobs(self, enqueued_delta=5, started_delta=0): + """Fix jobs that are in a bad states + + :param in_queue_delta: lookup time in minutes for jobs + that are in enqueued state + + :param started_delta: lookup time in minutes for jobs + that are in enqueued state, + 0 means that it is not checked + """ + self._get_stuck_jobs_to_requeue( + enqueued_delta=enqueued_delta, started_delta=started_delta + ).requeue() + return True + + def _get_stuck_jobs_domain(self, queue_dl, started_dl): + domain = [] + now = fields.datetime.now() + if queue_dl: + queue_dl = now - timedelta(minutes=queue_dl) + domain.append( + [ + "&", + ("date_enqueued", "<=", fields.Datetime.to_string(queue_dl)), + ("state", "=", "enqueued"), + ] + ) + if started_dl: + started_dl = now - timedelta(minutes=started_dl) + domain.append( + [ + "&", + ("date_started", "<=", fields.Datetime.to_string(started_dl)), + ("state", "=", "started"), + ] + ) + if not domain: + raise exceptions.ValidationError( + _("If both parameters are 0, ALL jobs will be requeued!") + ) + return expression.OR(domain) + + def _get_stuck_jobs_to_requeue(self, enqueued_delta, started_delta): + job_model = self.env["queue.job"] + stuck_jobs = job_model.search( + self._get_stuck_jobs_domain(enqueued_delta, started_delta) + ) + return stuck_jobs + + def related_action_open_record(self): + """Open a form view with the record(s) of the job. + + For instance, for a job on a ``product.product``, it will open a + ``product.product`` form view with the product record(s) concerned by + the job. If the job concerns more than one record, it opens them in a + list. + + This is the default related action. + + """ + self.ensure_one() + records = self.records.exists() + if not records: + return None + action = { + "name": _("Related Record"), + "type": "ir.actions.act_window", + "view_mode": "form", + "res_model": records._name, + } + if len(records) == 1: + action["res_id"] = records.id + else: + action.update( + { + "name": _("Related Records"), + "view_mode": "tree,form", + "domain": [("id", "in", records.ids)], + } + ) + return action + + def _test_job(self, failure_rate=0): + _logger.info("Running test job.") + if random.random() <= failure_rate: + raise JobError("Job failed") diff --git a/queue_job/models/queue_job_channel.py b/queue_job/models/queue_job_channel.py new file mode 100644 index 0000000000..4aabb0188c --- /dev/null +++ b/queue_job/models/queue_job_channel.py @@ -0,0 +1,89 @@ +# Copyright 2013-2020 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + + +from odoo import _, api, exceptions, fields, models + + +class QueueJobChannel(models.Model): + _name = "queue.job.channel" + _description = "Job Channels" + _rec_name = "complete_name" + + name = fields.Char() + complete_name = fields.Char( + compute="_compute_complete_name", store=True, readonly=True, recursive=True + ) + parent_id = fields.Many2one( + comodel_name="queue.job.channel", string="Parent Channel", ondelete="restrict" + ) + job_function_ids = fields.One2many( + comodel_name="queue.job.function", + inverse_name="channel_id", + string="Job Functions", + ) + removal_interval = fields.Integer( + default=lambda self: self.env["queue.job"]._removal_interval, required=True + ) + + _sql_constraints = [ + ("name_uniq", "unique(complete_name)", "Channel complete name must be unique") + ] + + @api.depends("name", "parent_id.complete_name") + def _compute_complete_name(self): + for record in self: + if not record.name: + complete_name = "" # new record + elif record.parent_id: + complete_name = ".".join([record.parent_id.complete_name, record.name]) + else: + complete_name = record.name + record.complete_name = complete_name + + @api.constrains("parent_id", "name") + def parent_required(self): + for record in self: + if record.name != "root" and not record.parent_id: + raise exceptions.ValidationError(_("Parent channel required.")) + + @api.model_create_multi + def create(self, vals_list): + records = self.browse() + if self.env.context.get("install_mode"): + # installing a module that creates a channel: rebinds the channel + # to an existing one (likely we already had the channel created by + # the @job decorator previously) + new_vals_list = [] + for vals in vals_list: + name = vals.get("name") + parent_id = vals.get("parent_id") + if name and parent_id: + existing = self.search( + [("name", "=", name), ("parent_id", "=", parent_id)] + ) + if existing: + if not existing.get_metadata()[0].get("noupdate"): + existing.write(vals) + records |= existing + continue + new_vals_list.append(vals) + vals_list = new_vals_list + records |= super().create(vals_list) + return records + + def write(self, values): + for channel in self: + if ( + not self.env.context.get("install_mode") + and channel.name == "root" + and ("name" in values or "parent_id" in values) + ): + raise exceptions.UserError(_("Cannot change the root channel")) + return super().write(values) + + def unlink(self): + for channel in self: + if channel.name == "root": + raise exceptions.UserError(_("Cannot remove the root channel")) + return super().unlink() diff --git a/queue_job/models/queue_job_function.py b/queue_job/models/queue_job_function.py new file mode 100644 index 0000000000..7cf73ea370 --- /dev/null +++ b/queue_job/models/queue_job_function.py @@ -0,0 +1,273 @@ +# Copyright 2013-2020 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +import ast +import logging +import re +from collections import namedtuple + +from odoo import _, api, exceptions, fields, models, tools + +from ..fields import JobSerialized + +_logger = logging.getLogger(__name__) + + +regex_job_function_name = re.compile(r"^<([0-9a-z_\.]+)>\.([0-9a-zA-Z_]+)$") + + +class QueueJobFunction(models.Model): + _name = "queue.job.function" + _description = "Job Functions" + _log_access = False + + JobConfig = namedtuple( + "JobConfig", + "channel " + "retry_pattern " + "related_action_enable " + "related_action_func_name " + "related_action_kwargs " + "job_function_id ", + ) + + def _default_channel(self): + return self.env.ref("queue_job.channel_root") + + name = fields.Char( + compute="_compute_name", + inverse="_inverse_name", + index=True, + store=True, + ) + + # model and method should be required, but the required flag doesn't + # let a chance to _inverse_name to be executed + model_id = fields.Many2one( + comodel_name="ir.model", string="Model", ondelete="cascade" + ) + method = fields.Char() + + channel_id = fields.Many2one( + comodel_name="queue.job.channel", + string="Channel", + required=True, + default=lambda r: r._default_channel(), + ) + channel = fields.Char(related="channel_id.complete_name", store=True, readonly=True) + retry_pattern = JobSerialized(string="Retry Pattern (serialized)", base_type=dict) + edit_retry_pattern = fields.Text( + string="Retry Pattern", + compute="_compute_edit_retry_pattern", + inverse="_inverse_edit_retry_pattern", + help="Pattern expressing from the count of retries on retryable errors," + " the number of of seconds to postpone the next execution. Setting the " + "number of seconds to a 2-element tuple or list will randomize the " + "retry interval between the 2 values.\n" + "Example: {1: 10, 5: 20, 10: 30, 15: 300}.\n" + "Example: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n" + "See the module description for details.", + ) + related_action = JobSerialized(string="Related Action (serialized)", base_type=dict) + edit_related_action = fields.Text( + string="Related Action", + compute="_compute_edit_related_action", + inverse="_inverse_edit_related_action", + help="The action when the button *Related Action* is used on a job. " + "The default action is to open the view of the record related " + "to the job. Configured as a dictionary with optional keys: " + "enable, func_name, kwargs.\n" + "See the module description for details.", + ) + + @api.depends("model_id.model", "method") + def _compute_name(self): + for record in self: + if not (record.model_id and record.method): + record.name = "" + continue + record.name = self.job_function_name(record.model_id.model, record.method) + + def _inverse_name(self): + groups = regex_job_function_name.match(self.name) + if not groups: + raise exceptions.UserError(_("Invalid job function: {}").format(self.name)) + model_name = groups[1] + method = groups[2] + model = ( + self.env["ir.model"].sudo().search([("model", "=", model_name)], limit=1) + ) + if not model: + raise exceptions.UserError(_("Model {} not found").format(model_name)) + self.model_id = model.id + self.method = method + + @api.depends("retry_pattern") + def _compute_edit_retry_pattern(self): + for record in self: + retry_pattern = record._parse_retry_pattern() + record.edit_retry_pattern = str(retry_pattern) + + def _inverse_edit_retry_pattern(self): + try: + edited = (self.edit_retry_pattern or "").strip() + if edited: + self.retry_pattern = ast.literal_eval(edited) + else: + self.retry_pattern = {} + except (ValueError, TypeError, SyntaxError) as ex: + raise exceptions.UserError( + self._retry_pattern_format_error_message() + ) from ex + + @api.depends("related_action") + def _compute_edit_related_action(self): + for record in self: + record.edit_related_action = str(record.related_action) + + def _inverse_edit_related_action(self): + try: + edited = (self.edit_related_action or "").strip() + if edited: + self.related_action = ast.literal_eval(edited) + else: + self.related_action = {} + except (ValueError, TypeError, SyntaxError) as ex: + raise exceptions.UserError( + self._related_action_format_error_message() + ) from ex + + @staticmethod + def job_function_name(model_name, method_name): + return f"<{model_name}>.{method_name}" + + def job_default_config(self): + return self.JobConfig( + channel="root", + retry_pattern={}, + related_action_enable=True, + related_action_func_name=None, + related_action_kwargs={}, + job_function_id=None, + ) + + def _parse_retry_pattern(self): + try: + # as json can't have integers as keys and the field is stored + # as json, convert back to int + retry_pattern = {} + for try_count, postpone_value in self.retry_pattern.items(): + if isinstance(postpone_value, int): + retry_pattern[int(try_count)] = postpone_value + else: + retry_pattern[int(try_count)] = tuple(postpone_value) + except ValueError: + _logger.error( + "Invalid retry pattern for job function %s," + " keys could not be parsed as integers, fallback" + " to the default retry pattern.", + self.name, + ) + retry_pattern = {} + return retry_pattern + + @tools.ormcache("name") + def job_config(self, name): + config = self.search([("name", "=", name)], limit=1) + if not config: + return self.job_default_config() + retry_pattern = config._parse_retry_pattern() + return self.JobConfig( + channel=config.channel, + retry_pattern=retry_pattern, + related_action_enable=config.related_action.get("enable", True), + related_action_func_name=config.related_action.get("func_name"), + related_action_kwargs=config.related_action.get("kwargs", {}), + job_function_id=config.id, + ) + + def _retry_pattern_format_error_message(self): + return _( + "Unexpected format of Retry Pattern for {}.\n" + "Example of valid formats:\n" + "{{1: 300, 5: 600, 10: 1200, 15: 3000}}\n" + "{{1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}}" + ).format(self.name) + + @api.constrains("retry_pattern") + def _check_retry_pattern(self): + for record in self: + retry_pattern = record.retry_pattern + if not retry_pattern: + continue + + all_values = list(retry_pattern) + list(retry_pattern.values()) + for value in all_values: + try: + self._retry_value_type_check(value) + except ValueError as ex: + raise exceptions.UserError( + record._retry_pattern_format_error_message() + ) from ex + + def _retry_value_type_check(self, value): + if isinstance(value, (tuple | list)): + if len(value) != 2: + raise ValueError + [self._retry_value_type_check(element) for element in value] + return + int(value) + + def _related_action_format_error_message(self): + return _( + "Unexpected format of Related Action for {}.\n" + "Example of valid format:\n" + '{{"enable": True, "func_name": "related_action_foo",' + ' "kwargs" {{"limit": 10}}}}' + ).format(self.name) + + @api.constrains("related_action") + def _check_related_action(self): + valid_keys = ("enable", "func_name", "kwargs") + for record in self: + related_action = record.related_action + if not related_action: + continue + + if any(key not in valid_keys for key in related_action): + raise exceptions.UserError( + record._related_action_format_error_message() + ) + + @api.model_create_multi + def create(self, vals_list): + records = self.browse() + if self.env.context.get("install_mode"): + # installing a module that creates a job function: rebinds the record + # to an existing one (likely we already had the job function created by + # the @job decorator previously) + new_vals_list = [] + for vals in vals_list: + name = vals.get("name") + if name: + existing = self.search([("name", "=", name)], limit=1) + if existing: + if not existing.get_metadata()[0].get("noupdate"): + existing.write(vals) + records |= existing + continue + new_vals_list.append(vals) + vals_list = new_vals_list + records |= super().create(vals_list) + self.env.registry.clear_cache() + return records + + def write(self, values): + res = super().write(values) + self.env.registry.clear_cache() + return res + + def unlink(self): + res = super().unlink() + self.env.registry.clear_cache() + return res diff --git a/queue_job/post_init_hook.py b/queue_job/post_init_hook.py new file mode 100644 index 0000000000..c0c807a291 --- /dev/null +++ b/queue_job/post_init_hook.py @@ -0,0 +1,33 @@ +# Copyright 2020 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +logger = logging.getLogger(__name__) + + +def post_init_hook(env): + # this is the trigger that sends notifications when jobs change + logger.info("Create queue_job_notify trigger") + env.cr.execute( + """ + DROP TRIGGER IF EXISTS queue_job_notify ON queue_job; + CREATE OR REPLACE + FUNCTION queue_job_notify() RETURNS trigger AS $$ + BEGIN + IF TG_OP = 'DELETE' THEN + IF OLD.state != 'done' THEN + PERFORM pg_notify('queue_job', OLD.uuid); + END IF; + ELSE + PERFORM pg_notify('queue_job', NEW.uuid); + END IF; + RETURN NULL; + END; + $$ LANGUAGE plpgsql; + CREATE TRIGGER queue_job_notify + AFTER INSERT OR UPDATE OR DELETE + ON queue_job + FOR EACH ROW EXECUTE PROCEDURE queue_job_notify(); + """ + ) diff --git a/queue_job/post_load.py b/queue_job/post_load.py new file mode 100644 index 0000000000..f0c1df870f --- /dev/null +++ b/queue_job/post_load.py @@ -0,0 +1,25 @@ +import logging + +from odoo import http + +_logger = logging.getLogger(__name__) + + +def post_load(): + _logger.info( + "Apply Request._get_session_and_dbname monkey patch to capture db" + " from request with multiple databases" + ) + _get_session_and_dbname_orig = http.Request._get_session_and_dbname + + def _get_session_and_dbname(self): + session, dbname = _get_session_and_dbname_orig(self) + if ( + not dbname + and self.httprequest.path == "/queue_job/runjob" + and self.httprequest.args.get("db") + ): + dbname = self.httprequest.args["db"] + return session, dbname + + http.Request._get_session_and_dbname = _get_session_and_dbname diff --git a/queue_job/pyproject.toml b/queue_job/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/queue_job/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/queue_job/readme/CONFIGURE.md b/queue_job/readme/CONFIGURE.md new file mode 100644 index 0000000000..07b7b84126 --- /dev/null +++ b/queue_job/readme/CONFIGURE.md @@ -0,0 +1,37 @@ +- Using environment variables and command line: + - Adjust environment variables (optional): + - `ODOO_QUEUE_JOB_CHANNELS=root:4` or any other channels + configuration. The default is `root:1` + - if `xmlrpc_port` is not set: `ODOO_QUEUE_JOB_PORT=8069` + - Start Odoo with `--load=web,queue_job` and `--workers` greater than + 1.[^1] +- Using the Odoo configuration file: + +``` ini +[options] +(...) +workers = 6 +server_wide_modules = web,queue_job + +(...) +[queue_job] +channels = root:2 +``` + +- Confirm the runner is starting correctly by checking the odoo log + file: + +``` +...INFO...queue_job.jobrunner.runner: starting +...INFO...queue_job.jobrunner.runner: initializing database connections +...INFO...queue_job.jobrunner.runner: queue job runner ready for db +...INFO...queue_job.jobrunner.runner: database connections ready +``` + +- Create jobs (eg using `base_import_async`) and observe they start + immediately and in parallel. +- Tip: to enable debug logging for the queue job, use + `--log-handler=odoo.addons.queue_job:DEBUG` + +[^1]: It works with the threaded Odoo server too, although this way of + running Odoo is obviously not for production purposes. diff --git a/queue_job/readme/CONTRIBUTORS.md b/queue_job/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..9f92cfb5a6 --- /dev/null +++ b/queue_job/readme/CONTRIBUTORS.md @@ -0,0 +1,15 @@ +- Guewen Baconnier \<\> +- Stéphane Bidoul \<\> +- Matthieu Dietrich \<\> +- Jos De Graeve \<\> +- David Lefever \<\> +- Laurent Mignon \<\> +- Laetitia Gangloff \<\> +- Cédric Pigeon \<\> +- Tatiana Deribina \<\> +- Souheil Bejaoui \<\> +- Eric Antones \<\> +- Simone Orsi \<\> +- Nguyen Minh Chien \<\> +- Tran Quoc Duong \<> +- Vo Hong Thien \<> diff --git a/queue_job/readme/CREDITS.md b/queue_job/readme/CREDITS.md new file mode 100644 index 0000000000..83b3ec91f7 --- /dev/null +++ b/queue_job/readme/CREDITS.md @@ -0,0 +1 @@ +The migration of this module from 17.0 to 18.0 was financially supported by Camptocamp. diff --git a/queue_job/readme/DESCRIPTION.md b/queue_job/readme/DESCRIPTION.md new file mode 100644 index 0000000000..8617673ff3 --- /dev/null +++ b/queue_job/readme/DESCRIPTION.md @@ -0,0 +1,47 @@ +This addon adds an integrated Job Queue to Odoo. + +It allows to postpone method calls executed asynchronously. + +Jobs are executed in the background by a `Jobrunner`, in their own +transaction. + +Example: + +``` python +from odoo import models, fields, api + +class MyModel(models.Model): + _name = 'my.model' + + def my_method(self, a, k=None): + _logger.info('executed with a: %s and k: %s', a, k) + + +class MyOtherModel(models.Model): + _name = 'my.other.model' + + def button_do_stuff(self): + self.env['my.model'].with_delay().my_method('a', k=2) +``` + +In the snippet of code above, when we call `button_do_stuff`, a job +**capturing the method and arguments** will be postponed. It will be +executed as soon as the Jobrunner has a free bucket, which can be +instantaneous if no other job is running. + +Features: + +- Views for jobs, jobs are stored in PostgreSQL +- Jobrunner: execute the jobs, highly efficient thanks to PostgreSQL's + NOTIFY +- Channels: give a capacity for the root channel and its sub-channels + and segregate jobs in them. Allow for instance to restrict heavy jobs + to be executed one at a time while little ones are executed 4 at a + times. +- Retries: Ability to retry jobs by raising a type of exception +- Retry Pattern: the 3 first tries, retry after 10 seconds, the 5 next + tries, retry after 1 minutes, ... +- Job properties: priorities, estimated time of arrival (ETA), custom + description, number of retries +- Related Actions: link an action on the job view, such as open the + record concerned by the job diff --git a/queue_job/readme/HISTORY.md b/queue_job/readme/HISTORY.md new file mode 100644 index 0000000000..e1f251392a --- /dev/null +++ b/queue_job/readme/HISTORY.md @@ -0,0 +1,7 @@ +## Next + +- \[ADD\] Run jobrunner as a worker process instead of a thread in the + main process (when running with --workers \> 0) +- \[REF\] `@job` and `@related_action` deprecated, any method can be + delayed, and configured using `queue.job.function` records +- \[MIGRATION\] from 13.0 branched at rev. e24ff4b diff --git a/queue_job/readme/INSTALL.md b/queue_job/readme/INSTALL.md new file mode 100644 index 0000000000..cdf9b90b52 --- /dev/null +++ b/queue_job/readme/INSTALL.md @@ -0,0 +1 @@ +Be sure to have the `requests` library. diff --git a/queue_job/readme/ROADMAP.md b/queue_job/readme/ROADMAP.md new file mode 100644 index 0000000000..a13be6beb3 --- /dev/null +++ b/queue_job/readme/ROADMAP.md @@ -0,0 +1,17 @@ +- After creating a new database or installing `queue_job` on an existing + database, Odoo must be restarted for the runner to detect it. +- When Odoo shuts down normally, it waits for running jobs to finish. + However, when the Odoo server crashes or is otherwise force-stopped, + running jobs are interrupted while the runner has no chance to know + they have been aborted. In such situations, jobs may remain in + `started` or `enqueued` state after the Odoo server is halted. Since + the runner has no way to know if they are actually running or not, and + does not know for sure if it is safe to restart the jobs, it does not + attempt to restart them automatically. Such stale jobs therefore fill + the running queue and prevent other jobs to start. You must therefore + requeue them manually, either from the Jobs view, or by running the + following SQL statement *before starting Odoo*: + +``` sql +update queue_job set state='pending' where state in ('started', 'enqueued') +``` diff --git a/queue_job/readme/USAGE.md b/queue_job/readme/USAGE.md new file mode 100644 index 0000000000..fb160bfa48 --- /dev/null +++ b/queue_job/readme/USAGE.md @@ -0,0 +1,436 @@ +To use this module, you need to: + +1. Go to `Job Queue` menu + +## Developers + +### Delaying jobs + +The fast way to enqueue a job for a method is to use `with_delay()` on a +record or model: + +``` python +def button_done(self): + self.with_delay().print_confirmation_document(self.state) + self.write({"state": "done"}) + return True +``` + +Here, the method `print_confirmation_document()` will be executed +asynchronously as a job. `with_delay()` can take several parameters to +define more precisely how the job is executed (priority, ...). + +All the arguments passed to the method being delayed are stored in the +job and passed to the method when it is executed asynchronously, +including `self`, so the current record is maintained during the job +execution (warning: the context is not kept). + +Dependencies can be expressed between jobs. To start a graph of jobs, +use `delayable()` on a record or model. The following is the equivalent +of `with_delay()` but using the long form: + +``` python +def button_done(self): + delayable = self.delayable() + delayable.print_confirmation_document(self.state) + delayable.delay() + self.write({"state": "done"}) + return True +``` + +Methods of Delayable objects return itself, so it can be used as a +builder pattern, which in some cases allow to build the jobs +dynamically: + +``` python +def button_generate_simple_with_delayable(self): + self.ensure_one() + # Introduction of a delayable object, using a builder pattern + # allowing to chain jobs or set properties. The delay() method + # on the delayable object actually stores the delayable objects + # in the queue_job table + ( + self.delayable() + .generate_thumbnail((50, 50)) + .set(priority=30) + .set(description=_("generate xxx")) + .delay() + ) +``` + +The simplest way to define a dependency is to use `.on_done(job)` on a +Delayable: + +``` python +def button_chain_done(self): + self.ensure_one() + job1 = self.browse(1).delayable().generate_thumbnail((50, 50)) + job2 = self.browse(1).delayable().generate_thumbnail((50, 50)) + job3 = self.browse(1).delayable().generate_thumbnail((50, 50)) + # job 3 is executed when job 2 is done which is executed when job 1 is done + job1.on_done(job2.on_done(job3)).delay() +``` + +Delayables can be chained to form more complex graphs using the +`chain()` and `group()` primitives. A chain represents a sequence of +jobs to execute in order, a group represents jobs which can be executed +in parallel. Using `chain()` has the same effect as using several nested +`on_done()` but is more readable. Both can be combined to form a graph, +for instance we can group \[A\] of jobs, which blocks another group +\[B\] of jobs. When and only when all the jobs of the group \[A\] are +executed, the jobs of the group \[B\] are executed. The code would look +like: + +``` python +from odoo.addons.queue_job.delay import group, chain + +def button_done(self): + group_a = group(self.delayable().method_foo(), self.delayable().method_bar()) + group_b = group(self.delayable().method_baz(1), self.delayable().method_baz(2)) + chain(group_a, group_b).delay() + self.write({"state": "done"}) + return True +``` + +When a failure happens in a graph of jobs, the execution of the jobs +that depend on the failed job stops. They remain in a state +`wait_dependencies` until their "parent" job is successful. This can +happen in two ways: either the parent job retries and is successful on a +second try, either the parent job is manually "set to done" by a user. +In these two cases, the dependency is resolved and the graph will +continue to be processed. Alternatively, the failed job and all its +dependent jobs can be canceled by a user. The other jobs of the graph +that do not depend on the failed job continue their execution in any +case. + +Note: `delay()` must be called on the delayable, chain, or group which +is at the top of the graph. In the example above, if it was called on +`group_a`, then `group_b` would never be delayed (but a warning would be +shown). + +### Enqueing Job Options + +- priority: default is 10, the closest it is to 0, the faster it will be + executed +- eta: Estimated Time of Arrival of the job. It will not be executed + before this date/time +- max_retries: default is 5, maximum number of retries before giving up + and set the job state to 'failed'. A value of 0 means infinite + retries. +- description: human description of the job. If not set, description is + computed from the function doc or method name +- channel: the complete name of the channel to use to process the + function. If specified it overrides the one defined on the function +- identity_key: key uniquely identifying the job, if specified and a job + with the same key has not yet been run, the new job will not be + created + +### Configure default options for jobs + +In earlier versions, jobs could be configured using the `@job` +decorator. This is now obsolete, they can be configured using optional +`queue.job.function` and `queue.job.channel` XML records. + +Example of channel: + +``` XML + + sale + + +``` + +Example of job function: + +``` XML + + + action_done + + + + +``` + +The general form for the `name` is: `.method`. + +The channel, related action and retry pattern options are optional, they +are documented below. + +When writing modules, if 2+ modules add a job function or channel with +the same name (and parent for channels), they'll be merged in the same +record, even if they have different xmlids. On uninstall, the merged +record is deleted when all the modules using it are uninstalled. + +**Job function: model** + +If the function is defined in an abstract model, you can not write +`` but +you have to define a function for each model that inherits from the +abstract model. + +**Job function: channel** + +The channel where the job will be delayed. The default channel is +`root`. + +**Job function: related action** + +The *Related Action* appears as a button on the Job's view. The button +will execute the defined action. + +The default one is to open the view of the record related to the job +(form view when there is a single record, list view for several +records). In many cases, the default related action is enough and +doesn't need customization, but it can be customized by providing a +dictionary on the job function: + +``` python +{ + "enable": False, + "func_name": "related_action_partner", + "kwargs": {"name": "Partner"}, +} +``` + +- `enable`: when `False`, the button has no effect (default: `True`) +- `func_name`: name of the method on `queue.job` that returns an action +- `kwargs`: extra arguments to pass to the related action method + +Example of related action code: + +``` python +class QueueJob(models.Model): + _inherit = 'queue.job' + + def related_action_partner(self, name): + self.ensure_one() + model = self.model_name + partner = self.records + action = { + 'name': name, + 'type': 'ir.actions.act_window', + 'res_model': model, + 'view_type': 'form', + 'view_mode': 'form', + 'res_id': partner.id, + } + return action +``` + +**Job function: retry pattern** + +When a job fails with a retryable error type, it is automatically +retried later. By default, the retry is always 10 minutes later. + +A retry pattern can be configured on the job function. What a pattern +represents is "from X tries, postpone to Y seconds". It is expressed as +a dictionary where keys are tries and values are seconds to postpone as +integers: + +``` python +{ + 1: 10, + 5: 20, + 10: 30, + 15: 300, +} +``` + +Based on this configuration, we can tell that: + +- 5 first retries are postponed 10 seconds later +- retries 5 to 10 postponed 20 seconds later +- retries 10 to 15 postponed 30 seconds later +- all subsequent retries postponed 5 minutes later + +**Job Context** + +The context of the recordset of the job, or any recordset passed in +arguments of a job, is transferred to the job according to an +allow-list. + +The default allow-list is ("tz", "lang", "allowed_company_ids", +"force_company", "active_test"). It can be customized in +`Base._job_prepare_context_before_enqueue_keys`. **Bypass jobs on +running Odoo** + +When you are developing (ie: connector modules) you might want to bypass +the queue job and run your code immediately. + +To do so you can set QUEUE_JOB\_\_NO_DELAY=1 in your enviroment. + +**Bypass jobs in tests** + +When writing tests on job-related methods is always tricky to deal with +delayed recordsets. To make your testing life easier you can set +queue_job\_\_no_delay=True in the context. + +Tip: you can do this at test case level like this + +``` python +@classmethod +def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict( + cls.env.context, + queue_job__no_delay=True, # no jobs thanks + )) +``` + +Then all your tests execute the job methods synchronously without +delaying any jobs. + +### Testing + +**Asserting enqueued jobs** + +The recommended way to test jobs, rather than running them directly and +synchronously is to split the tests in two parts: + +> - one test where the job is mocked (trap jobs with `trap_jobs()` and +> the test only verifies that the job has been delayed with the +> expected arguments +> - one test that only calls the method of the job synchronously, to +> validate the proper behavior of this method only + +Proceeding this way means that you can prove that jobs will be enqueued +properly at runtime, and it ensures your code does not have a different +behavior in tests and in production (because running your jobs +synchronously may have a different behavior as they are in the same +transaction / in the middle of the method). Additionally, it gives more +control on the arguments you want to pass when calling the job's method +(synchronously, this time, in the second type of tests), and it makes +tests smaller. + +The best way to run such assertions on the enqueued jobs is to use +`odoo.addons.queue_job.tests.common.trap_jobs()`. + +A very small example (more details in `tests/common.py`): + +``` python +# code +def my_job_method(self, name, count): + self.write({"name": " ".join([name] * count) + +def method_to_test(self): + count = self.env["other.model"].search_count([]) + self.with_delay(priority=15).my_job_method("Hi!", count=count) + return count + +# tests +from odoo.addons.queue_job.tests.common import trap_jobs + +# first test only check the expected behavior of the method and the proper +# enqueuing of jobs +def test_method_to_test(self): + with trap_jobs() as trap: + result = self.env["model"].method_to_test() + expected_count = 12 + + trap.assert_jobs_count(1, only=self.env["model"].my_job_method) + trap.assert_enqueued_job( + self.env["model"].my_job_method, + args=("Hi!",), + kwargs=dict(count=expected_count), + properties=dict(priority=15) + ) + self.assertEqual(result, expected_count) + + + # second test to validate the behavior of the job unitarily + def test_my_job_method(self): + record = self.env["model"].browse(1) + record.my_job_method("Hi!", count=12) + self.assertEqual(record.name, "Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi!") +``` + +If you prefer, you can still test the whole thing in a single test, by +calling `jobs_tester.perform_enqueued_jobs()` in your test. + +``` python +def test_method_to_test(self): + with trap_jobs() as trap: + result = self.env["model"].method_to_test() + expected_count = 12 + + trap.assert_jobs_count(1, only=self.env["model"].my_job_method) + trap.assert_enqueued_job( + self.env["model"].my_job_method, + args=("Hi!",), + kwargs=dict(count=expected_count), + properties=dict(priority=15) + ) + self.assertEqual(result, expected_count) + + trap.perform_enqueued_jobs() + + record = self.env["model"].browse(1) + record.my_job_method("Hi!", count=12) + self.assertEqual(record.name, "Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi!") +``` + +**Execute jobs synchronously when running Odoo** + +When you are developing (ie: connector modules) you might want to bypass +the queue job and run your code immediately. + +To do so you can set `QUEUE_JOB__NO_DELAY=1` in your environment. + +Warning + +Do not do this in production + +**Execute jobs synchronously in tests** + +You should use `trap_jobs`, really, but if for any reason you could not +use it, and still need to have job methods executed synchronously in +your tests, you can do so by setting `queue_job__no_delay=True` in the +context. + +Tip: you can do this at test case level like this + +``` python +@classmethod +def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict( + cls.env.context, + queue_job__no_delay=True, # no jobs thanks + )) +``` + +Then all your tests execute the job methods synchronously without +delaying any jobs. + +In tests you'll have to mute the logger like: + +> @mute_logger('odoo.addons.queue_job.models.base') + +Note + +in graphs of jobs, the `queue_job__no_delay` context key must be in at +least one job's env of the graph for the whole graph to be executed +synchronously + +### Tips and tricks + +- **Idempotency** + (): The + queue_job should be idempotent so they can be retried several times + without impact on the data. +- **The job should test at the very beginning its relevance**: the + moment the job will be executed is unknown by design. So the first + task of a job should be to check if the related work is still relevant + at the moment of the execution. + +### Patterns + +Through the time, two main patterns emerged: + +1. For data exposed to users, a model should store the data and the + model should be the creator of the job. The job is kept hidden from + the users +2. For technical data, that are not exposed to the users, it is + generally alright to create directly jobs with data passed as + arguments to the job, without intermediary models. diff --git a/queue_job/security/ir.model.access.csv b/queue_job/security/ir.model.access.csv new file mode 100644 index 0000000000..634daf8ede --- /dev/null +++ b/queue_job/security/ir.model.access.csv @@ -0,0 +1,7 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_queue_job_manager,queue job manager,queue_job.model_queue_job,queue_job.group_queue_job_manager,1,1,1,1 +access_queue_job_function_manager,queue job functions manager,queue_job.model_queue_job_function,queue_job.group_queue_job_manager,1,1,1,1 +access_queue_job_channel_manager,queue job channel manager,queue_job.model_queue_job_channel,queue_job.group_queue_job_manager,1,1,1,1 +access_queue_requeue_job,queue requeue job manager,queue_job.model_queue_requeue_job,queue_job.group_queue_job_manager,1,1,1,1 +access_queue_jobs_to_done,queue jobs to done manager,queue_job.model_queue_jobs_to_done,queue_job.group_queue_job_manager,1,1,1,1 +access_queue_jobs_to_cancelled,queue jobs to cancelled manager,queue_job.model_queue_jobs_to_cancelled,queue_job.group_queue_job_manager,1,1,1,1 diff --git a/queue_job/security/security.xml b/queue_job/security/security.xml new file mode 100644 index 0000000000..947644e95c --- /dev/null +++ b/queue_job/security/security.xml @@ -0,0 +1,27 @@ + + + + + Job Queue + 20 + + + Job Queue Manager + + + + + + + Job Queue multi-company + + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + + diff --git a/queue_job/static/description/icon.png b/queue_job/static/description/icon.png new file mode 100644 index 0000000000..c3bbca6d3d Binary files /dev/null and b/queue_job/static/description/icon.png differ diff --git a/queue_job/static/description/icon.svg b/queue_job/static/description/icon.svg new file mode 100644 index 0000000000..a0a6afb414 --- /dev/null +++ b/queue_job/static/description/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/queue_job/static/description/index.html b/queue_job/static/description/index.html new file mode 100644 index 0000000000..7c54db9f84 --- /dev/null +++ b/queue_job/static/description/index.html @@ -0,0 +1,983 @@ + + + + + +Job Queue + + + +
+

Job Queue

+ + +

Mature License: LGPL-3 OCA/queue Translate me on Weblate Try me on Runboat

+

This addon adds an integrated Job Queue to Odoo.

+

It allows to postpone method calls executed asynchronously.

+

Jobs are executed in the background by a Jobrunner, in their own +transaction.

+

Example:

+
+from odoo import models, fields, api
+
+class MyModel(models.Model):
+   _name = 'my.model'
+
+   def my_method(self, a, k=None):
+       _logger.info('executed with a: %s and k: %s', a, k)
+
+
+class MyOtherModel(models.Model):
+    _name = 'my.other.model'
+
+    def button_do_stuff(self):
+        self.env['my.model'].with_delay().my_method('a', k=2)
+
+

In the snippet of code above, when we call button_do_stuff, a job +capturing the method and arguments will be postponed. It will be +executed as soon as the Jobrunner has a free bucket, which can be +instantaneous if no other job is running.

+

Features:

+
    +
  • Views for jobs, jobs are stored in PostgreSQL
  • +
  • Jobrunner: execute the jobs, highly efficient thanks to PostgreSQL’s +NOTIFY
  • +
  • Channels: give a capacity for the root channel and its sub-channels +and segregate jobs in them. Allow for instance to restrict heavy jobs +to be executed one at a time while little ones are executed 4 at a +times.
  • +
  • Retries: Ability to retry jobs by raising a type of exception
  • +
  • Retry Pattern: the 3 first tries, retry after 10 seconds, the 5 next +tries, retry after 1 minutes, …
  • +
  • Job properties: priorities, estimated time of arrival (ETA), custom +description, number of retries
  • +
  • Related Actions: link an action on the job view, such as open the +record concerned by the job
  • +
+

Table of contents

+ +
+

Installation

+

Be sure to have the requests library.

+
+
+

Configuration

+
    +
  • Using environment variables and command line:
      +
    • Adjust environment variables (optional):
        +
      • ODOO_QUEUE_JOB_CHANNELS=root:4 or any other channels +configuration. The default is root:1
      • +
      • if xmlrpc_port is not set: ODOO_QUEUE_JOB_PORT=8069
      • +
      +
    • +
    • Start Odoo with --load=web,queue_job and --workers greater +than 1. [1]
    • +
    +
  • +
  • Using the Odoo configuration file:
  • +
+
+[options]
+(...)
+workers = 6
+server_wide_modules = web,queue_job
+
+(...)
+[queue_job]
+channels = root:2
+
+
    +
  • Confirm the runner is starting correctly by checking the odoo log +file:
  • +
+
+...INFO...queue_job.jobrunner.runner: starting
+...INFO...queue_job.jobrunner.runner: initializing database connections
+...INFO...queue_job.jobrunner.runner: queue job runner ready for db <dbname>
+...INFO...queue_job.jobrunner.runner: database connections ready
+
+
    +
  • Create jobs (eg using base_import_async) and observe they start +immediately and in parallel.
  • +
  • Tip: to enable debug logging for the queue job, use +--log-handler=odoo.addons.queue_job:DEBUG
  • +
+ + + + + +
[1]It works with the threaded Odoo server too, although this way of +running Odoo is obviously not for production purposes.
+
+
+

Usage

+

To use this module, you need to:

+
    +
  1. Go to Job Queue menu
  2. +
+
+

Developers

+
+

Delaying jobs

+

The fast way to enqueue a job for a method is to use with_delay() on +a record or model:

+
+def button_done(self):
+    self.with_delay().print_confirmation_document(self.state)
+    self.write({"state": "done"})
+    return True
+
+

Here, the method print_confirmation_document() will be executed +asynchronously as a job. with_delay() can take several parameters to +define more precisely how the job is executed (priority, …).

+

All the arguments passed to the method being delayed are stored in the +job and passed to the method when it is executed asynchronously, +including self, so the current record is maintained during the job +execution (warning: the context is not kept).

+

Dependencies can be expressed between jobs. To start a graph of jobs, +use delayable() on a record or model. The following is the +equivalent of with_delay() but using the long form:

+
+def button_done(self):
+    delayable = self.delayable()
+    delayable.print_confirmation_document(self.state)
+    delayable.delay()
+    self.write({"state": "done"})
+    return True
+
+

Methods of Delayable objects return itself, so it can be used as a +builder pattern, which in some cases allow to build the jobs +dynamically:

+
+def button_generate_simple_with_delayable(self):
+    self.ensure_one()
+    # Introduction of a delayable object, using a builder pattern
+    # allowing to chain jobs or set properties. The delay() method
+    # on the delayable object actually stores the delayable objects
+    # in the queue_job table
+    (
+        self.delayable()
+        .generate_thumbnail((50, 50))
+        .set(priority=30)
+        .set(description=_("generate xxx"))
+        .delay()
+    )
+
+

The simplest way to define a dependency is to use .on_done(job) on a +Delayable:

+
+def button_chain_done(self):
+    self.ensure_one()
+    job1 = self.browse(1).delayable().generate_thumbnail((50, 50))
+    job2 = self.browse(1).delayable().generate_thumbnail((50, 50))
+    job3 = self.browse(1).delayable().generate_thumbnail((50, 50))
+    # job 3 is executed when job 2 is done which is executed when job 1 is done
+    job1.on_done(job2.on_done(job3)).delay()
+
+

Delayables can be chained to form more complex graphs using the +chain() and group() primitives. A chain represents a sequence of +jobs to execute in order, a group represents jobs which can be executed +in parallel. Using chain() has the same effect as using several +nested on_done() but is more readable. Both can be combined to form +a graph, for instance we can group [A] of jobs, which blocks another +group [B] of jobs. When and only when all the jobs of the group [A] are +executed, the jobs of the group [B] are executed. The code would look +like:

+
+from odoo.addons.queue_job.delay import group, chain
+
+def button_done(self):
+    group_a = group(self.delayable().method_foo(), self.delayable().method_bar())
+    group_b = group(self.delayable().method_baz(1), self.delayable().method_baz(2))
+    chain(group_a, group_b).delay()
+    self.write({"state": "done"})
+    return True
+
+

When a failure happens in a graph of jobs, the execution of the jobs +that depend on the failed job stops. They remain in a state +wait_dependencies until their “parent” job is successful. This can +happen in two ways: either the parent job retries and is successful on a +second try, either the parent job is manually “set to done” by a user. +In these two cases, the dependency is resolved and the graph will +continue to be processed. Alternatively, the failed job and all its +dependent jobs can be canceled by a user. The other jobs of the graph +that do not depend on the failed job continue their execution in any +case.

+

Note: delay() must be called on the delayable, chain, or group which +is at the top of the graph. In the example above, if it was called on +group_a, then group_b would never be delayed (but a warning +would be shown).

+
+
+

Enqueing Job Options

+
    +
  • priority: default is 10, the closest it is to 0, the faster it will +be executed
  • +
  • eta: Estimated Time of Arrival of the job. It will not be executed +before this date/time
  • +
  • max_retries: default is 5, maximum number of retries before giving up +and set the job state to ‘failed’. A value of 0 means infinite +retries.
  • +
  • description: human description of the job. If not set, description is +computed from the function doc or method name
  • +
  • channel: the complete name of the channel to use to process the +function. If specified it overrides the one defined on the function
  • +
  • identity_key: key uniquely identifying the job, if specified and a +job with the same key has not yet been run, the new job will not be +created
  • +
+
+
+

Configure default options for jobs

+

In earlier versions, jobs could be configured using the @job +decorator. This is now obsolete, they can be configured using optional +queue.job.function and queue.job.channel XML records.

+

Example of channel:

+
+<record id="channel_sale" model="queue.job.channel">
+    <field name="name">sale</field>
+    <field name="parent_id" ref="queue_job.channel_root" />
+</record>
+
+

Example of job function:

+
+<record id="job_function_sale_order_action_done" model="queue.job.function">
+    <field name="model_id" ref="sale.model_sale_order" />
+    <field name="method">action_done</field>
+    <field name="channel_id" ref="channel_sale" />
+    <field name="related_action" eval='{"func_name": "custom_related_action"}' />
+    <field name="retry_pattern" eval="{1: 60, 2: 180, 3: 10, 5: 300}" />
+</record>
+
+

The general form for the name is: <model.name>.method.

+

The channel, related action and retry pattern options are optional, they +are documented below.

+

When writing modules, if 2+ modules add a job function or channel with +the same name (and parent for channels), they’ll be merged in the same +record, even if they have different xmlids. On uninstall, the merged +record is deleted when all the modules using it are uninstalled.

+

Job function: model

+

If the function is defined in an abstract model, you can not write +<field name="model_id" ref="xml_id_of_the_abstract_model"</field> +but you have to define a function for each model that inherits from the +abstract model.

+

Job function: channel

+

The channel where the job will be delayed. The default channel is +root.

+

Job function: related action

+

The Related Action appears as a button on the Job’s view. The button +will execute the defined action.

+

The default one is to open the view of the record related to the job +(form view when there is a single record, list view for several +records). In many cases, the default related action is enough and +doesn’t need customization, but it can be customized by providing a +dictionary on the job function:

+
+{
+    "enable": False,
+    "func_name": "related_action_partner",
+    "kwargs": {"name": "Partner"},
+}
+
+
    +
  • enable: when False, the button has no effect (default: +True)
  • +
  • func_name: name of the method on queue.job that returns an +action
  • +
  • kwargs: extra arguments to pass to the related action method
  • +
+

Example of related action code:

+
+class QueueJob(models.Model):
+    _inherit = 'queue.job'
+
+    def related_action_partner(self, name):
+        self.ensure_one()
+        model = self.model_name
+        partner = self.records
+        action = {
+            'name': name,
+            'type': 'ir.actions.act_window',
+            'res_model': model,
+            'view_type': 'form',
+            'view_mode': 'form',
+            'res_id': partner.id,
+        }
+        return action
+
+

Job function: retry pattern

+

When a job fails with a retryable error type, it is automatically +retried later. By default, the retry is always 10 minutes later.

+

A retry pattern can be configured on the job function. What a pattern +represents is “from X tries, postpone to Y seconds”. It is expressed as +a dictionary where keys are tries and values are seconds to postpone as +integers:

+
+{
+    1: 10,
+    5: 20,
+    10: 30,
+    15: 300,
+}
+
+

Based on this configuration, we can tell that:

+
    +
  • 5 first retries are postponed 10 seconds later
  • +
  • retries 5 to 10 postponed 20 seconds later
  • +
  • retries 10 to 15 postponed 30 seconds later
  • +
  • all subsequent retries postponed 5 minutes later
  • +
+

Job Context

+

The context of the recordset of the job, or any recordset passed in +arguments of a job, is transferred to the job according to an +allow-list.

+

The default allow-list is (“tz”, “lang”, “allowed_company_ids”, +“force_company”, “active_test”). It can be customized in +Base._job_prepare_context_before_enqueue_keys. Bypass jobs on +running Odoo

+

When you are developing (ie: connector modules) you might want to bypass +the queue job and run your code immediately.

+

To do so you can set QUEUE_JOB__NO_DELAY=1 in your enviroment.

+

Bypass jobs in tests

+

When writing tests on job-related methods is always tricky to deal with +delayed recordsets. To make your testing life easier you can set +queue_job__no_delay=True in the context.

+

Tip: you can do this at test case level like this

+
+@classmethod
+def setUpClass(cls):
+    super().setUpClass()
+    cls.env = cls.env(context=dict(
+        cls.env.context,
+        queue_job__no_delay=True,  # no jobs thanks
+    ))
+
+

Then all your tests execute the job methods synchronously without +delaying any jobs.

+
+
+

Testing

+

Asserting enqueued jobs

+

The recommended way to test jobs, rather than running them directly and +synchronously is to split the tests in two parts:

+
+
    +
  • one test where the job is mocked (trap jobs with trap_jobs() +and the test only verifies that the job has been delayed with the +expected arguments
  • +
  • one test that only calls the method of the job synchronously, to +validate the proper behavior of this method only
  • +
+
+

Proceeding this way means that you can prove that jobs will be enqueued +properly at runtime, and it ensures your code does not have a different +behavior in tests and in production (because running your jobs +synchronously may have a different behavior as they are in the same +transaction / in the middle of the method). Additionally, it gives more +control on the arguments you want to pass when calling the job’s method +(synchronously, this time, in the second type of tests), and it makes +tests smaller.

+

The best way to run such assertions on the enqueued jobs is to use +odoo.addons.queue_job.tests.common.trap_jobs().

+

A very small example (more details in tests/common.py):

+
+# code
+def my_job_method(self, name, count):
+    self.write({"name": " ".join([name] * count)
+
+def method_to_test(self):
+    count = self.env["other.model"].search_count([])
+    self.with_delay(priority=15).my_job_method("Hi!", count=count)
+    return count
+
+# tests
+from odoo.addons.queue_job.tests.common import trap_jobs
+
+# first test only check the expected behavior of the method and the proper
+# enqueuing of jobs
+def test_method_to_test(self):
+    with trap_jobs() as trap:
+        result = self.env["model"].method_to_test()
+        expected_count = 12
+
+        trap.assert_jobs_count(1, only=self.env["model"].my_job_method)
+        trap.assert_enqueued_job(
+            self.env["model"].my_job_method,
+            args=("Hi!",),
+            kwargs=dict(count=expected_count),
+            properties=dict(priority=15)
+        )
+        self.assertEqual(result, expected_count)
+
+
+ # second test to validate the behavior of the job unitarily
+ def test_my_job_method(self):
+     record = self.env["model"].browse(1)
+     record.my_job_method("Hi!", count=12)
+     self.assertEqual(record.name, "Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi!")
+
+

If you prefer, you can still test the whole thing in a single test, by +calling jobs_tester.perform_enqueued_jobs() in your test.

+
+def test_method_to_test(self):
+    with trap_jobs() as trap:
+        result = self.env["model"].method_to_test()
+        expected_count = 12
+
+        trap.assert_jobs_count(1, only=self.env["model"].my_job_method)
+        trap.assert_enqueued_job(
+            self.env["model"].my_job_method,
+            args=("Hi!",),
+            kwargs=dict(count=expected_count),
+            properties=dict(priority=15)
+        )
+        self.assertEqual(result, expected_count)
+
+        trap.perform_enqueued_jobs()
+
+        record = self.env["model"].browse(1)
+        record.my_job_method("Hi!", count=12)
+        self.assertEqual(record.name, "Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi!")
+
+

Execute jobs synchronously when running Odoo

+

When you are developing (ie: connector modules) you might want to bypass +the queue job and run your code immediately.

+

To do so you can set QUEUE_JOB__NO_DELAY=1 in your environment.

+

Warning

+

Do not do this in production

+

Execute jobs synchronously in tests

+

You should use trap_jobs, really, but if for any reason you could +not use it, and still need to have job methods executed synchronously in +your tests, you can do so by setting queue_job__no_delay=True in the +context.

+

Tip: you can do this at test case level like this

+
+@classmethod
+def setUpClass(cls):
+    super().setUpClass()
+    cls.env = cls.env(context=dict(
+        cls.env.context,
+        queue_job__no_delay=True,  # no jobs thanks
+    ))
+
+

Then all your tests execute the job methods synchronously without +delaying any jobs.

+

In tests you’ll have to mute the logger like:

+
+@mute_logger(‘odoo.addons.queue_job.models.base’)
+

Note

+

in graphs of jobs, the queue_job__no_delay context key must be in at +least one job’s env of the graph for the whole graph to be executed +synchronously

+
+
+

Tips and tricks

+
    +
  • Idempotency +(https://www.restapitutorial.com/lessons/idempotency.html): The +queue_job should be idempotent so they can be retried several times +without impact on the data.
  • +
  • The job should test at the very beginning its relevance: the +moment the job will be executed is unknown by design. So the first +task of a job should be to check if the related work is still +relevant at the moment of the execution.
  • +
+
+
+

Patterns

+

Through the time, two main patterns emerged:

+
    +
  1. For data exposed to users, a model should store the data and the +model should be the creator of the job. The job is kept hidden from +the users
  2. +
  3. For technical data, that are not exposed to the users, it is +generally alright to create directly jobs with data passed as +arguments to the job, without intermediary models.
  4. +
+
+
+
+
+

Known issues / Roadmap

+
    +
  • After creating a new database or installing queue_job on an +existing database, Odoo must be restarted for the runner to detect +it.
  • +
  • When Odoo shuts down normally, it waits for running jobs to finish. +However, when the Odoo server crashes or is otherwise force-stopped, +running jobs are interrupted while the runner has no chance to know +they have been aborted. In such situations, jobs may remain in +started or enqueued state after the Odoo server is halted. +Since the runner has no way to know if they are actually running or +not, and does not know for sure if it is safe to restart the jobs, it +does not attempt to restart them automatically. Such stale jobs +therefore fill the running queue and prevent other jobs to start. You +must therefore requeue them manually, either from the Jobs view, or +by running the following SQL statement before starting Odoo:
  • +
+
+update queue_job set state='pending' where state in ('started', 'enqueued')
+
+
+
+

Changelog

+ +
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
  • ACSONE SA/NV
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+

The migration of this module from 17.0 to 18.0 was financially supported +by Camptocamp.

+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

guewen

+

This module is part of the OCA/queue project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/queue_job/static/lib/vis/vis-network.min.css b/queue_job/static/lib/vis/vis-network.min.css new file mode 100644 index 0000000000..d708f173b6 --- /dev/null +++ b/queue_job/static/lib/vis/vis-network.min.css @@ -0,0 +1 @@ +.vis-overlay{position:absolute;top:0;right:0;bottom:0;left:0;z-index:10}.vis-active{box-shadow:0 0 10px #86d5f8}.vis [class*=span]{min-height:0;width:auto}div.vis-color-picker{position:absolute;top:0;left:30px;margin-top:-140px;margin-left:30px;width:310px;height:444px;z-index:1;padding:10px;border-radius:15px;background-color:#fff;display:none;box-shadow:0 0 10px 0 rgba(0,0,0,.5)}div.vis-color-picker div.vis-arrow{position:absolute;top:147px;left:5px}div.vis-color-picker div.vis-arrow:after,div.vis-color-picker div.vis-arrow:before{right:100%;top:50%;border:solid transparent;content:" ";height:0;width:0;position:absolute;pointer-events:none}div.vis-color-picker div.vis-arrow:after{border-color:hsla(0,0%,100%,0) #fff hsla(0,0%,100%,0) hsla(0,0%,100%,0);border-width:30px;margin-top:-30px}div.vis-color-picker div.vis-color{position:absolute;width:289px;height:289px;cursor:pointer}div.vis-color-picker div.vis-brightness{position:absolute;top:313px}div.vis-color-picker div.vis-opacity{position:absolute;top:350px}div.vis-color-picker div.vis-selector{position:absolute;top:137px;left:137px;width:15px;height:15px;border-radius:15px;border:1px solid #fff;background:#4c4c4c;background:-moz-linear-gradient(top,#4c4c4c 0,#595959 12%,#666 25%,#474747 39%,#2c2c2c 50%,#000 51%,#111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#4c4c4c),color-stop(12%,#595959),color-stop(25%,#666),color-stop(39%,#474747),color-stop(50%,#2c2c2c),color-stop(51%,#000),color-stop(60%,#111),color-stop(76%,#2b2b2b),color-stop(91%,#1c1c1c),color-stop(100%,#131313));background:-webkit-linear-gradient(top,#4c4c4c,#595959 12%,#666 25%,#474747 39%,#2c2c2c 50%,#000 51%,#111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313);background:-o-linear-gradient(top,#4c4c4c 0,#595959 12%,#666 25%,#474747 39%,#2c2c2c 50%,#000 51%,#111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313 100%);background:-ms-linear-gradient(top,#4c4c4c 0,#595959 12%,#666 25%,#474747 39%,#2c2c2c 50%,#000 51%,#111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313 100%);background:linear-gradient(180deg,#4c4c4c 0,#595959 12%,#666 25%,#474747 39%,#2c2c2c 50%,#000 51%,#111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#4c4c4c",endColorstr="#131313",GradientType=0)}div.vis-color-picker div.vis-new-color{left:159px;text-align:right;padding-right:2px}div.vis-color-picker div.vis-initial-color,div.vis-color-picker div.vis-new-color{position:absolute;width:140px;height:20px;border:1px solid rgba(0,0,0,.1);border-radius:5px;top:380px;font-size:10px;color:rgba(0,0,0,.4);vertical-align:middle;line-height:20px}div.vis-color-picker div.vis-initial-color{left:10px;text-align:left;padding-left:2px}div.vis-color-picker div.vis-label{position:absolute;width:300px;left:10px}div.vis-color-picker div.vis-label.vis-brightness{top:300px}div.vis-color-picker div.vis-label.vis-opacity{top:338px}div.vis-color-picker div.vis-button{position:absolute;width:68px;height:25px;border-radius:10px;vertical-align:middle;text-align:center;line-height:25px;top:410px;border:2px solid #d9d9d9;background-color:#f7f7f7;cursor:pointer}div.vis-color-picker div.vis-button.vis-cancel{left:5px}div.vis-color-picker div.vis-button.vis-load{left:82px}div.vis-color-picker div.vis-button.vis-apply{left:159px}div.vis-color-picker div.vis-button.vis-save{left:236px}div.vis-color-picker input.vis-range{width:290px;height:20px}div.vis-configuration{position:relative;display:block;float:left;font-size:12px}div.vis-configuration-wrapper{display:block;width:700px}div.vis-configuration-wrapper:after{clear:both;content:"";display:block}div.vis-configuration.vis-config-option-container{display:block;width:495px;background-color:#fff;border:2px solid #f7f8fa;border-radius:4px;margin-top:20px;left:10px;padding-left:5px}div.vis-configuration.vis-config-button{display:block;width:495px;height:25px;vertical-align:middle;line-height:25px;background-color:#f7f8fa;border:2px solid #ceced0;border-radius:4px;margin-top:20px;left:10px;padding-left:5px;cursor:pointer;margin-bottom:30px}div.vis-configuration.vis-config-button.hover{background-color:#4588e6;border:2px solid #214373;color:#fff}div.vis-configuration.vis-config-item{display:block;float:left;width:495px;height:25px;vertical-align:middle;line-height:25px}div.vis-configuration.vis-config-item.vis-config-s2{left:10px;background-color:#f7f8fa;padding-left:5px;border-radius:3px}div.vis-configuration.vis-config-item.vis-config-s3{left:20px;background-color:#e4e9f0;padding-left:5px;border-radius:3px}div.vis-configuration.vis-config-item.vis-config-s4{left:30px;background-color:#cfd8e6;padding-left:5px;border-radius:3px}div.vis-configuration.vis-config-header{font-size:18px;font-weight:700}div.vis-configuration.vis-config-label{width:120px;height:25px;line-height:25px}div.vis-configuration.vis-config-label.vis-config-s3{width:110px}div.vis-configuration.vis-config-label.vis-config-s4{width:100px}div.vis-configuration.vis-config-colorBlock{top:1px;width:30px;height:19px;border:1px solid #444;border-radius:2px;padding:0;margin:0;cursor:pointer}input.vis-configuration.vis-config-checkbox{left:-5px}input.vis-configuration.vis-config-rangeinput{position:relative;top:-5px;width:60px;padding:1px;margin:0;pointer-events:none}input.vis-configuration.vis-config-range{-webkit-appearance:none;border:0 solid #fff;background-color:transparent;width:300px;height:20px}input.vis-configuration.vis-config-range::-webkit-slider-runnable-track{width:300px;height:5px;background:#dedede;background:-moz-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#dedede),color-stop(99%,#c8c8c8));background:-webkit-linear-gradient(top,#dedede,#c8c8c8 99%);background:-o-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:-ms-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:linear-gradient(180deg,#dedede 0,#c8c8c8 99%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#dedede",endColorstr="#c8c8c8",GradientType=0);border:1px solid #999;box-shadow:0 0 3px 0 #aaa;border-radius:3px}input.vis-configuration.vis-config-range::-webkit-slider-thumb{-webkit-appearance:none;border:1px solid #14334b;height:17px;width:17px;border-radius:50%;background:#3876c2;background:-moz-linear-gradient(top,#3876c2 0,#385380 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#3876c2),color-stop(100%,#385380));background:-webkit-linear-gradient(top,#3876c2,#385380);background:-o-linear-gradient(top,#3876c2 0,#385380 100%);background:-ms-linear-gradient(top,#3876c2 0,#385380 100%);background:linear-gradient(180deg,#3876c2 0,#385380);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#3876c2",endColorstr="#385380",GradientType=0);box-shadow:0 0 1px 0 #111927;margin-top:-7px}input.vis-configuration.vis-config-range:focus{outline:none}input.vis-configuration.vis-config-range:focus::-webkit-slider-runnable-track{background:#9d9d9d;background:-moz-linear-gradient(top,#9d9d9d 0,#c8c8c8 99%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#9d9d9d),color-stop(99%,#c8c8c8));background:-webkit-linear-gradient(top,#9d9d9d,#c8c8c8 99%);background:-o-linear-gradient(top,#9d9d9d 0,#c8c8c8 99%);background:-ms-linear-gradient(top,#9d9d9d 0,#c8c8c8 99%);background:linear-gradient(180deg,#9d9d9d 0,#c8c8c8 99%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#9d9d9d",endColorstr="#c8c8c8",GradientType=0)}input.vis-configuration.vis-config-range::-moz-range-track{width:300px;height:10px;background:#dedede;background:-moz-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#dedede),color-stop(99%,#c8c8c8));background:-webkit-linear-gradient(top,#dedede,#c8c8c8 99%);background:-o-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:-ms-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:linear-gradient(180deg,#dedede 0,#c8c8c8 99%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#dedede",endColorstr="#c8c8c8",GradientType=0);border:1px solid #999;box-shadow:0 0 3px 0 #aaa;border-radius:3px}input.vis-configuration.vis-config-range::-moz-range-thumb{border:none;height:16px;width:16px;border-radius:50%;background:#385380}input.vis-configuration.vis-config-range:-moz-focusring{outline:1px solid #fff;outline-offset:-1px}input.vis-configuration.vis-config-range::-ms-track{width:300px;height:5px;background:transparent;border-color:transparent;border-width:6px 0;color:transparent}input.vis-configuration.vis-config-range::-ms-fill-lower{background:#777;border-radius:10px}input.vis-configuration.vis-config-range::-ms-fill-upper{background:#ddd;border-radius:10px}input.vis-configuration.vis-config-range::-ms-thumb{border:none;height:16px;width:16px;border-radius:50%;background:#385380}input.vis-configuration.vis-config-range:focus::-ms-fill-lower{background:#888}input.vis-configuration.vis-config-range:focus::-ms-fill-upper{background:#ccc}.vis-configuration-popup{position:absolute;background:rgba(57,76,89,.85);border:2px solid #f2faff;line-height:30px;height:30px;width:150px;text-align:center;color:#fff;font-size:14px;border-radius:4px;-webkit-transition:opacity .3s ease-in-out;-moz-transition:opacity .3s ease-in-out;transition:opacity .3s ease-in-out}.vis-configuration-popup:after,.vis-configuration-popup:before{left:100%;top:50%;border:solid transparent;content:" ";height:0;width:0;position:absolute;pointer-events:none}.vis-configuration-popup:after{border-color:rgba(136,183,213,0) rgba(136,183,213,0) rgba(136,183,213,0) rgba(57,76,89,.85);border-width:8px;margin-top:-8px}.vis-configuration-popup:before{border-color:rgba(194,225,245,0) rgba(194,225,245,0) rgba(194,225,245,0) #f2faff;border-width:12px;margin-top:-12px}div.vis-tooltip{position:absolute;visibility:hidden;padding:5px;white-space:nowrap;font-family:verdana;font-size:14px;color:#000;background-color:#f5f4ed;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;border:1px solid #808074;box-shadow:3px 3px 10px rgba(0,0,0,.2);pointer-events:none;z-index:5}div.vis-network div.vis-navigation div.vis-button{width:34px;height:34px;-moz-border-radius:17px;border-radius:17px;position:absolute;display:inline-block;background-position:2px 2px;background-repeat:no-repeat;cursor:pointer;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}div.vis-network div.vis-navigation div.vis-button:hover{box-shadow:0 0 3px 3px rgba(56,207,21,.3)}div.vis-network div.vis-navigation div.vis-button:active{box-shadow:0 0 1px 3px rgba(56,207,21,.95)}div.vis-network div.vis-navigation div.vis-button.vis-up{background-image:url("data:image/png;base64,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");bottom:50px;left:55px}div.vis-network div.vis-navigation div.vis-button.vis-down{background-image:url("data:image/png;base64,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");bottom:10px;left:55px}div.vis-network div.vis-navigation div.vis-button.vis-left{background-image:url("data:image/png;base64,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");bottom:10px;left:15px}div.vis-network div.vis-navigation div.vis-button.vis-right{background-image:url("data:image/png;base64,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");bottom:10px;left:95px}div.vis-network div.vis-navigation div.vis-button.vis-zoomIn{background-image:url("data:image/png;base64,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");bottom:10px;right:15px}div.vis-network div.vis-navigation div.vis-button.vis-zoomOut{background-image:url("data:image/png;base64,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");bottom:10px;right:55px}div.vis-network div.vis-navigation div.vis-button.vis-zoomExtends{background-image:url("data:image/png;base64,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");bottom:50px;right:15px}div.vis-network div.vis-manipulation{box-sizing:content-box;border:0 solid #d6d9d8;border-bottom:1px;background:#fff;background:-moz-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#fff),color-stop(48%,#fcfcfc),color-stop(50%,#fafafa),color-stop(100%,#fcfcfc));background:-webkit-linear-gradient(top,#fff,#fcfcfc 48%,#fafafa 50%,#fcfcfc);background:-o-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:-ms-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:linear-gradient(180deg,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#ffffff",endColorstr="#fcfcfc",GradientType=0);padding-top:4px;position:absolute;left:0;top:0;width:100%;height:28px}div.vis-network button.vis-edit-mode,div.vis-network div.vis-edit-mode{position:absolute;left:0;top:5px;height:30px}div.vis-network button.vis-close{position:absolute;right:0;top:0;width:30px;height:30px;background-color:transparent;background-position:20px 3px;background-repeat:no-repeat;background-image:url("data:image/png;base64,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/GMzLdsAADvGaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjUtYzAyMSA3OS4xNTQ5MTEsIDIwMTMvMTAvMjktMTE6NDc6MTYgICAgICAgICI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgICAgICAgICAgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iCiAgICAgICAgICAgIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiCiAgICAgICAgICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgICAgICAgICAgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8eG1wOkNyZWF0b3JUb29sPkFkb2JlIFBob3Rvc2hvcCBDQyAoV2luZG93cyk8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgICAgPHhtcDpDcmVhdGVEYXRlPjIwMTQtMDItMTRUMTE6NTU6MzUrMDE6MDA8L3htcDpDcmVhdGVEYXRlPgogICAgICAgICA8eG1wOk1ldGFkYXRhRGF0ZT4yMDE0LTAyLTE0VDEyOjA1OjE3KzAxOjAwPC94bXA6TWV0YWRhdGFEYXRlPgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxNC0wMi0xNFQxMjowNToxNyswMTowMDwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXBNTTpJbnN0YW5jZUlEPnhtcC5paWQ6NjU0YmM5YmQtMWI2Yi1jYjRhLTllOWQtNWY2MzgxNDVjZjk0PC94bXBNTTpJbnN0YW5jZUlEPgogICAgICAgICA8eG1wTU06RG9jdW1lbnRJRD54bXAuZGlkOjk4MmM2MGIwLWUzZjMtMDk0MC04MjU0LTFiZTliNWE0ZTE4MzwveG1wTU06RG9jdW1lbnRJRD4KICAgICAgICAgPHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD54bXAuZGlkOjk4MmM2MGIwLWUzZjMtMDk0MC04MjU0LTFiZTliNWE0ZTE4MzwveG1wTU06T3JpZ2luYWxEb2N1bWVudElEPgogICAgICAgICA8eG1wTU06SGlzdG9yeT4KICAgICAgICAgICAgPHJkZjpTZXE+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPmNyZWF0ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0Omluc3RhbmNlSUQ+eG1wLmlpZDo5ODJjNjBiMC1lM2YzLTA5NDAtODI1NC0xYmU5YjVhNGUxODM8L3N0RXZ0Omluc3RhbmNlSUQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDp3aGVuPjIwMTQtMDItMTRUMTE6NTU6MzUrMDE6MDA8L3N0RXZ0OndoZW4+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDQyAoV2luZG93cyk8L3N0RXZ0OnNvZnR3YXJlQWdlbnQ+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjIxODYxNmM2LTM1MWMtNDI0OS04YWFkLWJkZDQ2ZTczNWE0NDwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OndoZW4+MjAxNC0wMi0xNFQxMTo1NTozNSswMTowMDwvc3RFdnQ6d2hlbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+Lzwvc3RFdnQ6Y2hhbmdlZD4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPnNhdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDppbnN0YW5jZUlEPnhtcC5paWQ6NjU0YmM5YmQtMWI2Yi1jYjRhLTllOWQtNWY2MzgxNDVjZjk0PC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE0LTAyLTE0VDEyOjA1OjE3KzAxOjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6Y2hhbmdlZD4vPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6U2VxPgogICAgICAgICA8L3htcE1NOkhpc3Rvcnk+CiAgICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2UvcG5nPC9kYzpmb3JtYXQ+CiAgICAgICAgIDxwaG90b3Nob3A6Q29sb3JNb2RlPjM8L3Bob3Rvc2hvcDpDb2xvck1vZGU+CiAgICAgICAgIDxwaG90b3Nob3A6SUNDUHJvZmlsZT5zUkdCIElFQzYxOTY2LTIuMTwvcGhvdG9zaG9wOklDQ1Byb2ZpbGU+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjcyMDAwMC8xMDAwMDwvdGlmZjpYUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6WVJlc29sdXRpb24+NzIwMDAwLzEwMDAwPC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjc8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFlEaW1lbnNpb24+NzwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgIAo8P3hwYWNrZXQgZW5kPSJ3Ij8+cZUZMwAAACBjSFJNAAB6JQAAgIMAAPn/AACA6QAAdTAAAOpgAAA6mAAAF2+SX8VGAAAA2ElEQVR42gDLADT/AS0tLUQFBQUVFxcXtPHx8fPl5eUNCAgITCkpKesEHx8fGgYGBjH+/v4a+Pj4qgQEBFU6OjodMTExzwQUFBSvEBAQEfX19SD19fVqNDQ0CElJSd/9/f2vAwEBAfrn5+fkBwcHLRYWFgsXFxfz29vbo9LS0uwDDQ0NDfPz81orKysXIyMj+ODg4Avh4eEa/f391gMkJCRYPz8/KUhISOMCAgKh8fHxHRsbGx4UFBQQBDk5OeY7Ozv7CAgItPb29vMEBASaJSUlTQ0NDesDAEwpT0Ko8Ri2AAAAAElFTkSuQmCC");border:none;cursor:pointer;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}div.vis-network button.vis-close:hover{opacity:.6}div.vis-network div.vis-edit-mode button.vis-button,div.vis-network div.vis-manipulation button.vis-button{float:left;font-family:verdana;font-size:12px;border:none;box-sizing:content-box;-moz-border-radius:15px;border-radius:15px;background-color:transparent;background-position:0 0;background-repeat:no-repeat;height:24px;margin-left:10px;cursor:pointer;padding:0 8px;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}div.vis-network div.vis-manipulation button.vis-button:hover{box-shadow:1px 1px 8px rgba(0,0,0,.2)}div.vis-network div.vis-manipulation button.vis-button:active{box-shadow:1px 1px 8px rgba(0,0,0,.5)}div.vis-network div.vis-manipulation button.vis-button.vis-back{background-image:url("data:image/png;base64,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/GMzLdsAAEEOaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjUtYzAyMSA3OS4xNTQ5MTEsIDIwMTMvMTAvMjktMTE6NDc6MTYgICAgICAgICI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgICAgICAgICAgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiCiAgICAgICAgICAgIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5BZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpPC94bXA6Q3JlYXRvclRvb2w+CiAgICAgICAgIDx4bXA6Q3JlYXRlRGF0ZT4yMDE0LTAxLTIyVDE5OjI0OjUxKzAxOjAwPC94bXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhtcDpNZXRhZGF0YURhdGU+MjAxNC0wMi0wNFQxNTowMTowOSswMTowMDwveG1wOk1ldGFkYXRhRGF0ZT4KICAgICAgICAgPHhtcDpNb2RpZnlEYXRlPjIwMTQtMDItMDRUMTU6MDE6MDkrMDE6MDA8L3htcDpNb2RpZnlEYXRlPgogICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3BuZzwvZGM6Zm9ybWF0PgogICAgICAgICA8eG1wTU06SW5zdGFuY2VJRD54bXAuaWlkOmI2YjQwMjVkLTAxNjQtMzU0OC1hOTdlLTQ4ZmYxMWM3NTYzMzwveG1wTU06SW5zdGFuY2VJRD4KICAgICAgICAgPHhtcE1NOkRvY3VtZW50SUQ+eG1wLmRpZDpFQTc2MkY5Njc0ODNFMzExOTQ4QkQxM0UyQkU3OTlBMTwveG1wTU06RG9jdW1lbnRJRD4KICAgICAgICAgPHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD54bXAuZGlkOjczQjYyQUFEOTE4M0UzMTE5NDhCRDEzRTJCRTc5OUExPC94bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ+CiAgICAgICAgIDx4bXBNTTpIaXN0b3J5PgogICAgICAgICAgICA8cmRmOlNlcT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+Y3JlYXRlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjczQjYyQUFEOTE4M0UzMTE5NDhCRDEzRTJCRTc5OUExPC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE0LTAxLTIyVDE5OjI0OjUxKzAxOjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ1M2IChXaW5kb3dzKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPnNhdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDppbnN0YW5jZUlEPnhtcC5paWQ6RUE2MEEyNEUxOTg0RTMxMUFEQUZFRkU2RUMzMzNFMDM8L3N0RXZ0Omluc3RhbmNlSUQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDp3aGVuPjIwMTQtMDEtMjNUMTk6MTg6MDcrMDE6MDA8L3N0RXZ0OndoZW4+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6Y2hhbmdlZD4vPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+c2F2ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0Omluc3RhbmNlSUQ+eG1wLmlpZDpmOWQ3OGY4ZC1lNzY0LTc1NDgtODZiNy1iNmQ1OGMzZDg2OTc8L3N0RXZ0Omluc3RhbmNlSUQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDp3aGVuPjIwMTQtMDItMDRUMTU6MDE6MDkrMDE6MDA8L3N0RXZ0OndoZW4+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDQyAoV2luZG93cyk8L3N0RXZ0OnNvZnR3YXJlQWdlbnQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpjaGFuZ2VkPi88L3N0RXZ0OmNoYW5nZWQ+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5jb252ZXJ0ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnBhcmFtZXRlcnM+ZnJvbSBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIHRvIGltYWdlL3BuZzwvc3RFdnQ6cGFyYW1ldGVycz4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPmRlcml2ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnBhcmFtZXRlcnM+Y29udmVydGVkIGZyb20gYXBwbGljYXRpb24vdm5kLmFkb2JlLnBob3Rvc2hvcCB0byBpbWFnZS9wbmc8L3N0RXZ0OnBhcmFtZXRlcnM+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOmI2YjQwMjVkLTAxNjQtMzU0OC1hOTdlLTQ4ZmYxMWM3NTYzMzwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OndoZW4+MjAxNC0wMi0wNFQxNTowMTowOSswMTowMDwvc3RFdnQ6d2hlbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+Lzwvc3RFdnQ6Y2hhbmdlZD4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOlNlcT4KICAgICAgICAgPC94bXBNTTpIaXN0b3J5PgogICAgICAgICA8eG1wTU06RGVyaXZlZEZyb20gcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICA8c3RSZWY6aW5zdGFuY2VJRD54bXAuaWlkOmY5ZDc4ZjhkLWU3NjQtNzU0OC04NmI3LWI2ZDU4YzNkODY5Nzwvc3RSZWY6aW5zdGFuY2VJRD4KICAgICAgICAgICAgPHN0UmVmOmRvY3VtZW50SUQ+eG1wLmRpZDpFQTc2MkY5Njc0ODNFMzExOTQ4QkQxM0UyQkU3OTlBMTwvc3RSZWY6ZG9jdW1lbnRJRD4KICAgICAgICAgICAgPHN0UmVmOm9yaWdpbmFsRG9jdW1lbnRJRD54bXAuZGlkOjczQjYyQUFEOTE4M0UzMTE5NDhCRDEzRTJCRTc5OUExPC9zdFJlZjpvcmlnaW5hbERvY3VtZW50SUQ+CiAgICAgICAgIDwveG1wTU06RGVyaXZlZEZyb20+CiAgICAgICAgIDxwaG90b3Nob3A6Q29sb3JNb2RlPjM8L3Bob3Rvc2hvcDpDb2xvck1vZGU+CiAgICAgICAgIDxwaG90b3Nob3A6SUNDUHJvZmlsZT5zUkdCIElFQzYxOTY2LTIuMTwvcGhvdG9zaG9wOklDQ1Byb2ZpbGU+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjcyMDA5MC8xMDAwMDwvdGlmZjpYUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6WVJlc29sdXRpb24+NzIwMDkwLzEwMDAwPC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjI0PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjI0PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz4jq1U/AAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAVTSURBVHjanFVfTFNnFP+d77ve8qeVFbBrpcVgRrCRFikFByLxwSAaE32oRCHD6JMxxhhn8G2RxxH3MsOTbyYsmCAxPMmMMYtkIUYmK60OO0qAK23BFlNob0uh3x7WS5jLZPpLbm6+k/P9zrm5v9855PF4UFhYCABgjIExBgAgIqRSqRIi6gDQRkQ1RGTB3wgR0e8AHgH4Sa/XR/EBiAiJRAJ04cIF5Ofng4g2n0gkUkxENwF0c843LzHGQEQQQkCLExEA9ALotVgsUQAQQmgNQhJCbF5kjCEUCl0moj4t5na7fTU1NUpVVVXUYrEkASAcDhe8efOmxOfzWScmJqoBdBNR99LS0hWz2dynNSSEAF28eBGFhYVgjCEcDn9HRD1EhIMHD3o9Hs9kWVlZAh9BKBQqGB4edr58+dKZ+6JbJpOpBwBWV1fB6+rqIMsyIpHIFcZYL2MMra2tY5cuXRrfuXNnBtvAYDBk3G63oqpqZm5uzgrgSDKZjBoMhueZTAbc5XIhFouVEtFTxhiOHTs2dv78eS8+Efv374+oqpqZnZ21cs5PJJPJPlmWkyynnBuMMTQ0NHi7uro+mVyDx+Pxulwu71ZOlkqlSonoJhGhvb39s8k1nDx50ss5hyRJN9PpdKlERB2aWjSVaEilUvzBgwcORVEs5eXloXPnzk1sV8BkMiUdDofP7/dXZ7PZDilnIhw4cGBeS1pbW2P37t1zBwKBikQiUUREWFhYsHHO0d7evm0Ru90+/+rVq2rO+XGJiJxEhMrKyhgAjI6OWoeHh5tWVla+4JzDZrO9bW5unhwcHGzz+/32np4e+xaDbfoHAMxmc6ijo2O0oqIiJkkSNjY2HBIRmRljMJvNyWfPnln7+/tPMMZQXl6+0NbW9qK2tjYcj8floaEhqKpq+HCkbD3PzMwYBgYG0NXV9UuusFna2kEgELAQEQ4dOvSis7PzN41Ar9dnrl27NqCNkv/C3bt3zy4tLVmICJxzEBFJRBQmorLFxcWCqqqq0Pj4eO3Y2JhbUZTdra2tL2pra8OJRGLHnTt3zkqS9K+huHU4EhHMZnMoGo0W5OIh7nK5jjLGKq1W69vDhw8rRqMxMjc3t2t5eXnX5ORklc/nM+fl5SWnpqa+0uv1K/n5+Ws6nW5NluXNd15e3ppOp1uz2WyzZ86cGQ0Gg6ZAIFCZzWZ/lYjokRDiuN/vt7W0tMw3NTUpbrd78P79++5gMFgRiUTKHj58WMYYQ3V19etTp05tq6Lp6Wkb5xxCiEfc7XZPM8a6FxcXTfX19a/1en2Gcy5qamreNjY2/qGq6joRZe12+9Tp06e3JY/FYgWPHz8+mhvr3/CWlpbk+vp6PmOseWVlBS6XS9GSJUkSdrs93NDQ8Oe+ffvC/8fJIyMjddFo9Esi6pVleVjT2m0A8Hq9zqGhIefnjoknT544A4GAM/eDbxMReFNTE0pKSpKqqsaI6Pj8/LxVVdWM3W6PfCr5xMTE1zllXS0uLn6aSqXAGxsbodPpoNfrn6uqCs75EUVRrJFIZMfevXsXdTrdxseIE4mEPDIyUu/3++tynd8yGo29RIR0Og26fv06ioqKwBgD5xzv3r27zBjrIyJIkgSHwzFZWVmp7NmzJ1ZaWpoAgGg0WqgoSvHMzIw1GAw6tvjhitFo7NPW5fv370Hd3d0oKCgA53zTQMvLy+VCiKuSJH0rSdLmztZytIWv5RPRD0T0Y3Fx8dzWfby6ugopHo//w4mcc8iyPMc5v5FOp7/PZrOdQohWInIC2C2EgBBigYi8Qoifs9lsv06nWyIiaFxagXg8jr8GAGxuIe7LBeWhAAAAAElFTkSuQmCC")}div.vis-network div.vis-manipulation div.vis-none:hover{box-shadow:1px 1px 8px transparent;cursor:default}div.vis-network div.vis-manipulation div.vis-none:active{box-shadow:1px 1px 8px transparent}div.vis-network div.vis-manipulation div.vis-none{padding:0;line-height:23px}div.vis-network div.vis-manipulation div.notification{margin:2px;font-weight:700}div.vis-network div.vis-manipulation button.vis-button.vis-add{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAEEOaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjUtYzAyMSA3OS4xNTQ5MTEsIDIwMTMvMTAvMjktMTE6NDc6MTYgICAgICAgICI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgICAgICAgICAgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiCiAgICAgICAgICAgIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5BZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpPC94bXA6Q3JlYXRvclRvb2w+CiAgICAgICAgIDx4bXA6Q3JlYXRlRGF0ZT4yMDE0LTAxLTIyVDE5OjI0OjUxKzAxOjAwPC94bXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhtcDpNZXRhZGF0YURhdGU+MjAxNC0wMi0wNFQxNDo0MDoyOSswMTowMDwveG1wOk1ldGFkYXRhRGF0ZT4KICAgICAgICAgPHhtcDpNb2RpZnlEYXRlPjIwMTQtMDItMDRUMTQ6NDA6MjkrMDE6MDA8L3htcDpNb2RpZnlEYXRlPgogICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3BuZzwvZGM6Zm9ybWF0PgogICAgICAgICA8eG1wTU06SW5zdGFuY2VJRD54bXAuaWlkOjVkNWIwNmQwLTVmMjAtOGE0NC1hMzIwLWZmMTEzMzQwNDc0YjwveG1wTU06SW5zdGFuY2VJRD4KICAgICAgICAgPHhtcE1NOkRvY3VtZW50SUQ+eG1wLmRpZDpFQTc2MkY5Njc0ODNFMzExOTQ4QkQxM0UyQkU3OTlBMTwveG1wTU06RG9jdW1lbnRJRD4KICAgICAgICAgPHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD54bXAuZGlkOjczQjYyQUFEOTE4M0UzMTE5NDhCRDEzRTJCRTc5OUExPC94bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ+CiAgICAgICAgIDx4bXBNTTpIaXN0b3J5PgogICAgICAgICAgICA8cmRmOlNlcT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+Y3JlYXRlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjczQjYyQUFEOTE4M0UzMTE5NDhCRDEzRTJCRTc5OUExPC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE0LTAxLTIyVDE5OjI0OjUxKzAxOjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ1M2IChXaW5kb3dzKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPnNhdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDppbnN0YW5jZUlEPnhtcC5paWQ6RUE2MEEyNEUxOTg0RTMxMUFEQUZFRkU2RUMzMzNFMDM8L3N0RXZ0Omluc3RhbmNlSUQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDp3aGVuPjIwMTQtMDEtMjNUMTk6MTg6MDcrMDE6MDA8L3N0RXZ0OndoZW4+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6Y2hhbmdlZD4vPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+c2F2ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0Omluc3RhbmNlSUQ+eG1wLmlpZDo2OWVmYWE1NS01ZTI5LTIzNGUtYTUzMy0xNDkxYjM1NDNmYmE8L3N0RXZ0Omluc3RhbmNlSUQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDp3aGVuPjIwMTQtMDItMDRUMTQ6NDA6MjkrMDE6MDA8L3N0RXZ0OndoZW4+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDQyAoV2luZG93cyk8L3N0RXZ0OnNvZnR3YXJlQWdlbnQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpjaGFuZ2VkPi88L3N0RXZ0OmNoYW5nZWQ+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5jb252ZXJ0ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnBhcmFtZXRlcnM+ZnJvbSBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIHRvIGltYWdlL3BuZzwvc3RFdnQ6cGFyYW1ldGVycz4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPmRlcml2ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnBhcmFtZXRlcnM+Y29udmVydGVkIGZyb20gYXBwbGljYXRpb24vdm5kLmFkb2JlLnBob3Rvc2hvcCB0byBpbWFnZS9wbmc8L3N0RXZ0OnBhcmFtZXRlcnM+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjVkNWIwNmQwLTVmMjAtOGE0NC1hMzIwLWZmMTEzMzQwNDc0Yjwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OndoZW4+MjAxNC0wMi0wNFQxNDo0MDoyOSswMTowMDwvc3RFdnQ6d2hlbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+Lzwvc3RFdnQ6Y2hhbmdlZD4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOlNlcT4KICAgICAgICAgPC94bXBNTTpIaXN0b3J5PgogICAgICAgICA8eG1wTU06RGVyaXZlZEZyb20gcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICA8c3RSZWY6aW5zdGFuY2VJRD54bXAuaWlkOjY5ZWZhYTU1LTVlMjktMjM0ZS1hNTMzLTE0OTFiMzU0M2ZiYTwvc3RSZWY6aW5zdGFuY2VJRD4KICAgICAgICAgICAgPHN0UmVmOmRvY3VtZW50SUQ+eG1wLmRpZDpFQTc2MkY5Njc0ODNFMzExOTQ4QkQxM0UyQkU3OTlBMTwvc3RSZWY6ZG9jdW1lbnRJRD4KICAgICAgICAgICAgPHN0UmVmOm9yaWdpbmFsRG9jdW1lbnRJRD54bXAuZGlkOjczQjYyQUFEOTE4M0UzMTE5NDhCRDEzRTJCRTc5OUExPC9zdFJlZjpvcmlnaW5hbERvY3VtZW50SUQ+CiAgICAgICAgIDwveG1wTU06RGVyaXZlZEZyb20+CiAgICAgICAgIDxwaG90b3Nob3A6Q29sb3JNb2RlPjM8L3Bob3Rvc2hvcDpDb2xvck1vZGU+CiAgICAgICAgIDxwaG90b3Nob3A6SUNDUHJvZmlsZT5zUkdCIElFQzYxOTY2LTIuMTwvcGhvdG9zaG9wOklDQ1Byb2ZpbGU+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjcyMDA5MC8xMDAwMDwvdGlmZjpYUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6WVJlc29sdXRpb24+NzIwMDkwLzEwMDAwPC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjI0PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjI0PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz5WKqp9AAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAYXSURBVHjafFZtUFTXGX7e9z27sveuMCwYV8ElrA7YSFYHtJUPkaaI0aRqG8wP00zUzljDINNSA/2ROtpO24SxnahlxjYd7SSjmUkymcxYlDhQPzHGisEVp8HwYWCVVVgEsrsuLnL74+5uqTF9Z+7cO/d8PO95zvO851BlZSV0XQcAMDOYGQBARDhX3JRmMDYZwLPMWAzGHACYIgwS46oBNBNwtOL8CwE8EkSEUCgE2rJlC2w2G4go8Zwo/bMDgnoG6gxLfAAAYvPDMCCszKTAMIAGAhrWnf15AAAMwwARIRKJgDZv3gy73Q4iAjPjxIr9VVOMRhbAYKB8zvrO0llrfEsdKwLZek6YAPSFvtSu3GtLawu0ZJ6625SHGBQB1T88t6MxvopgMAjaunUrdF0HM+P4yv27DMYeJmB1RqW3Jnf3tQX2p0L4P9EXuqEd7PmDp+XuMU9sRbvXnnt1TxxACgoKYLVacbzsQDUJGkSATe6qi28uPtzusM6Kxie6NHLGUX3lxVUNX9StPHnn4wy3njuUYcu6n2pNi66avcEXnByP/nv8aiaIyrqz2gO5A9+9FI1GIfn5+WhZdTAdjFMkwMvZOy7uWnTAOz3L4Yk71m3t69fdfTDoUGTBeHTUfiHQ6lo7Z2OXJvpDAChKe+aOCdKRKWxZ2+1qb3yyd3GYmRkQ7GQBVs99wfv6on3eR2k4PdTkDEbH7IuS8/svld/561PJS/pDk1/bzwx94pze7xc5v/H+YPY6r5BAkdrJzODTK46lE6PeYEJt7u+8j+OZwCBiEAgAoNgKJoEQf6PvNvdrXgtZoNhSf7q0KZ3B2AQmVMze0Jmt54S/DcDCVig2NcvEUGxJAE4Pl+YOr0iv6BRSIPAmBeBZAmHlE2sH4p1uhrq1s0MnnEQMBsf8wRASAICQQCCITN1X7/sOuc0kgOVp3/fPs2WHv+coG7gQOJUnLGsUCTxEjPzUohEA+NfIWUdtx0+efzA1kSSkIGyBAQNCKgHAEBAJ3u79U7kiAcWoem/gb5Fd33nrH3kp+SMWtuAB+GllMJxMjCx9QRgA3uiqL5kwHiTlpxb3smlfMDGYGPP1hcMAkJvs8ScpfdJspdj+MK6Pf+5+u29vyb4lR4+BGEziVESAkEpw6Av1OhUpHCz4qOXbzFWz4Ncdj/v/o08Lt92ODDgZDCEFJYoUGH4mzugP92puPTf0pD3H7wvfdFZdqSxnMtWjoGAAmG9fOLxjwesdjT2/XzIQ7ks3sycYMSEwGHNtWf5bkX5NkYCJBxUBXiGV0XHvosOt54Zey33j/K+8P33++vjnbiGJbbLE+J9SANAb6nJ2B79wcUwETAwQQ7fMjPzMvfP8ja87HUIKMOiaAqMZhrGmLdAy78eZrwwsTS0eObTs+IdtgVanxBUExqGbb5VzrIISGIoUXsmqbgEhJldCQWqRf27SvPAn/o8XmgLhZsUkR4ll37mhk3n94Z4OlzY/7NLcYZfm7o1z2zT4vsvUNSXqprBCkmiTFbPX90/fh8GIT2sf+zTPdDMf4dVnNg4z+E0ixsGeBs9jd5ViSgLHjCb/peaR+MD3d4/ZJg2llyuG2Vwy7QWAs8PNnn1f7vkGSGxAzE6mk+kxkx/p/4unffSCR0hAoL1EBCYiPNdWNcwkNQTCR7feWX6g+7f/A7I8rcw/U6UEe0Ndrhc/W7mtL9ztmqlSgstSS/zTJ28dalpOpkRryrwbhwBACgsLMWPGDOT4ll3qyeqAkJTdCF7P/CrUY/GkLL1rE+2hTbSH8+0Lb/WEuhzhyaA905blf9Vd/895WnZwLHrPevir/cvOB1oLYpTtLrm6oYGIMDExAaqtrUVKSgqYGSKCk0WHq5ikkWEWtNL0imv5qUW+RclLRjJsrhBAuH1/QL8R7HR4xy5nescuP23E6hOA6mLv+sb4uTw6Ogqqq6uDpmkQkcStorX4XRcM1FjZ+kvFFjCJKU1WpkNJJUqIMtX1RyLeX3JtQ0JRhmGYZ/L27duRnJycuFGISOJ9pqh5lrB6iYgqGOxRrOaa54DcZmKvkJxk8JHC9rKh+KVhOsD4+Dj+MwADIf8n5m4xGwAAAABJRU5ErkJggg==")}div.vis-network div.vis-edit-mode button.vis-button.vis-edit,div.vis-network div.vis-manipulation button.vis-button.vis-edit{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAEEOaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjUtYzAyMSA3OS4xNTQ5MTEsIDIwMTMvMTAvMjktMTE6NDc6MTYgICAgICAgICI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgICAgICAgICAgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiCiAgICAgICAgICAgIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5BZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpPC94bXA6Q3JlYXRvclRvb2w+CiAgICAgICAgIDx4bXA6Q3JlYXRlRGF0ZT4yMDE0LTAxLTIyVDE5OjI0OjUxKzAxOjAwPC94bXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhtcDpNZXRhZGF0YURhdGU+MjAxNC0wMi0wNVQxNDoxMjoyNSswMTowMDwveG1wOk1ldGFkYXRhRGF0ZT4KICAgICAgICAgPHhtcDpNb2RpZnlEYXRlPjIwMTQtMDItMDVUMTQ6MTI6MjUrMDE6MDA8L3htcDpNb2RpZnlEYXRlPgogICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3BuZzwvZGM6Zm9ybWF0PgogICAgICAgICA8eG1wTU06SW5zdGFuY2VJRD54bXAuaWlkOjY5OTM3ZGZjLTJjNzQtYTU0YS05OTIzLTQyMmZhNDNkMjljNDwveG1wTU06SW5zdGFuY2VJRD4KICAgICAgICAgPHhtcE1NOkRvY3VtZW50SUQ+eG1wLmRpZDpFQTc2MkY5Njc0ODNFMzExOTQ4QkQxM0UyQkU3OTlBMTwveG1wTU06RG9jdW1lbnRJRD4KICAgICAgICAgPHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD54bXAuZGlkOjczQjYyQUFEOTE4M0UzMTE5NDhCRDEzRTJCRTc5OUExPC94bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ+CiAgICAgICAgIDx4bXBNTTpIaXN0b3J5PgogICAgICAgICAgICA8cmRmOlNlcT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+Y3JlYXRlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjczQjYyQUFEOTE4M0UzMTE5NDhCRDEzRTJCRTc5OUExPC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE0LTAxLTIyVDE5OjI0OjUxKzAxOjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ1M2IChXaW5kb3dzKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPnNhdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDppbnN0YW5jZUlEPnhtcC5paWQ6RUE2MEEyNEUxOTg0RTMxMUFEQUZFRkU2RUMzMzNFMDM8L3N0RXZ0Omluc3RhbmNlSUQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDp3aGVuPjIwMTQtMDEtMjNUMTk6MTg6MDcrMDE6MDA8L3N0RXZ0OndoZW4+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6Y2hhbmdlZD4vPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+c2F2ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0Omluc3RhbmNlSUQ+eG1wLmlpZDozOWNhNzE5ZC03YzNlLTUyNGEtYmY1NS03NGVmMmM1MzE0YTc8L3N0RXZ0Omluc3RhbmNlSUQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDp3aGVuPjIwMTQtMDItMDVUMTQ6MTI6MjUrMDE6MDA8L3N0RXZ0OndoZW4+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDQyAoV2luZG93cyk8L3N0RXZ0OnNvZnR3YXJlQWdlbnQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpjaGFuZ2VkPi88L3N0RXZ0OmNoYW5nZWQ+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5jb252ZXJ0ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnBhcmFtZXRlcnM+ZnJvbSBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIHRvIGltYWdlL3BuZzwvc3RFdnQ6cGFyYW1ldGVycz4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPmRlcml2ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnBhcmFtZXRlcnM+Y29udmVydGVkIGZyb20gYXBwbGljYXRpb24vdm5kLmFkb2JlLnBob3Rvc2hvcCB0byBpbWFnZS9wbmc8L3N0RXZ0OnBhcmFtZXRlcnM+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjY5OTM3ZGZjLTJjNzQtYTU0YS05OTIzLTQyMmZhNDNkMjljNDwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OndoZW4+MjAxNC0wMi0wNVQxNDoxMjoyNSswMTowMDwvc3RFdnQ6d2hlbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+Lzwvc3RFdnQ6Y2hhbmdlZD4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOlNlcT4KICAgICAgICAgPC94bXBNTTpIaXN0b3J5PgogICAgICAgICA8eG1wTU06RGVyaXZlZEZyb20gcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICA8c3RSZWY6aW5zdGFuY2VJRD54bXAuaWlkOjM5Y2E3MTlkLTdjM2UtNTI0YS1iZjU1LTc0ZWYyYzUzMTRhNzwvc3RSZWY6aW5zdGFuY2VJRD4KICAgICAgICAgICAgPHN0UmVmOmRvY3VtZW50SUQ+eG1wLmRpZDpFQTc2MkY5Njc0ODNFMzExOTQ4QkQxM0UyQkU3OTlBMTwvc3RSZWY6ZG9jdW1lbnRJRD4KICAgICAgICAgICAgPHN0UmVmOm9yaWdpbmFsRG9jdW1lbnRJRD54bXAuZGlkOjczQjYyQUFEOTE4M0UzMTE5NDhCRDEzRTJCRTc5OUExPC9zdFJlZjpvcmlnaW5hbERvY3VtZW50SUQ+CiAgICAgICAgIDwveG1wTU06RGVyaXZlZEZyb20+CiAgICAgICAgIDxwaG90b3Nob3A6Q29sb3JNb2RlPjM8L3Bob3Rvc2hvcDpDb2xvck1vZGU+CiAgICAgICAgIDxwaG90b3Nob3A6SUNDUHJvZmlsZT5zUkdCIElFQzYxOTY2LTIuMTwvcGhvdG9zaG9wOklDQ1Byb2ZpbGU+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjcyMDA5MC8xMDAwMDwvdGlmZjpYUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6WVJlc29sdXRpb24+NzIwMDkwLzEwMDAwPC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjI0PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjI0PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz4ykninAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAYpSURBVHjafFZtTFvnFX7Oea+NudiY2Hwam4CBlgQwXdKREDKUoYg0jbRJ29RJ2VZ1mjRFUxSpA3VTfkzJfkQbS7spU6rtx5Z2UtppScjaHxvLuiatWi2jLEoMIUDCh23g2gbj7+tPuPvhOurawPl1dc99n+c55z33fV46ceIEZFkGADAziAgAQERoe/9ZK4GPM/AcgbsIXAcABCgMvkfAqAa89eDoJyF8LogIqqqChoaGYDAYHr8kItS8uc8iIH6iAa9IkAo5EAQX8pqmgUVBCBggYFgDhv0/GAsBgKZpICJkMhnQ4OAgZFkGEYGZUXmp+0cS+CKBwWA0DVRPOg5Zl2q6zaHyJlnVAMQXVTkwHrUqH0Xsvn+tdQAAMQDgpPLS2MViFY8rkGUZzIzaS/t/xqCzGggtz9e697zsnKhoLUtim4jOq/LE6x7X0nsh16dEZ5a/O3a2SCAOHjwInU6Hujd6ThJ4mCDQ+b2G232v7v6vwarPbQn8MGlMr+X0kpE3Wr5Zt5hL5HPhqYSdQIfKJ+yhxDPKWC6Xg+jt7UXD5b5KBt1kCHS85Ljd8/On3NupfnhFaZj4rWff1B98B1R/hnUmKd36bdtCNl4g0en4edNE/cXwLq8qMTMIPAQwmo/WuHvObA8+9c58k/dKtD0TyZWXN5YGA7ej7epKxspM//7SoNOdWc/Jyq2wiwhDzPxT8cP0jys3VMM7OmL0/77zn4Ydui3b8uiK0jD7RrA77c9Wd57cefPpF+2T6bWsFPWkaiPTCWvTsZpHFU+XrS+8G3AR08F6X+1FJvBxQQzHQOWk2SmrW4FPX/U2LVwPuDZj+fJKl2khPpeyAqA9rzR/YqwuiWXX8taN/CabGkrVuq9YJlkQQDjOAJ5jAhz9Vt9W4N5/rNp8I+vtMV/aZm4zLnUNNt0urdYnF68HWoJj4Wo1mLGUNRr8LEgDgNqeCh8xQIKOsgC7iAjVe83rT9zQa8uNM28u70kspessu8q8zq/V3NcZpVzb9+0zmVhOvvvrhaMVzrJg0zeq7xMVCCwdpnWSGBqjUyJwLTFgbvxie3w31uoWR1Y74r60rdxZqrR8q85t2W2MGCp12bm/KC3hyaSTiMhxuGrKcahqpbjOaDOoEhOEoFqJQCCJvqA85I6bfTdDjQlf2lbxVNlS6wt19yy7jRHZZlDnrinNj/6sHMhnNw2Ogco7O79e5fm/xQywRBBCEAuwn4gQ96bkYj4Vyuq9N1Z3Bj4Od5bs0MXt/dZZ21ctiqFan174q985P+Lfp+U1g7XDON/1ctP458WlVjLyJhOISZE0wM0S1QfuRC3lTjkJAKKEtNC9eIOhSh9xHLZOJRZTFuXDsEoStLkR/768ummsaJG9Pb9oe+9J+xaeSVokiQDSJphAo5uaBuWjiKP4QTqS1cUWU7ayesN66wu22frD1vmVW6GW6T8u9eVjGyZzs+w78Nqu0a2mbvVu1KEJQAgeZRL0liQYyx+GOmKeQpu0rMYsAJPNEFGD2dLodLIy6c9Ys7G8yeSUl3tf2/X3rcBVJSOv34l3sCBogi7z1LH/rBHjl4IJ93/ncQFAnjeImJD0Z8zuCwu9q3djDXqTlAKID5xv+9t2R8n8VcUFBljQ8Gyfe40BYBM4DwDLt8Kue79ZcFkbzfEdbUbv+oN4c9KTtsfm1MbYQqqh+2zrVZYKs/7Ef+byimt1POYiJhDhPBFBIiIEXhxfs7/dfYoIF+auBfYTE/pebx/V8hqBP2ODvD34yvuh/WCAmU75Bx6sIgaI/v5+6PV6JLqUsYr7dpDAoehs0h73pHTWrvKgThYbRSt9UmSjef3MpaUvBz4O72UmADgTOPJguGiZor+/HyUlJWBmJFz+D8xTtlUiOpbwpmrmrweeSXrT+g11k4SBN3RGKUcAVCVdFhyP1nreDbY//NPyEXUlU/Pp4XYycGT6V0Ux2WwWdO7cOZSWlkII8diX7SPPNgDaKdbxoNAxwATBAEkEEgSWCEQAqPAMwqvMdCEwMO0tVqZpWsGTT58+DaPR+PhGIYQAAAgh0P7B3ioW/B0iGiCGiwXbCuOHFSJys6AbYFye2T+xWhT3WYJEIoH/DQBMw3kes8OJPgAAAABJRU5ErkJggg==")}div.vis-network div.vis-edit-mode button.vis-button.vis-edit.vis-edit-mode{background-color:#fcfcfc;border:1px solid #ccc}div.vis-network div.vis-manipulation button.vis-button.vis-connect{background-image:url("data:image/png;base64,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/GMzLdsAAEEOaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjUtYzAyMSA3OS4xNTQ5MTEsIDIwMTMvMTAvMjktMTE6NDc6MTYgICAgICAgICI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgICAgICAgICAgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiCiAgICAgICAgICAgIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5BZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpPC94bXA6Q3JlYXRvclRvb2w+CiAgICAgICAgIDx4bXA6Q3JlYXRlRGF0ZT4yMDE0LTAxLTIyVDE5OjI0OjUxKzAxOjAwPC94bXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhtcDpNZXRhZGF0YURhdGU+MjAxNC0wMi0wNFQxNDozODo1NyswMTowMDwveG1wOk1ldGFkYXRhRGF0ZT4KICAgICAgICAgPHhtcDpNb2RpZnlEYXRlPjIwMTQtMDItMDRUMTQ6Mzg6NTcrMDE6MDA8L3htcDpNb2RpZnlEYXRlPgogICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3BuZzwvZGM6Zm9ybWF0PgogICAgICAgICA8eG1wTU06SW5zdGFuY2VJRD54bXAuaWlkOjlmYjUwMDU0LWE3ODEtMWQ0OC05ZTllLTU2ZWQ5YzhlYjdjNjwveG1wTU06SW5zdGFuY2VJRD4KICAgICAgICAgPHhtcE1NOkRvY3VtZW50SUQ+eG1wLmRpZDpFQTc2MkY5Njc0ODNFMzExOTQ4QkQxM0UyQkU3OTlBMTwveG1wTU06RG9jdW1lbnRJRD4KICAgICAgICAgPHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD54bXAuZGlkOjczQjYyQUFEOTE4M0UzMTE5NDhCRDEzRTJCRTc5OUExPC94bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ+CiAgICAgICAgIDx4bXBNTTpIaXN0b3J5PgogICAgICAgICAgICA8cmRmOlNlcT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+Y3JlYXRlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjczQjYyQUFEOTE4M0UzMTE5NDhCRDEzRTJCRTc5OUExPC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE0LTAxLTIyVDE5OjI0OjUxKzAxOjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ1M2IChXaW5kb3dzKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPnNhdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDppbnN0YW5jZUlEPnhtcC5paWQ6RUE2MEEyNEUxOTg0RTMxMUFEQUZFRkU2RUMzMzNFMDM8L3N0RXZ0Omluc3RhbmNlSUQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDp3aGVuPjIwMTQtMDEtMjNUMTk6MTg6MDcrMDE6MDA8L3N0RXZ0OndoZW4+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6Y2hhbmdlZD4vPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+c2F2ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0Omluc3RhbmNlSUQ+eG1wLmlpZDo3ZWRhMjI0MC0yYTQxLTNlNDQtYWM2My1iNzNiYTE5OWI3Y2E8L3N0RXZ0Omluc3RhbmNlSUQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDp3aGVuPjIwMTQtMDItMDRUMTQ6Mzg6NTcrMDE6MDA8L3N0RXZ0OndoZW4+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDQyAoV2luZG93cyk8L3N0RXZ0OnNvZnR3YXJlQWdlbnQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpjaGFuZ2VkPi88L3N0RXZ0OmNoYW5nZWQ+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5jb252ZXJ0ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnBhcmFtZXRlcnM+ZnJvbSBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIHRvIGltYWdlL3BuZzwvc3RFdnQ6cGFyYW1ldGVycz4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPmRlcml2ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnBhcmFtZXRlcnM+Y29udmVydGVkIGZyb20gYXBwbGljYXRpb24vdm5kLmFkb2JlLnBob3Rvc2hvcCB0byBpbWFnZS9wbmc8L3N0RXZ0OnBhcmFtZXRlcnM+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjlmYjUwMDU0LWE3ODEtMWQ0OC05ZTllLTU2ZWQ5YzhlYjdjNjwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OndoZW4+MjAxNC0wMi0wNFQxNDozODo1NyswMTowMDwvc3RFdnQ6d2hlbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+Lzwvc3RFdnQ6Y2hhbmdlZD4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOlNlcT4KICAgICAgICAgPC94bXBNTTpIaXN0b3J5PgogICAgICAgICA8eG1wTU06RGVyaXZlZEZyb20gcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICA8c3RSZWY6aW5zdGFuY2VJRD54bXAuaWlkOjdlZGEyMjQwLTJhNDEtM2U0NC1hYzYzLWI3M2JhMTk5YjdjYTwvc3RSZWY6aW5zdGFuY2VJRD4KICAgICAgICAgICAgPHN0UmVmOmRvY3VtZW50SUQ+eG1wLmRpZDpFQTc2MkY5Njc0ODNFMzExOTQ4QkQxM0UyQkU3OTlBMTwvc3RSZWY6ZG9jdW1lbnRJRD4KICAgICAgICAgICAgPHN0UmVmOm9yaWdpbmFsRG9jdW1lbnRJRD54bXAuZGlkOjczQjYyQUFEOTE4M0UzMTE5NDhCRDEzRTJCRTc5OUExPC9zdFJlZjpvcmlnaW5hbERvY3VtZW50SUQ+CiAgICAgICAgIDwveG1wTU06RGVyaXZlZEZyb20+CiAgICAgICAgIDxwaG90b3Nob3A6Q29sb3JNb2RlPjM8L3Bob3Rvc2hvcDpDb2xvck1vZGU+CiAgICAgICAgIDxwaG90b3Nob3A6SUNDUHJvZmlsZT5zUkdCIElFQzYxOTY2LTIuMTwvcGhvdG9zaG9wOklDQ1Byb2ZpbGU+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjcyMDA5MC8xMDAwMDwvdGlmZjpYUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6WVJlc29sdXRpb24+NzIwMDkwLzEwMDAwPC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjI0PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjI0PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz4ubxs+AAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAUtSURBVHjajJZ/bNT1Gcdfz/P53PV6B4W7VltLqdAaplIOiMOoyxxJCSs/Gv/yB4gzJroAosmmDklwkYWR0bQsdmkykoojTpcsWYLxD/lRZdMQkTHRtkLZRqG0tIVe7662vTu43n32x/VKZ/jh89cn38/zvN7P5/l88zwf2blzJz6fDwARQUSm1n8s31CM0/VAnbNmsUPuAsDpgEO+Bg4C7//iyv5hvmMiQiqVQpqamvB6vVNwEeG1JZtCBrYi/MrkAwDNgjhwAlbzICBLA0rDb0+/839C6XQaaWxspLCw8Dp86cbNmqVFJQddE6KzdjZ9D89g+B6fSyCOcyn1nxil+O9xKg5HqWFSHGXLjrP7W/ICqVQK2bNnDz6fDxFh65KNvxbHDhF4rJj2bXPo+IGfcW5h5xL4f99P+FCEMIAob75x9t0dAMlkElNXV4e1lteXbNqiQoMaeOFOjrdU868SD2luYyEP6dUh+sYmSHeOU6GO5Z8VLx5+NNZxIpPJ5AS2L3upROCoCvz8Lo7vnkf77cAHhpiz/zIL9vWz8L8p/NvupmM0Q7pjnAoLqz8tDrc8MnQqYVUVhVdF4LEg7b+rvDn8wDDlH0WoPpukLJImSBaMwjcJqmwWts2jPZLG/8kwYVFeVdXXZcFf4yVDc2cNKfBFmD9X+0ncCP58F48eG+Feo2CAUkvs4dl0V/uJvdXLiiV+ut++n7YLSfxPfMMG54ChzB3WIesVWB2i82bw1AR6fJR7C4VsfYiv6u/k3A9nEgP4zXke8DiYHyAOMK+QxPIgnZ9GqSHr1itQJ8DK2fTerDQ+S/bHRXQJaHSCwNIZ2Xh+7+S3VAmwNMBA/tuPZtErgKquUmdMWIFlRURvdamRNEXGwIWrlP47pTMzLiunxghGMwTLvcTWlHAp77s4QNSrYMQtss6ZMgWqCm5cHoDHO1nbk6K8zEN8+3zatv2Hn1b59EqJZdxmYUERg9P9KwpIiAOTdWUWBXuLzB/vZG3P1Un4PNp2d1MbmyD45TWCxuCsQm0x56bHGHFYEZwxok7toAA9Sfw3hCcoL/NOwi9QO5wmWO1j4JEgZxTkodmcWRGkf3pcX0r8xoAaBixKu4U5/xwndM+0tpAvS6mP+PZK2nb1UBvPEKwKMLDvPj4ESGc55lGy303sdJKQdZB2rkMdctAB/4gzN+/Q2ENNd4LyUi/xN+bTtquX2thk5nk4wI3gAF+OMNcA1nFQDfK+BY5GqbkwWabTY5QZhXWlnNx1ntrY1Rz87fuvw29m/Sn8J+PUGAFj5T19baA1IspuBZp7cx1x4SwG1cEf+lgRSROs8jGwb+Ht4QB/GSSsAhYano39LWIBxNEIbP14hPDuiyS2VtJuHXQlKKvxM/jiXDq/D/xPlwifGMkJZB2NIoKpr69nxeiZxLHicFSFVWfGqBidIP3LSjrWltD94CyufF/4kQgPuVz2Lz93+dDRa9eu5QQ8Hg8/iXee+Dy4CKMs7xqn4nwKz9IirhQqmVuB42m8ey+x7LMoD6iAON782eChhqmRuXfvXgKBAKqKqtI0/8nNKrQI4BVYXkzHgzPpC88gWuHL/caXrhLoGiN0apSKr0ZZRBZM7q2w5ZnLR1oAnHOMjY0hra2tFBQUYIyZmstvVT1Z6eDlAuEVq7merxmwueNPDXy9PvybjKP5mctHLk4/XTKZRJqbm/H7/VNw1VyEMYbW4FN3WNWnnchKoy5sHeVGBRX6VWi3ymFx7r11Ix8MTX/y5C2RSPC/AQB61erowbpqSwAAAABJRU5ErkJggg==")}div.vis-network div.vis-manipulation button.vis-button.vis-delete{background-image:url("data:image/png;base64,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/GMzLdsAAEEOaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjUtYzAyMSA3OS4xNTQ5MTEsIDIwMTMvMTAvMjktMTE6NDc6MTYgICAgICAgICI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgICAgICAgICAgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiCiAgICAgICAgICAgIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5BZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpPC94bXA6Q3JlYXRvclRvb2w+CiAgICAgICAgIDx4bXA6Q3JlYXRlRGF0ZT4yMDE0LTAxLTIyVDE5OjI0OjUxKzAxOjAwPC94bXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhtcDpNZXRhZGF0YURhdGU+MjAxNC0wMi0wNFQxNDo0MTowNCswMTowMDwveG1wOk1ldGFkYXRhRGF0ZT4KICAgICAgICAgPHhtcDpNb2RpZnlEYXRlPjIwMTQtMDItMDRUMTQ6NDE6MDQrMDE6MDA8L3htcDpNb2RpZnlEYXRlPgogICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3BuZzwvZGM6Zm9ybWF0PgogICAgICAgICA8eG1wTU06SW5zdGFuY2VJRD54bXAuaWlkOjc3NDkzYmUxLTEyZGItOTg0NC1iNDYyLTg2NGVmNGIzMzM3MTwveG1wTU06SW5zdGFuY2VJRD4KICAgICAgICAgPHhtcE1NOkRvY3VtZW50SUQ+eG1wLmRpZDpFQTc2MkY5Njc0ODNFMzExOTQ4QkQxM0UyQkU3OTlBMTwveG1wTU06RG9jdW1lbnRJRD4KICAgICAgICAgPHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD54bXAuZGlkOjczQjYyQUFEOTE4M0UzMTE5NDhCRDEzRTJCRTc5OUExPC94bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ+CiAgICAgICAgIDx4bXBNTTpIaXN0b3J5PgogICAgICAgICAgICA8cmRmOlNlcT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+Y3JlYXRlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjczQjYyQUFEOTE4M0UzMTE5NDhCRDEzRTJCRTc5OUExPC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE0LTAxLTIyVDE5OjI0OjUxKzAxOjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ1M2IChXaW5kb3dzKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPnNhdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDppbnN0YW5jZUlEPnhtcC5paWQ6RUE2MEEyNEUxOTg0RTMxMUFEQUZFRkU2RUMzMzNFMDM8L3N0RXZ0Omluc3RhbmNlSUQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDp3aGVuPjIwMTQtMDEtMjNUMTk6MTg6MDcrMDE6MDA8L3N0RXZ0OndoZW4+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6Y2hhbmdlZD4vPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+c2F2ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0Omluc3RhbmNlSUQ+eG1wLmlpZDowNmE3NWYwMy04MDdhLWUzNGYtYjk1Zi1jZGU2MjM0Mzg4OGY8L3N0RXZ0Omluc3RhbmNlSUQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDp3aGVuPjIwMTQtMDItMDRUMTQ6NDE6MDQrMDE6MDA8L3N0RXZ0OndoZW4+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDQyAoV2luZG93cyk8L3N0RXZ0OnNvZnR3YXJlQWdlbnQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpjaGFuZ2VkPi88L3N0RXZ0OmNoYW5nZWQ+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5jb252ZXJ0ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnBhcmFtZXRlcnM+ZnJvbSBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIHRvIGltYWdlL3BuZzwvc3RFdnQ6cGFyYW1ldGVycz4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPmRlcml2ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnBhcmFtZXRlcnM+Y29udmVydGVkIGZyb20gYXBwbGljYXRpb24vdm5kLmFkb2JlLnBob3Rvc2hvcCB0byBpbWFnZS9wbmc8L3N0RXZ0OnBhcmFtZXRlcnM+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjc3NDkzYmUxLTEyZGItOTg0NC1iNDYyLTg2NGVmNGIzMzM3MTwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OndoZW4+MjAxNC0wMi0wNFQxNDo0MTowNCswMTowMDwvc3RFdnQ6d2hlbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+Lzwvc3RFdnQ6Y2hhbmdlZD4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOlNlcT4KICAgICAgICAgPC94bXBNTTpIaXN0b3J5PgogICAgICAgICA8eG1wTU06RGVyaXZlZEZyb20gcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICA8c3RSZWY6aW5zdGFuY2VJRD54bXAuaWlkOjA2YTc1ZjAzLTgwN2EtZTM0Zi1iOTVmLWNkZTYyMzQzODg4Zjwvc3RSZWY6aW5zdGFuY2VJRD4KICAgICAgICAgICAgPHN0UmVmOmRvY3VtZW50SUQ+eG1wLmRpZDpFQTc2MkY5Njc0ODNFMzExOTQ4QkQxM0UyQkU3OTlBMTwvc3RSZWY6ZG9jdW1lbnRJRD4KICAgICAgICAgICAgPHN0UmVmOm9yaWdpbmFsRG9jdW1lbnRJRD54bXAuZGlkOjczQjYyQUFEOTE4M0UzMTE5NDhCRDEzRTJCRTc5OUExPC9zdFJlZjpvcmlnaW5hbERvY3VtZW50SUQ+CiAgICAgICAgIDwveG1wTU06RGVyaXZlZEZyb20+CiAgICAgICAgIDxwaG90b3Nob3A6Q29sb3JNb2RlPjM8L3Bob3Rvc2hvcDpDb2xvck1vZGU+CiAgICAgICAgIDxwaG90b3Nob3A6SUNDUHJvZmlsZT5zUkdCIElFQzYxOTY2LTIuMTwvcGhvdG9zaG9wOklDQ1Byb2ZpbGU+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjcyMDA5MC8xMDAwMDwvdGlmZjpYUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6WVJlc29sdXRpb24+NzIwMDkwLzEwMDAwPC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjI0PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjI0PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz4aYJzYAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAYGSURBVHjalJZ7UJTnFcZ/73m/72PdJY1RbhoQp6lkXRAvmIYxdCUadLVOozPNtGObap1JsKipjiShbdoRbeKEiQHpQK3xj0xa03aamTbaTGyAYV1QGeqFi+JyiZFLAlmESBkWRmS3fyzslGkmnZ5/v/M873Oe75zzvqqoqAibzQaAiKCUAkApRdHIK/NFsx2NR91nOSILADDoJyzNaM4xxbtvPHh0iC+JiYkJ1OHDh4mJiUEpFSXPv/ziPC28TIiXDCOSrAClQDSEpsCwJPIhrEBRQpiSytXlQwDhcBilFPfu3UMVFxdjt9ujFTzfcLBADCoEEAFr1ZbrrNjch2vtEImPBgHob7fTcWE+bVXJNJ/NiFQlEGLvieXHKmYqGB8fRx05cgSbzYaIsPvywV8pKFaA7fGtLTzz61YWpo/xVTHQbufsq5lcez9zWuWhk5mvFwMEg0H0+vXrMU2Tn1wp3CtCiQ5DjGd3A/m/v8IDCZP8r4iNmyRrWx/j/5qktykZpXKzAjVDVxPzGqemptDr1q1jX3NRnIJarcDKK2hgR2ULXRfncv7UYv7xpovhnhiW5Mz+kefeSKO6LJ1A1xzEuk/Ojm4mRibpuZaMZW3OCtRUND60NmiICCIUShisx7a2sLMiQn4s77uEQgIabnqdfHIlgT1/qQeg8vs5dHhdCNB1wYn3RIiC995j26stjAbsNH+YiZJCESnS1Y/XxIXu8r4YIPv/VkVs3CTnTy2ms34xro1+sp9po6sxlTu34ultmsPVvy6is86FCHgO+DDs49zpjufBpCG+seYOC9OHaTidieicb9ouVAhKtouAseI710ma7pLuqwmgYfHqAFt+6WdLoQ/LBl11Lm7VudAa8vb72PCin9TlAWIsGGhLACD+kSAZnusYBii1XQAPYWDllt6ov2lrBkDBR2+6Ofuak2//3M+G/T4wAAPW7fPhKfRTVeqk9qQbFKRmDUTxS3N7QYGYmwzCkqklBGlPDEcTNv+sg9tNCbTXuvBWujE0bHrZj9JE1B/wU1Pm5PwJN6YBS9a2kVvQEcWnrh5GTFD3lxkYkqRMgYQlwVldUvDnen73LHTUuqitdKM0eAr9AFQfd1J/yo2aJn+2sn4Wdn5qEFODJskgBIjx5T0uCrQA08pnIjS9PERDjPnfOKXAMEBECUoGEIHBj+2zkt76UQ6dXheGAev3+cg74Kf6uJPqcicbfuond7cPy4SOiy7+tD9nFvZurx00KOk3CNEC+mE+vjSPBc7IWqgqTaPT60IMcO/xsXGa3HfKjRgRdbl7/KDg0jtubje6aHj7c7J3dgLQ2zoPwwQ91SooOQdAW1VKVMHty0kA5Bb48BycJn/LjWFGbLv4thvvb53kFvjJ+XEdWkPfjQVR/CcNKYgGMc8JWt5Fa2j+MIPPuyI2pa4IoHSkt6vLIuRaQ9q32khzt4GCxtNu6k46GeiIR2lIfDQQsafPzq1LGRGL9Gk9d+vrwewvfHPQOoexQVjxdB/auk/zmaUMdsfz6bVUtIalT7bxveP1ZHh6GPDPYeSzeD69kcpIfxymFWLNrka+ljhBTWkWwz2JiJT84YHnz2iPx0P20PkmRF5i6HYiwZFJsn/YzdezbzE3cQibY5xV266z6RfXohakb+xB9CjanCD9qTbW7Grk4WV38VZm0l6dhQiEw9taHSuDqrS0FIfDwXM3X9mHMsvRAk/sauDpQy38P+GtzOTGB9mEpkD0C2dS8n8zOjqK9ng8WJZFU+JTjasGvaCNXPpvJBPoMlm0OoDNMfWVxONfWNSUPUZ7TUQ56tCZlPwSgMnJSVRpaSmxsbFE1raw82ZxAZZRQUiBYUKGp5UlOX2krBzmoUVjiIKhHge9rfPo+Wcy3ZeXIYASgL1/X5RfMXMvj46OosrLy7HZbGitUUohIuzoem0RofALaOsghgWGjky0MiJTL8b0lOvI8hN1DKXKP0jd3TNTWDgcJhgMoo4ePYrD4Yi+KmaeLlprnrtXFo9h/AAlG1AqE8yFmBrC+jO0bgH9EVpO/1F2Dc5g//OAsbEx/j0Af+USsQynL1UAAAAASUVORK5CYII=")}div.vis-network div.vis-edit-mode div.vis-label,div.vis-network div.vis-manipulation div.vis-label{margin:0 0 0 23px;line-height:25px}div.vis-network div.vis-manipulation div.vis-separator-line{float:left;display:inline-block;width:1px;height:21px;background-color:#bdbdbd;margin:0 7px 0 15px} \ No newline at end of file diff --git a/queue_job/static/lib/vis/vis-network.min.js b/queue_job/static/lib/vis/vis-network.min.js new file mode 100644 index 0000000000..aa1897181e --- /dev/null +++ b/queue_job/static/lib/vis/vis-network.min.js @@ -0,0 +1,27 @@ +/** + * vis-network + * https://visjs.github.io/vis-network/ + * + * A dynamic, browser-based visualization library. + * + * @version 9.0.4 + * @date 2021-03-16T05:44:27.440Z + * + * @copyright (c) 2011-2017 Almende B.V, http://almende.com + * @copyright (c) 2017-2019 visjs contributors, https://github.com/visjs + * + * @license + * vis.js is dual licensed under both + * + * 1. The Apache 2.0 License + * http://www.apache.org/licenses/LICENSE-2.0 + * + * and + * + * 2. The MIT License + * http://opensource.org/licenses/MIT + * + * vis.js may be distributed under either license. + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).vis=t.vis||{})}(this,(function(t){"use strict";var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function i(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}function n(t,e){return t(e={exports:{}},e.exports),e.exports}var o=function(t){return t&&t.Math==Math&&t},r=o("object"==typeof globalThis&&globalThis)||o("object"==typeof window&&window)||o("object"==typeof self&&self)||o("object"==typeof e&&e)||function(){return this}()||Function("return this")(),s=function(t){try{return!!t()}catch(t){return!0}},a=!s((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]})),h={}.propertyIsEnumerable,l=Object.getOwnPropertyDescriptor,d={f:l&&!h.call({1:2},1)?function(t){var e=l(this,t);return!!e&&e.enumerable}:h},c=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}},u={}.toString,f=function(t){return u.call(t).slice(8,-1)},p="".split,v=s((function(){return!Object("z").propertyIsEnumerable(0)}))?function(t){return"String"==f(t)?p.call(t,""):Object(t)}:Object,g=function(t){if(null==t)throw TypeError("Can't call method on "+t);return t},y=function(t){return v(g(t))},m=function(t){return"object"==typeof t?null!==t:"function"==typeof t},b=function(t,e){if(!m(t))return t;var i,n;if(e&&"function"==typeof(i=t.toString)&&!m(n=i.call(t)))return n;if("function"==typeof(i=t.valueOf)&&!m(n=i.call(t)))return n;if(!e&&"function"==typeof(i=t.toString)&&!m(n=i.call(t)))return n;throw TypeError("Can't convert object to primitive value")},w={}.hasOwnProperty,k=function(t,e){return w.call(t,e)},_=r.document,x=m(_)&&m(_.createElement),E=function(t){return x?_.createElement(t):{}},O=!a&&!s((function(){return 7!=Object.defineProperty(E("div"),"a",{get:function(){return 7}}).a})),C=Object.getOwnPropertyDescriptor,S={f:a?C:function(t,e){if(t=y(t),e=b(e,!0),O)try{return C(t,e)}catch(t){}if(k(t,e))return c(!d.f.call(t,e),t[e])}},T=/#|\.prototype\./,M=function(t,e){var i=D[P(t)];return i==B||i!=I&&("function"==typeof e?s(e):!!e)},P=M.normalize=function(t){return String(t).replace(T,".").toLowerCase()},D=M.data={},I=M.NATIVE="N",B=M.POLYFILL="P",z=M,N={},A=function(t){if("function"!=typeof t)throw TypeError(String(t)+" is not a function");return t},F=function(t,e,i){if(A(t),void 0===e)return t;switch(i){case 0:return function(){return t.call(e)};case 1:return function(i){return t.call(e,i)};case 2:return function(i,n){return t.call(e,i,n)};case 3:return function(i,n,o){return t.call(e,i,n,o)}}return function(){return t.apply(e,arguments)}},j=function(t){if(!m(t))throw TypeError(String(t)+" is not an object");return t},R=Object.defineProperty,L={f:a?R:function(t,e,i){if(j(t),e=b(e,!0),j(i),O)try{return R(t,e,i)}catch(t){}if("get"in i||"set"in i)throw TypeError("Accessors not supported");return"value"in i&&(t[e]=i.value),t}},H=a?function(t,e,i){return L.f(t,e,c(1,i))}:function(t,e,i){return t[e]=i,t},W=S.f,q=function(t){var e=function(e,i,n){if(this instanceof t){switch(arguments.length){case 0:return new t;case 1:return new t(e);case 2:return new t(e,i)}return new t(e,i,n)}return t.apply(this,arguments)};return e.prototype=t.prototype,e},V=function(t,e){var i,n,o,s,a,h,l,d,c=t.target,u=t.global,f=t.stat,p=t.proto,v=u?r:f?r[c]:(r[c]||{}).prototype,g=u?N:N[c]||(N[c]={}),y=g.prototype;for(o in e)i=!z(u?o:c+(f?".":"#")+o,t.forced)&&v&&k(v,o),a=g[o],i&&(h=t.noTargetGet?(d=W(v,o))&&d.value:v[o]),s=i&&h?h:e[o],i&&typeof a==typeof s||(l=t.bind&&i?F(s,r):t.wrap&&i?q(s):p&&"function"==typeof s?F(Function.call,s):s,(t.sham||s&&s.sham||a&&a.sham)&&H(l,"sham",!0),g[o]=l,p&&(k(N,n=c+"Prototype")||H(N,n,{}),N[n][o]=s,t.real&&y&&!y[o]&&H(y,o,s)))},U=Math.ceil,Y=Math.floor,X=function(t){return isNaN(t=+t)?0:(t>0?Y:U)(t)},G=Math.min,K=function(t){return t>0?G(X(t),9007199254740991):0},Q=Math.max,$=Math.min,Z=function(t,e){var i=X(t);return i<0?Q(i+e,0):$(i,e)},J=function(t){return function(e,i,n){var o,r=y(e),s=K(r.length),a=Z(n,s);if(t&&i!=i){for(;s>a;)if((o=r[a++])!=o)return!0}else for(;s>a;a++)if((t||a in r)&&r[a]===i)return t||a||0;return!t&&-1}},tt={includes:J(!0),indexOf:J(!1)},et={},it=tt.indexOf,nt=function(t,e){var i,n=y(t),o=0,r=[];for(i in n)!k(et,i)&&k(n,i)&&r.push(i);for(;e.length>o;)k(n,i=e[o++])&&(~it(r,i)||r.push(i));return r},ot=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],rt=Object.keys||function(t){return nt(t,ot)},st={f:Object.getOwnPropertySymbols},at=function(t){return Object(g(t))},ht=Object.assign,lt=Object.defineProperty,dt=!ht||s((function(){if(a&&1!==ht({b:1},ht(lt({},"a",{enumerable:!0,get:function(){lt(this,"b",{value:3,enumerable:!1})}}),{b:2})).b)return!0;var t={},e={},i=Symbol(),n="abcdefghijklmnopqrst";return t[i]=7,n.split("").forEach((function(t){e[t]=t})),7!=ht({},t)[i]||rt(ht({},e)).join("")!=n}))?function(t,e){for(var i=at(t),n=arguments.length,o=1,r=st.f,s=d.f;n>o;)for(var h,l=v(arguments[o++]),c=r?rt(l).concat(r(l)):rt(l),u=c.length,f=0;u>f;)h=c[f++],a&&!s.call(l,h)||(i[h]=l[h]);return i}:ht;V({target:"Object",stat:!0,forced:Object.assign!==dt},{assign:dt});var ct=N.Object.assign,ut=[].slice,ft={},pt=function(t,e,i){if(!(e in ft)){for(var n=[],o=0;o=.1;)(p=+r[c++%s])>d&&(p=d),f=Math.sqrt(p*p/(1+l*l)),e+=f=a<0?-f:f,i+=l*f,!0===u?t.lineTo(e,i):t.moveTo(e,i),d-=p,u=!u}var Ot={circle:wt,dashedLine:Et,database:xt,diamond:function(t,e,i,n){t.beginPath(),t.lineTo(e,i+n),t.lineTo(e+n,i),t.lineTo(e,i-n),t.lineTo(e-n,i),t.closePath()},ellipse:_t,ellipse_vis:_t,hexagon:function(t,e,i,n){t.beginPath();var o=2*Math.PI/6;t.moveTo(e+n,i);for(var r=1;r<6;r++)t.lineTo(e+n*Math.cos(o*r),i+n*Math.sin(o*r));t.closePath()},roundRect:kt,square:function(t,e,i,n){t.beginPath(),t.rect(e-n,i-n,2*n,2*n),t.closePath()},star:function(t,e,i,n){t.beginPath(),i+=.1*(n*=.82);for(var o=0;o<10;o++){var r=o%2==0?1.3*n:.5*n;t.lineTo(e+r*Math.sin(2*o*Math.PI/10),i-r*Math.cos(2*o*Math.PI/10))}t.closePath()},triangle:function(t,e,i,n){t.beginPath(),i+=.275*(n*=1.15);var o=2*n,r=o/2,s=Math.sqrt(3)/6*o,a=Math.sqrt(o*o-r*r);t.moveTo(e,i-(a-s)),t.lineTo(e+r,i+s),t.lineTo(e-r,i+s),t.lineTo(e,i-(a-s)),t.closePath()},triangleDown:function(t,e,i,n){t.beginPath(),i-=.275*(n*=1.15);var o=2*n,r=o/2,s=Math.sqrt(3)/6*o,a=Math.sqrt(o*o-r*r);t.moveTo(e,i+(a-s)),t.lineTo(e+r,i-s),t.lineTo(e-r,i-s),t.lineTo(e,i+(a-s)),t.closePath()}};var Ct=n((function(t){function e(t){if(t)return function(t){for(var i in e.prototype)t[i]=e.prototype[i];return t}(t)}t.exports=e,e.prototype.on=e.prototype.addEventListener=function(t,e){return this._callbacks=this._callbacks||{},(this._callbacks["$"+t]=this._callbacks["$"+t]||[]).push(e),this},e.prototype.once=function(t,e){function i(){this.off(t,i),e.apply(this,arguments)}return i.fn=e,this.on(t,i),this},e.prototype.off=e.prototype.removeListener=e.prototype.removeAllListeners=e.prototype.removeEventListener=function(t,e){if(this._callbacks=this._callbacks||{},0==arguments.length)return this._callbacks={},this;var i,n=this._callbacks["$"+t];if(!n)return this;if(1==arguments.length)return delete this._callbacks["$"+t],this;for(var o=0;o=a?t?"":void 0:(n=r.charCodeAt(s))<55296||n>56319||s+1===a||(o=r.charCodeAt(s+1))<56320||o>57343?t?r.charAt(s):n:t?r.slice(s,s+2):o-56320+(n-55296<<10)+65536}},Tt={codeAt:St(!1),charAt:St(!0)},Mt="__core-js_shared__",Pt=r[Mt]||function(t,e){try{H(r,t,e)}catch(i){r[t]=e}return e}(Mt,{}),Dt=Function.toString;"function"!=typeof Pt.inspectSource&&(Pt.inspectSource=function(t){return Dt.call(t)});var It,Bt,zt,Nt=Pt.inspectSource,At=r.WeakMap,Ft="function"==typeof At&&/native code/.test(Nt(At)),jt=n((function(t){(t.exports=function(t,e){return Pt[t]||(Pt[t]=void 0!==e?e:{})})("versions",[]).push({version:"3.9.1",mode:"pure",copyright:"© 2021 Denis Pushkarev (zloirock.ru)"})})),Rt=0,Lt=Math.random(),Ht=function(t){return"Symbol("+String(void 0===t?"":t)+")_"+(++Rt+Lt).toString(36)},Wt=jt("keys"),qt=function(t){return Wt[t]||(Wt[t]=Ht(t))},Vt=r.WeakMap;if(Ft){var Ut=Pt.state||(Pt.state=new Vt),Yt=Ut.get,Xt=Ut.has,Gt=Ut.set;It=function(t,e){return e.facade=t,Gt.call(Ut,t,e),e},Bt=function(t){return Yt.call(Ut,t)||{}},zt=function(t){return Xt.call(Ut,t)}}else{var Kt=qt("state");et[Kt]=!0,It=function(t,e){return e.facade=t,H(t,Kt,e),e},Bt=function(t){return k(t,Kt)?t[Kt]:{}},zt=function(t){return k(t,Kt)}}var Qt,$t,Zt={set:It,get:Bt,has:zt,enforce:function(t){return zt(t)?Bt(t):It(t,{})},getterFor:function(t){return function(e){var i;if(!m(e)||(i=Bt(e)).type!==t)throw TypeError("Incompatible receiver, "+t+" required");return i}}},Jt=!s((function(){function t(){}return t.prototype.constructor=null,Object.getPrototypeOf(new t)!==t.prototype})),te=qt("IE_PROTO"),ee=Object.prototype,ie=Jt?Object.getPrototypeOf:function(t){return t=at(t),k(t,te)?t[te]:"function"==typeof t.constructor&&t instanceof t.constructor?t.constructor.prototype:t instanceof Object?ee:null},ne="process"==f(r.process),oe=function(t){return"function"==typeof t?t:void 0},re=function(t,e){return arguments.length<2?oe(N[t])||oe(r[t]):N[t]&&N[t][e]||r[t]&&r[t][e]},se=re("navigator","userAgent")||"",ae=r.process,he=ae&&ae.versions,le=he&&he.v8;le?$t=(Qt=le.split("."))[0]+Qt[1]:se&&(!(Qt=se.match(/Edge\/(\d+)/))||Qt[1]>=74)&&(Qt=se.match(/Chrome\/(\d+)/))&&($t=Qt[1]);var de,ce,ue,fe=$t&&+$t,pe=!!Object.getOwnPropertySymbols&&!s((function(){return!Symbol.sham&&(ne?38===fe:fe>37&&fe<41)})),ve=pe&&!Symbol.sham&&"symbol"==typeof Symbol.iterator,ge=jt("wks"),ye=r.Symbol,me=ve?ye:ye&&ye.withoutSetter||Ht,be=function(t){return k(ge,t)&&(pe||"string"==typeof ge[t])||(pe&&k(ye,t)?ge[t]=ye[t]:ge[t]=me("Symbol."+t)),ge[t]},we=be("iterator"),ke=!1;[].keys&&("next"in(ue=[].keys())?(ce=ie(ie(ue)))!==Object.prototype&&(de=ce):ke=!0);var _e=null==de||s((function(){var t={};return de[we].call(t)!==t}));_e&&(de={}),_e&&!k(de,we)&&H(de,we,(function(){return this}));var xe,Ee={IteratorPrototype:de,BUGGY_SAFARI_ITERATORS:ke},Oe=a?Object.defineProperties:function(t,e){j(t);for(var i,n=rt(e),o=n.length,r=0;o>r;)L.f(t,i=n[r++],e[i]);return t},Ce=re("document","documentElement"),Se=qt("IE_PROTO"),Te=function(){},Me=function(t){return"