Metadata-Version: 1.1
Name: django-task
Version: 1.5.0
Summary: A Django app to run new background tasks from either admin or cron, and inspect task history from admin; based on django-rq
Home-page: https://github.com/morlandi/django-task
Author: Mario Orlandi
Author-email: morlandi@brainstorm.it
License: MIT
Description-Content-Type: text/x-rst
Description: ===========
        django-task
        ===========
        
        .. image:: https://badge.fury.io/py/django-task.svg
            :target: https://badge.fury.io/py/django-task
        
        .. image:: https://travis-ci.org/morlandi/django-task.svg?branch=master
            :target: https://travis-ci.org/morlandi/django-task
        
        .. image:: https://codecov.io/gh/morlandi/django-task/branch/master/graph/badge.svg
            :target: https://codecov.io/gh/morlandi/django-task
        
        A Django app to run new background tasks from either admin or cron, and inspect task history from admin; based on django-rq
        
        .. contents::
        
        .. sectnum::
        
        Quickstart
        ----------
        
        1) **Install Django Task**:
        
        .. code-block:: bash
        
            pip install django-task
        
        2) **Add it to your `INSTALLED_APPS`**:
        
        .. code-block:: python
        
            INSTALLED_APPS = (
                ...
                'django_rq',
                'django_task',
                ...
            )
        
        3) **Add Django Task's URL patterns**:
        
        .. code-block:: python
        
            urlpatterns = [
                ...
                path('django_task/', include('django_task.urls', namespace='django_task')),
                ...
            ]
        
        4) **Configure Redis and RQ in settings.py**; example:
        
        .. code-block:: python
        
            #REDIS_URL = 'redis://localhost:6379/0'
            redis_host = os.environ.get('REDIS_HOST', 'localhost')
            redis_port = 6379
            REDIS_URL = 'redis://%s:%d/0' % (redis_host, redis_port)
        
            CACHES = {
                'default': {
                    'BACKEND': 'redis_cache.RedisCache',
                    'LOCATION': REDIS_URL
                },
            }
        
            #
            # RQ config
            #
        
            RQ_PREFIX = "myproject_"
            QUEUE_DEFAULT = RQ_PREFIX + 'default'
            QUEUE_HIGH = RQ_PREFIX + 'high'
            QUEUE_LOW = RQ_PREFIX + 'low'
        
            RQ_QUEUES = {
                QUEUE_DEFAULT: {
                    'URL': REDIS_URL,
                    #'PASSWORD': 'some-password',
                    'DEFAULT_TIMEOUT': 360,
                },
                QUEUE_HIGH: {
                    'URL': REDIS_URL,
                    'DEFAULT_TIMEOUT': 500,
                },
                QUEUE_LOW: {
                    'URL': REDIS_URL,
                    #'ASYNC': False,
                },
            }
        
        5) **or, if you plan to install many instances of the project on the same server**:
        
        .. code-block:: python
        
            #
            # RQ config
            #
        
            QUEUE_DEFAULT = 'default'
            QUEUE_LOW = 'low'
            QUEUE_HIGH = 'high'
        
            def rq_queue_name(prefix, name):
                return prefix + '_' + name
        
            def setup_rq_queues(prefix):
                """
                Purposes:
                    - setup RQ_PREFIX setting for later inspection
                    - setup RQ_QUEUES dictionary with instance-specific queues
        
                Invoke once from local.py providing an instance specific prefix;
                example:
        
                    RQ_PREFIX = "myproject"
                    RQ_QUEUES = setup_rq_queues(RQ_PREFIX)
        
                Alternatively, provide a fully customized RQ_QUEUES dictionary in local.py
                """
                data = {
                    QUEUE_DEFAULT: {
                        'URL': REDIS_URL,
                        #'PASSWORD': 'some-password',
                        #'DEFAULT_TIMEOUT': 5 * 60,
                        'DEFAULT_TIMEOUT': -1,  # -1 means infinite
                    },
                    QUEUE_LOW: {
                        'URL': REDIS_URL,
                        #'ASYNC': False,
                    },
                    QUEUE_HIGH: {
                        'URL': REDIS_URL,
                        'DEFAULT_TIMEOUT': 500,
                    },
                }
        
                queues = {rq_queue_name(prefix, key): value for key, value in data.items()}
                return queues
        
        then, in your "local.py":
        
        .. code-block:: python
        
            #
            # RQ configuration
            #
        
            RQ_PREFIX = "project_instance_XYZ"
            RQ_QUEUES = setup_rq_queues(RQ_PREFIX)
        
            print('RQ_QUEUES: ')
            print(RQ_QUEUES)
        
        
        
        6) **Customize django-task specific settings (optional)**:
        
        .. code-block:: python
        
            RQ_SHOW_ADMIN_LINK = False
            DJANGOTASK_LOG_ROOT = os.path.abspath(os.path.join(BASE_DIR, '..', 'protected', 'tasklog'))
            DJANGOTASK_ALWAYS_EAGER = False
            DJANGOTASK_JOB_TRACE_ENABLED = False
            DJANGOTASK_REJECT_IF_NO_WORKER_ACTIVE_FOR_QUEUE = True
        
        7) **Optionally, revoke pending tasks at startapp**;
        
        file `main/apps.py`:
        
        .. code-block:: python
        
            class MainConfig(AppConfig):
        
                ...
        
                def ready(self):
        
                    ...
                    try:
                        from django_task.utils import revoke_pending_tasks
                        revoke_pending_tasks()
                    except Exception as e:
                        print(e)
        
        Features
        --------
        
        **Purposes**
        
        - create async tasks either programmatically or from admin
        - monitor async tasks from admin
        - log all tasks in the database for later inspection
        - optionally save task-specific logs in a TextField and/or in a FileField
        
        **Details**
        
        1. each specific job is described my a Model derived from models.Task, which
           is responsible for:
        
           - selecting the name for the consumer queue among available queues
           - collecting and saving all parameters required by the associated job
           - running the specific job asyncronously
        
        2. a new job can be run either:
        
           - creating a Task from the Django admin
           - creating a Task from code, then calling Task.run()
        
        3. job execution workflow:
        
           - job execution is triggered by task.run(is_async)
           - job will receive the task.id, and retrieve paramerts from it (task.retrieve_params_as_dict())
           - on start, job will update task status to 'STARTED' and save job.id for reference
           - during execution, the job can update the progress indicator
           - on completion, task status is finally updated to either 'SUCCESS' or 'FAILURE'
           - See example.jobs.count_beans for an example
        
        
        Screenshots
        -----------
        
        .. image:: example/etc/screenshot_001.png
        
        .. image:: example/etc/screenshot_002.png
        
        
        App settings
        ------------
        
        DJANGOTASK_LOG_ROOT
            Path for log files.
        
            Default: None
        
            Example: os.path.abspath(os.path.join(BASE_DIR, '..', 'protected', 'tasklog'))
        
        DJANGOTASK_ALWAYS_EAGER
            When True, all task are execute syncronously (useful for debugging and unit testing).
        
            Default: False
        
        DJANGOTASK_JOB_TRACE_ENABLED
            Enables low level tracing in Job.run() - for debugging challenging race conditions
        
            Default: False
        
        DJANGOTASK_REJECT_IF_NO_WORKER_ACTIVE_FOR_QUEUE
            Rejects task if not active worker is available for the specific task queue
            when task.run() is called
        
            Default: False
        
        REDIS_URL
            Redis server to connect to
        
            Default: 'redis://localhost:6379/0'
        
        
        Running Tests
        -------------
        
        Does the code actually work?
        
        ::
        
            source <YOURVIRTUALENV>/bin/activate
            (myenv) $ pip install tox
            (myenv) $ tox
        
        
        Support Job class
        -----------------
        
        Starting from version 0.3.0, some conveniences have been added:
        
        - The @job decorator for job functions is no more required, as Task.run() now
          uses queue.enqueue() instead of jobfunc.delay(), and retrieves the queue
          name directly from the Task itself
        
        - each Task can set it's own TASK_TIMEOUT value (expressed in seconds),
          that when provided overrides the default queue timeout
        
        - a new Job class has been provided to share suggested common logic before and
          after jobfunc execution
        
        .. code :: python
        
            class Job(object):
        
                @classmethod
                def run(job_class, task_class, task_id):
                    job_trace('job.run() enter')
                    task = None
                    result = 'SUCCESS'
                    failure_reason = ''
        
                    try:
        
                        # this raises a "Could not resolve a Redis connection" exception in sync mode
                        #job = get_current_job()
                        job = get_current_job(connection=redis.Redis.from_url(REDIS_URL))
        
                        # Retrieve task obj and set as Started
                        task = task_class.get_task_from_id(task_id)
                        task.set_status(status='STARTED', job_id=job.get_id())
        
                        # Execute job passing by task
                        job_class.execute(job, task)
        
                    except Exception as e:
                        job_trace('ERROR: %s' % str(e))
                        job_trace(traceback.format_exc())
        
                        if task:
                            task.log(logging.ERROR, str(e))
                            task.log(logging.ERROR, traceback.format_exc())
                        result = 'FAILURE'
                        failure_reason = str(e)
        
                    finally:
                        if task:
                            task.set_status(status=result, failure_reason=failure_reason)
                        try:
                            job_class.on_complete(job, task)
                        except Exception as e:
                            job_trace('NESTED ERROR: Job.on_completed() raises error "%s"' % str(e))
                            job_trace(traceback.format_exc())
                    job_trace('job.run() leave')
        
                @staticmethod
                def on_complete(job, task):
                    pass
        
                @staticmethod
                def execute(job, task):
                    pass
        
        so you can either override `run()` to implement a different logic,
        or (in most cases) just supply your own `execute()` method, and optionally
        override `on_complete()` to execute cleanup actions after job completion;
        
        example:
        
        .. code :: python
        
            class CountBeansJob(Job):
        
                @staticmethod
                def execute(job, task):
                    params = task.retrieve_params_as_dict()
                    num_beans = params['num_beans']
                    for i in range(0, num_beans):
                        time.sleep(0.01)
                        task.set_progress((i + 1) * 100 / num_beans, step=10)
        
                @staticmethod
                def on_complete(job, task):
                    print('task "%s" completed with: %s' % (str(task.id), task.status))
                    # An more realistic example from a real project ...
                    # if task.status != 'SUCCESS' or task.error_counter > 0:
                    #    task.alarm = BaseTask.ALARM_STATUS_ALARMED
                    #    task.save(update_fields=['alarm', ])
        
        
        **Execute**
        
        Run consumer:
        
        .. code:: bash
        
            python manage.py runserver
        
        
        Run worker(s):
        
        .. code:: bash
        
            python manage.py rqworker low high default
            python manage.py rqworker low high default
            ...
        
        **Sample Task**
        
        .. code:: python
        
            from django.db import models
            from django.conf import settings
            from django_task.models import Task
        
        
            class SendEmailTask(Task):
        
                sender = models.CharField(max_length=256, null=False, blank=False)
                recipients = models.TextField(null=False, blank=False,
                    help_text='put addresses in separate rows')
                subject = models.CharField(max_length=256, null=False, blank=False)
                message = models.TextField(null=False, blank=True)
        
                TASK_QUEUE = settings.QUEUE_LOW
                TASK_TIMEOUT = 60
                LOG_TO_FIELD = True
                LOG_TO_FILE = False
                DEFAULT_VERBOSITY = 2
        
                @staticmethod
                def get_jobfunc():
                    from .jobs import SendEmailJob
                    return SendEmailJob
        
        You can change the `verbosity` dynamically by overridding the verbosity property:
        
        
        When using **LOG_TO_FILE = True**, you might want to add a cleanup handler to
        remove the log file when the corresponding record is deleted::
        
            import os
            from django.dispatch import receiver
        
            @receiver(models.signals.post_delete, sender=ImportaCantieriTask)
            def on_sendemailtask_delete_cleanup(sender, instance, **kwargs):
                """
                Autodelete logfile on Task delete
                """
                logfile = instance._logfile()
                if os.path.isfile(logfile):
                    os.remove(logfile)
        
        
        .. code:: python
        
            class SendEmailTask(Task):
        
                @property
                def verbosity(self):
                    #return self.DEFAULT_VERBOSITY
                    return 1  # either 0, 1 or 2
        
        **Sample Job**
        
        .. code:: python
        
            from __future__ import print_function
            import redis
            import logging
            import traceback
            from django.conf import settings
            from .models import SendEmailTask
            from django_task.job import Job
        
        
            class SendEmailJob(Job):
        
                @staticmethod
                def execute(job, task):
                    params = task.retrieve_params_as_dict()
                    recipient_list = params['recipients'].split()
                    sender = params['sender'].strip()
                    subject = params['subject'].strip()
                    message = params['message']
                    from django.core.mail import send_mail
                    send_mail(subject, message, sender, recipient_list)
        
        **Sample management command**
        
        .. code:: python
        
            from django_task.task_command import TaskCommand
            from django.contrib.auth import get_user_model
        
            class Command(TaskCommand):
        
                def add_arguments(self, parser):
                    super(Command, self).add_arguments(parser)
                    parser.add_argument('sender')
                    parser.add_argument('subject')
                    parser.add_argument('message')
                    parser.add_argument('-r', '--recipients', nargs='*')
                    parser.add_argument('-u', '--user', type=str, help="Specify username for 'created_by' task field")
        
                def handle(self, *args, **options):
                    from tasks.models import SendEmailTask
        
                    # transform the list of recipents into text
                    # (one line for each recipient)
                    options['recipients'] = '\n'.join(options['recipients']) if options['recipients'] is not None else ''
        
                    # format multiline message
                    options['message'] = options['message'].replace('\\n', '\n')
        
                    if 'user' in options:
                        created_by = get_user_model().objects.get(username=options['user'])
                    else:
                        created_by = None
        
                    self.run_task(SendEmailTask, created_by=created_by, **options)
        
        **Deferred Task retrieval to avoid job vs. Task race condition**
        
        An helper Task.get_task_from_id() classmethod is supplied to retrieve Task object
        from task_id safely.
        
        *Task queues create a new type of race condition. Why ?
        Because message queues are fast !
        How fast ?
        Faster than databases.*
        
        See:
        
        https://speakerdeck.com/siloraptor/django-tasty-salad-dos-and-donts-using-celery
        
        A similar generic helper is available for Job-derived needs::
        
            django_task.utils.get_model_from_id(model_cls, id, timeout=1000, retry_count=10)
        
        
        **Howto separate jobs for different instances on the same machine**
        
        To sepatare jobs for different instances on the same machine (or more precisely
        for the same redis connection), override queues names for each instance;
        
        for example:
        
        .. code:: python
        
            # file "settings.py"
        
            REDIS_URL = 'redis://localhost:6379/0'
            ...
        
            #
            # RQ config
            #
        
            RQ_PREFIX = "myproject_"
            QUEUE_DEFAULT = RQ_PREFIX + 'default'
            QUEUE_HIGH = RQ_PREFIX + 'high'
            QUEUE_LOW = RQ_PREFIX + 'low'
        
            RQ_QUEUES = {
                QUEUE_DEFAULT: {
                    'URL': REDIS_URL,
                    #'PASSWORD': 'some-password',
                    'DEFAULT_TIMEOUT': 360,
                },
                QUEUE_HIGH: {
                    'URL': REDIS_URL,
                    'DEFAULT_TIMEOUT': 500,
                },
                QUEUE_LOW: {
                    'URL': REDIS_URL,
                    #'ASYNC': False,
                },
            }
        
            RQ_SHOW_ADMIN_LINK = False
            DJANGOTASK_LOG_ROOT = os.path.abspath(os.path.join(BASE_DIR, '..', 'protected', 'tasklog'))
            DJANGOTASK_ALWAYS_EAGER = False
            DJANGOTASK_JOB_TRACE_ENABLED = False
            DJANGOTASK_REJECT_IF_NO_WORKER_ACTIVE_FOR_QUEUE = True
        
        then run worker as follows:
        
        .. code:: python
        
            python manage.py rqworker myproject_default
        
        **Howto schedule jobs with cron**
        
        Call management command 'count_beans', which in turn executes the required job.
        
        For example::
        
            SHELL=/bin/bash
            PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
        
            0 * * * *  {{username}}    timeout 55m {{django.pythonpath}}/python {{django.website_home}}/manage.py count_beans 1000 >> {{django.logto}}/cron.log 2>&1
        
        A base class TaskCommand has been provided to simplify the creation of any specific
        task-related management commad;
        
        a derived management command is only responsible for:
        
        - defining suitable command-line parameters
        - selecting the specific Task class and job function
        
        for example:
        
        .. code:: python
        
            from django_task.task_command import TaskCommand
        
        
            class Command(TaskCommand):
        
                def add_arguments(self, parser):
                    super(Command, self).add_arguments(parser)
                    parser.add_argument('num_beans', type=int)
        
                def handle(self, *args, **options):
                    from tasks.models import CountBeansTask
                    self.run_task(CountBeansTask, **options)
        
        
        Javascript helpers
        ------------------
        
        A few utility views have been supplied for interacting with tasks from javascript.
        
        tasks_info_api
        ..............
        
        Retrieve informations about a list of existing tasks
        
        Sample usage:
        
        .. code:: javascript
        
            var tasks = [{
                id: 'c50bf040-a886-4aed-bf41-4ae794db0941',
                model: 'tasks.devicetesttask'
            }, {
                id: 'e567c651-c8d5-4dc7-9cbf-860988f55022',
                model: 'tasks.devicetesttask'
            }];
        
            $.ajax({
                url: '/django_task/info/',
                data: JSON.stringify(tasks),
                cache: false,
                type: 'post',
                dataType: 'json',
                headers: {'X-CSRFToken': getCookie('csrftoken')}
            }).done(function(data) {
                console.log('data: %o', data);
            });
        
        Result::
        
            [
              {
                "id": "c50bf040-a886-4aed-bf41-4ae794db0941",
                "created_on": "2018-10-11T17:45:14.399491+00:00",
                "created_on_display": "10/11/2018 19:45:14",
                "created_by": "4f943f0b-f5a3-4fd8-bb2e-451d2be107e2",
                "started_on": null,
                "started_on_display": "",
                "completed_on": null,
                "completed_on_display": "",
                "job_id": "",
                "status": "PENDING",
                "status_display": "<div class=\"task_status\" data-task-model=\"tasks.devicetesttask\" data-task-id=\"c50bf040-a886-4aed-bf41-4ae794db0941\" data-task-status=\"PENDING\" data-task-complete=\"0\">PENDING</div>",
                "log_link_display": "",
                "failure_reason": "",
                "progress": null,
                "progress_display": "-",
                "completed": false,
                "duration": null,
                "duration_display": "",
                "extra_fields": {
                }
              },
              ...
            ]
        
        task_add_api
        ............
        
        Create and run a new task based on specified parameters
        
        Expected parameters:
        
        - 'task-model' = "<app_name>.<model_name>"
        - ... task parameters ...
        
        Returns the id of the new task.
        
        Sample usage:
        
        .. code:: javascript
        
            function exportAcquisition(object_id) {
                if (confirm('Do you want to export data ?')) {
        
                    var url = '/django_task/add/';
                    var data = JSON.stringify({
                        'task-model': 'tasks.exportdatatask',
                        'source': 'backend.acquisition',
                        'object_id': object_id
                    });
        
                    $.ajax({
                        type: 'POST',
                        url: url,
                        data: data,
                        cache: false,
                        crossDomain: true,
                        dataType: 'json',
                        headers: {'X-CSRFToken': getCookie('csrftoken')}
                    }).done(function(data) {
                        console.log('data: %o', data);
                        alert('New task created: "' + data.task_id + '"');
                    }).fail(function(jqXHR, textStatus, errorThrown) {
                        console.log('ERROR: ' + jqXHR.responseText);
                        alert(errorThrown);
                    });
                }
                return;
            }
        
        task_run_api
        ............
        
        Schedule execution of specified task.
        
        Returns job.id or throws error (400).
        
        Parameters:
        
        - app_label
        - model_name
        - pk
        - is_async (0 or 1, default=1)
        
        Sample usage:
        
        .. code:: javascript
        
            var task_id = 'c50bf040-a886-4aed-bf41-4ae794db0941';
        
            $.ajax({
                url: sprintf('/django_task/tasks/devicetesttask/%s/run/', task_id),
                cache: false,
                type: 'get'
            }).done(function(data) {
                console.log('data: %o', data);
            }).fail(function(jqXHR, textStatus, errorThrown) {
                display_server_error(jqXHR.responseText);
            });
        
        
        Updating the tasks listing dynamically in the frontend
        ------------------------------------------------------
        
        The list of Tasks in the admin changelist_view is automatically updated to refresh
        the progess and status of each running Task.
        
        You can obtain the same result in the frontend by calling the **DjangoTask.update_tasks()**
        javascript helper, provided you're listing the tasks in an HTML table with a similar layout.
        
        The simplest way to do it is to use the **render_task_column_names_as_table_row**
        and **render_task_as_table_row** template tags.
        
        Example:
        
        .. code:: html
        
            {% load i18n django_task_tags %}
        
            {% if not export_data_tasks %}
                <div>{% trans 'No recent jobs available' %}</div>
            {% else %}
                <table id="export_data_tasks" class="table table-striped">
                    {% with excluded='created_by,created_on,job_id,log_text,mode' %}
                    <thead>
                        <tr>
                            {{ export_data_tasks.0|render_task_column_names_as_table_row:excluded }}
                        </tr>
                    </thead>
                    <tbody>
                        {% for task in export_data_tasks %}
                        <tr>
                            {{ task|render_task_as_table_row:excluded }}
                        </tr>
                        {% endfor %}
                    </tbody>
                </table>
                {% endwith %}
            {% endif %}
        
        
            {% block extrajs %}
                {{ block.super }}
                <script type="text/javascript" src="{% static 'js/django_task.js' %}"></script>
                <script>
                    $(document).ready(function() {
                        DjangoTask.update_tasks(1000, '#export_data_tasks');
                    });
                </script>
            {% endblock extrajs %}
        
        For each fieldname included in the table rows, **render_task_as_table_row** will
        check if a FIELDNAME_display() method is available in the Task model, and in case
        will use it for rendering the field value; otherwise, the field value will be simply
        converted into a string.
        
        If the specific derived Task model defines some additional fields (unknown to the base Task model)
        which need to be updated regularly by **DjangoTask.update_tasks()**, include them as "extra_fields"
        as follows:
        
        .. code:: python
        
            def as_dict(self):
                data = super(ExportDataTask, self).as_dict()
                data['extra_fields'] = {
                    'result_display': mark_safe(self.result_display())
                }
                return data
        
        .. image:: example/etc/screenshot_003.png
        
        Example Project for django-task
        -------------------------------
        
        As example project is provided as a convenience feature to allow potential users
        to try the app straight from the app repo without having to create a django project.
        
        Please follow the instructions detailed in file `example/README.rst <example/README.rst>`_.
        
        
        Credits
        -------
        
        References:
        
        - `A simple app that provides django integration for RQ (Redis Queue) <https://github.com/ui/django-rq>`_
        - `Asynchronous tasks in django with django-rq <https://spapas.github.io/2015/01/27/async-tasks-with-django-rq/>`_
        - `django-rq redux: advanced techniques and tools <https://spapas.github.io/2015/09/01/django-rq-redux/>`_
        - `Benchmark: Shared vs. Dedicated Redis Instances <https://redislabs.com/blog/benchmark-shared-vs-dedicated-redis-instances/>`_
        - `Django tasty salad - DOs and DON'Ts using Celery by Roberto Rosario <https://speakerdeck.com/siloraptor/django-tasty-salad-dos-and-donts-using-celery>`_
        
        
        
        
        
        =======
        History
        =======
        
        1.5.0
        -----
        * Support for updating the tasks listing dynamically in the frontend
        * Example provided for task_add_api() javascript helper
        * POSSIBLY INCOMPATIBLE CHANGE: duration and duration_display are now methods rather then properties
        * it traslation for UI messages
        
        1.4.7
        -----
        * Added optional "created_by" parameter to TaskCommand utility
        
        1.4.6
        -----
        * replace namespace "django.jQuery" with more generic "jQuery" in js helpers
        * update example project
        * unit tests added to "tasks" app in example project
        
        1.4.5
        -----
        * Quickstart revised in README
        
        1.4.4
        -----
        * Task.get_logger() is now publicly available
        
        1.4.3
        -----
        * restore compatibility with Django 1.11; upgrade rq and django-rq requirements
        
        1.4.2
        -----
        * tasks_info_api() optimized to use a single query
        
        1.4.1
        -----
        * Cleanup: remove redundant REJECTED status
        
        1.4.0
        -----
        * Update requirements (Django >= 2.0, django-rq>=2.0)
        
        1.3.10
        ------
        * Use exceptions.TaskError class when raising specific exceptions
        
        v1.3.9
        ------
        * removed forgotten pdb.set_trace() in revoke_pending_tasks()
        
        v1.3.8
        ------
        * cleanup
        
        v1.3.7
        ------
        * cleanup
        
        v1.3.6
        ------
        * log queue name
        
        v1.3.5
        ------
        * Readme updated
        
        v1.3.4
        ------
        * javascript helper views
        * fix Task.set_progress(0)
        
        v1.3.3
        ------
        * make sure fields are unique in TaskAdmin fieldsets
        
        v1.3.1
        ------
        * unit tests verified with Python 2.7/3.6/3.7 and Django 1.10/2.0
        
        v1.3.0
        ------
        * cleanup
        * classify as production/stable
        
        v1.2.5
        ------
        * Tested with Django 2.0 and Python 3.7
        * Rename `async` to `is_async` to support Python 3.7
        * DJANGOTASK_REJECT_IF_NO_WORKER_ACTIVE_FOR_QUEUE app setting added
        * example cleanup
        
        v1.2.4
        ------
        * API to create and run task via ajax
        
        v1.2.3
        ------
        * TaskAdmin: postpone autorun to response_add() to have M2M task parameters (if any) ready
        * Task.clone() supports M2M parameters
        
        v1.2.2
        ------
        * property to change verbosity dinamically
        
        v1.2.1
        ------
        * util revoke_pending_tasks() added
        
        v1.2.0
        ------
        * DJANGOTASK_JOB_TRACE_ENABLED setting added to enable low level tracing in Job.run()
        * Added missing import in utils.py
        
        v1.1.3
        ------
        * cleanup: remove get_child() method being Task an abstract class
        * fix: skip Task model (being abstract) in dump_all_tasks and delete_all_tasks management commands
        * generic get_model_from_id() helper
        * Job.on_complete() callback
        
        v1.1.2
        ------
        * provide list of pending and completed task status
        
        v1.1.0
        ------
        * INCOMPATIBLE CHANGE: Make model Task abstract for better listing performances
        * redundant migrations removed
        * convert request.body to string for Python3
        * pretty print task params in log when task completes
        
        v0.3.8
        ------
        * return verbose name as description
        
        v0.3.7
        ------
        * description added to Task model
        
        v0.3.6
        ------
        * More fixes
        
        v0.3.5
        ------
        * log to field fix
        
        v0.3.4
        ------
        * log quickview + view
        
        v0.3.3
        ------
        * Optionally log to either file or text field
        * Management commands to dump and delete all tasks
        
        v0.3.2
        ------
        * search by task.id and task.job_id
        
        v0.3.1
        ------
        * Keep track of task mode (sync or async)
        
        v0.3.0
        ------
        * new class Job provided to share task-related logic among job funcs
        
        v0.2.0
        ------
        * fixes for django 2.x
        
        v0.1.15
        -------
        * hack for  prepopulated_fields
        
        v0.1.14
        -------
        * css fix
        
        v0.1.13
        -------
        * minor fixes
        
        v0.1.12
        -------
        * Deferred Task retrieval to avoid job vs. Task race condition
        * Improved Readme
        
        v0.1.11
        -------
        * superuser can view all tasks, while other users have access to their own tasks only
        * js fix
        
        v0.1.10
        -------
        * prevent task.failure_reason overflow
        
        v0.1.9
        ------
        * app settings
        
        v0.1.8
        ------
        * always start job from task.run() to prevent any possible race condition
        * task.run(async) can now accept async=False
        
        v0.1.7
        ------
        * javascript: use POST to retrieve tasks state for UI update to prevent URL length limit exceed
        
        v0.1.6
        ------
        * Improved ui for TaskAdmin
        * Fix unicode literals for Python3
        
        v0.1.5
        ------
        * fixes for Django 1.10
        * send_email management command example added
        
        v0.1.4
        ------
        * Fix OneToOneRel import for Django < 1.9
        
        v0.1.3
        ------
        * Polymorphic behaviour or Task.get_child() restored
        
        v0.1.2
        ------
        * TaskCommand.run_task() renamed as TaskCommand.run_job()
        * New TaskCommand.run_task() creates a Task, then runs it;
          this guarantees that something is traced even when background job will fail
        
Keywords: django-task
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Framework :: Django
Classifier: Framework :: Django :: 1.11
Classifier: Framework :: Django :: 2.2
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Natural Language :: English
Classifier: Programming Language :: Python :: 3.7
