Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-django-rq for 
openSUSE:Factory checked in at 2023-12-07 19:10:43
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-django-rq (Old)
 and      /work/SRC/openSUSE:Factory/.python-django-rq.new.25432 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-django-rq"

Thu Dec  7 19:10:43 2023 rev:6 rq:1131506 version:2.9.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-django-rq/python-django-rq.changes        
2023-06-11 19:58:53.440466033 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-django-rq.new.25432/python-django-rq.changes 
    2023-12-07 19:12:28.111825292 +0100
@@ -1,0 +2,11 @@
+Wed Dec  6 22:49:46 UTC 2023 - Dirk Müller <[email protected]>
+
+- update to 2.9.0:
+  * Added an option to delete all failed jobs.
+  * You can now specify `SERIALIZER` option while declaring
+    queues in `settings.py`
+  * Updated templates to match newer versions of Django admin's
+    styling.
+  * Don't show `Empty Queue` button on registry pages.
+
+-------------------------------------------------------------------

Old:
----
  django-rq-2.8.1.tar.gz

New:
----
  django-rq-2.9.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-django-rq.spec ++++++
--- /var/tmp/diff_new_pack.GOgxtz/_old  2023-12-07 19:12:28.639844771 +0100
+++ /var/tmp/diff_new_pack.GOgxtz/_new  2023-12-07 19:12:28.639844771 +0100
@@ -16,10 +16,9 @@
 #
 
 
-%define skip_python2 1
-%{?!python_module:%define python_module() python-%{**} python3-%{**}}
+%{?sle15_python_module_pythons}
 Name:           python-django-rq
-Version:        2.8.1
+Version:        2.9.0
 Release:        0
 Summary:        Simple app that provides django integration for RQ (Redis 
Queue)
 License:        MIT
@@ -38,6 +37,7 @@
 BuildRequires:  %{python_module django-redis >= 3.0}
 BuildRequires:  %{python_module pytest-django}
 BuildRequires:  %{python_module rq >= 1.14}
+BuildRequires:  %{python_module rq-scheduler}
 BuildRequires:  redis
 # /SECTION
 %python_subpackages

++++++ django-rq-2.8.1.tar.gz -> django-rq-2.9.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-rq-2.8.1/.github/workflows/test.yml 
new/django-rq-2.9.0/.github/workflows/test.yml
--- old/django-rq-2.8.1/.github/workflows/test.yml      2023-05-14 
03:31:42.000000000 +0200
+++ new/django-rq-2.9.0/.github/workflows/test.yml      2023-11-26 
12:34:09.000000000 +0100
@@ -16,7 +16,7 @@
     name: Python${{ matrix.python-version }}/Django${{ matrix.django-version }}
     strategy:
       matrix:
-        python-version: ["3.8", "3.9", "3.10"]
+        python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
         django-version: ["3.2.16", "4.0.8", "4.1.3", "4.2"]
 
     steps:
@@ -34,7 +34,7 @@
       run: |
         python -m pip install --upgrade pip
         pip install django==${{ matrix.django-version }}
-        pip install redis django-redis rq sentry-sdk
+        pip install redis django-redis rq sentry-sdk rq-scheduler
 
     - name: Run Test
       run: |
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-rq-2.8.1/CHANGELOG.md 
new/django-rq-2.9.0/CHANGELOG.md
--- old/django-rq-2.8.1/CHANGELOG.md    2023-05-14 03:31:42.000000000 +0200
+++ new/django-rq-2.9.0/CHANGELOG.md    2023-11-26 12:34:09.000000000 +0100
@@ -1,27 +1,33 @@
-# Version 2.8.1 (2023-05-14)
+### Version 2.9.0 (2023-11-26)
+* Added an option to delete all failed jobs. Thanks @chromium7!
+* You can now specify `SERIALIZER` option while declaring queues in 
`settings.py` Thanks @sophcass!
+* Updated templates to match newer versions of Django admin's styling. Thanks 
@nikhilweee!
+* Don't show `Empty Queue` button on registry pages. Thanks @selwin!
+
+### Version 2.8.1 (2023-05-14)
 * Added a button to stop currently running jobs. Thanks @gabriels1234!
 * Added a failed jobs column to rqstats command. Thanks @dangquangdon!
 * Explicitly requires RQ >= 1.14 in `setup.py`. Thanks @selwin!
 
-# Version 2.8.0 (2023-05-02)
+### Version 2.8.0 (2023-05-02)
 * Support for RQ 1.14. Thanks @Cerebro92 and @selwin!
 * Show scheduler PID information in admin interface. Thanks @gabriels1234!
 * Added `serializer` argument to `rqworker` command. Thanks @gabriels1234!
 * Added `USERNAME` and `SENTINEL_KWARGS` support. Thanks @joachimBurket!
 
-# Version 2.7.0 (2023-02-07)
+### Version 2.7.0 (2023-02-07)
 * Able to show multiple execution results for each job (requires RQ v1.12). 
Thanks @selwin!
 * Various admin interface improvements. Thanks @selwin!
 
-# Version 2.6.0 (2022-11-05)
+### Version 2.6.0 (2022-11-05)
 * Added `--max-jobs` argument to `rqworker` management command. Thanks 
@arpit-goel!
 * Remove job from `ScheduledJobRegistry` if a scheduled job is enqueued from 
admin. Thanks @robertaistleitner!
 * Minor code cleanup. Thanks @reybog90! 
 
-# Version 2.5.1 (2021-11-22)
+### Version 2.5.1 (2021-11-22)
 * `Redis.from_url` does not accept `ssl_cert_reqs` argument for non SSL Redis 
URL. Thanks @barash-asenov! 
 
-# Version 2.5.0 (2021-11-17)
+### Version 2.5.0 (2021-11-17)
 * Better integration with Django admin, along with a new `Access admin page` 
permission that you can selectively grant to users. Thanks @haakenlid!
 * Worker count is now updated everytime you view workers for that specific 
queue. Thanks @cgl!
 * Add the capability to pass arbitrary Redis client kwargs. Thanks 
@juanjgarcia!
@@ -29,21 +35,21 @@
 * Add `@never_cache` decorator to all Django-RQ views. Thanks @Cybernisk!
 * `SSL_CERT_REQS` argument should also be passed to Redis client even when 
Redis URL is used. Thanks @paltman!
 
-# Version 2.4.1 (2021-03-31)
+### Version 2.4.1 (2021-03-31)
 * Added `ssl_cert_reqs` and `username` to queue config. Thanks @jeyang!
 
-# Version 2.4.0 (2020-11-08)
+### Version 2.4.0 (2020-11-08)
 * Various admin interface improvements. Thanks @selwin and @atten!
 * Improved Sentry integration. Thanks @hugorodgerbrown and @kichawa!
 
-# Version 2.3.2 (2020-05-13)
+### Version 2.3.2 (2020-05-13)
 * Compatibility with RQ >= 1.4.0 which implements customizable serialization 
method. Thanks @selwin!
 
-# Version 2.3.1 (2020-04-10)
+### Version 2.3.1 (2020-04-10)
 * Added `--with-scheduler` argument to `rqworker` management command. Thanks 
@stlk!
 * Fixed a bug where opening job detail would crash if job.dependency no longer 
exists. Thanks @selwin!
 
-# Version 2.3.0 (2020-02-09)
+### Version 2.3.0 (2020-02-09)
 * Support for RQ's new `ScheduledJobRegistry`. Thanks @Yolley!
 * Improve performance when displaying pages showing a large number of jobs by 
using `Job.fetch_many()`. Thanks @selwin!
 * `django-rq` will now automatically cleanup orphaned worker keys in job 
registries. Thanks @selwin!
@@ -51,17 +57,17 @@
 * `NoSuchJobError`s are now handled properly when requeuing all jobs. Thanks 
@thomasmatecki!
 * Support for displaying jobs with names containing `$`. Thanks @gowthamk63!
 
-# Version 2.2.0 (2019-12-08)
+### Version 2.2.0 (2019-12-08)
 - Support for Django 3.0. This release also drops support for Django 1.X. 
Thanks @hugorodgerbrown!
 - `rqworker` management command now properly passes in `--verbosity` to 
`Worker`. Thanks @stlk!
 - The admin interface can now view jobs with `:` on their IDs. Thanks 
@carboncoop!
 - Job detail page now shows `job.dependency`. Thanks @selwin!
 
-# Version 2.1.0 (2019-06-14)
+### Version 2.1.0 (2019-06-14)
 - Fixed `Requeue All`
 - Django-RQ now automatically runs maintenance tasks when `rq_home` is opened
 
-# Version 2.0 (2019-04-06)
+### Version 2.0 (2019-04-06)
 - Compatibility with RQ 1.0 (Thanks @selwin). Backward incompatible changes 
include:
   * `FailedQueue` is now replaced by `FailedJobRegistry`
   * RQ now uses `sentry-sdk` to send job failures to Sentry.
@@ -69,39 +75,39 @@
 - Minor improvements and bug fixes. Thanks @selwin!
 
 
-# Version 1.3.1 (2019-03-15)
+### Version 1.3.1 (2019-03-15)
 - Run `rqworker` with `--sentry_dsn=""` to disable Sentry integration. Thanks 
@Bolayniuss!
 - Support for `SSL` Redis kwarg. Thanks @ajknv!
 - `rqworker`and `rqscheduler` management commands now uses RQ's built in 
`setup_loghandlers` function. Thanks @Paulius-Maruska!
 - Remove the use of deprecated `admin_static` template tag. Thanks 
@lorenzomorandini!
 
 
-# Version 1.3.0 (2018-12-18)
+### Version 1.3.0 (2018-12-18)
 - Added support `redis-py` >= 3 and `RQ` >= 0.13. Thanks @selwin!
 - Use `Worker.count(queue=queue)` to speed up the process of getting the 
number of active workers. Thanks @selwin!
 - Added an option to requeue job from the admin interface. Thanks @seiryuz!
 - Improve Sentinel support. Thanks @pnuckowski!
 
 
-# Version 1.2.0 (2018-07-26)
+### Version 1.2.0 (2018-07-26)
 - Supports Python 3.7 by renaming `async` to `is_async`. Thanks @Flimm!
 - `UnpickleError` is now handled properly. Thanks @selwin!
 - Redis Sentinel support. Thanks @SpeedyCoder!
 
 
-# Version 1.1.0
+### Version 1.1.0
 - Fixed some admin related bugs. Thanks @seiryuz!
 - More Django 2.0 compatibility fixes. Thanks @selwin and @koddr!
 - Custom `Job` and `Worker` classes are now supported. Thanks @skirsdeda!
 - `SENTRY_DSN` value in `settings.py` will now be used by default. Thanks 
@inetss!
 
 
-# 1.0.1
+### 1.0.1
 - Django 2.0 compatibility fixes.
 - Minor bug fixes
 
 
-# 1.0.0
+### 1.0.0
 
 -   You can now view worker information
 -   Detailed worker statistics such as failed/completed job count are
@@ -116,13 +122,13 @@
 -   Improved performance when requeueing all jobs. Thanks
     @therefromhere!
 
-# 0.9.6
+### 0.9.6
 
 -   More Django 1.10 compatibility fixes. Thanks @dmwyatt!
 -   Improves performance when dealing with a large number of workers.
     Thanks @lucastamoios!
 
-# 0.9.5
+### 0.9.5
 
 -   Fixed view paging for registry-based job lists. Thanks @smaccona!
 -   Fixed an issue where multiple failed queues may appear for the same
@@ -132,7 +138,7 @@
 -   Fixed an argument parsing bug `rqworker` management command. Thanks
     @hendi!
 
-# 0.9.3
+### 0.9.3
 
 -   Added a `--pid` option to `rqscheduler` management command. Thanks
     @vindemasi!
@@ -147,13 +153,13 @@
     @randomguy91!
 -   Other minor fixes by @jeromer and @sbussetti.
 
-# 0.9.2
+### 0.9.2
 
 -   Support for Django 1.10. Thanks @jtburchfield!
 -   Added `--queue-class` option to `rqworker` management command.
     Thanks @Krukov!
 
-# 0.9.1
+### 0.9.1
 
 -   Added `-i` and `--queue` options to rqscheduler management command.
     Thanks @mbodock and @sbussetti!
@@ -165,7 +171,7 @@
     `RQ_EXCEPTION_HANDLERS` in `settings.py`. Thanks @sbussetti!
 -   Queues in django-admin are now sorted by name. Thanks @pnuckowski!
 
-# 0.9.0
+### 0.9.0
 
 -   Support for Django 1.9. Thanks @aaugustin and @viaregio!
 -   `rqworker` management command now accepts `--worker-ttl` argument.
@@ -174,7 +180,7 @@
     `settings.py`. Thanks @xuhcc!
 -   `django-rq` now requires RQ &gt;= 0.5.5
 
-# 0.8.0
+### 0.8.0
 
 -   You can now view deferred, finished and currently active jobs from
     admin interface.
@@ -182,7 +188,7 @@
 -   Requires RQ &gt;= 0.5.
 -   You can now use StrictRedis with Django-RQ. Thanks @wastrachan!
 
-# 0.7.0
+### 0.7.0
 
 -   Added `rqenqueue` management command for easy scheduling of tasks
     (e.g via cron
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-rq-2.8.1/README.rst 
new/django-rq-2.9.0/README.rst
--- old/django-rq-2.8.1/README.rst      2023-05-14 03:31:42.000000000 +0200
+++ new/django-rq-2.9.0/README.rst      2023-11-26 12:34:09.000000000 +0100
@@ -67,7 +67,7 @@
             'SOCKET_TIMEOUT': 0.3,
             'CONNECTION_KWARGS': {  # Eventual additional Redis connection 
arguments
                 'ssl': True
-            }
+            },
             'SENTINEL_KWARGS': {    # Eventual Sentinel connection arguments
                 # If Sentinel also has auth, username/password can be passed 
here
                 'username': 'sentinel-user',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-rq-2.8.1/django_rq/__init__.py 
new/django-rq-2.9.0/django_rq/__init__.py
--- old/django-rq-2.8.1/django_rq/__init__.py   2023-05-14 03:31:42.000000000 
+0200
+++ new/django-rq-2.9.0/django_rq/__init__.py   2023-11-26 12:34:09.000000000 
+0100
@@ -1,4 +1,4 @@
-VERSION = (2, 8, 1)
+VERSION = (2, 9, 0)
 
 from .decorators import job
 from .queues import enqueue, get_connection, get_queue, get_scheduler
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-rq-2.8.1/django_rq/queues.py 
new/django-rq-2.9.0/django_rq/queues.py
--- old/django-rq-2.8.1/django_rq/queues.py     2023-05-14 03:31:42.000000000 
+0200
+++ new/django-rq-2.9.0/django_rq/queues.py     2023-11-26 12:34:09.000000000 
+0100
@@ -155,6 +155,7 @@
     connection=None,
     queue_class=None,
     job_class=None,
+    serializer=None,
     **kwargs
 ):
     """
@@ -176,6 +177,8 @@
         default_timeout = QUEUES[name].get('DEFAULT_TIMEOUT')
     if connection is None:
         connection = get_connection(name)
+    if serializer is None:
+        serializer = QUEUES[name].get('SERIALIZER')
     queue_class = get_queue_class(QUEUES[name], queue_class)
     return queue_class(
         name,
@@ -184,6 +187,7 @@
         is_async=is_async,
         job_class=job_class,
         autocommit=autocommit,
+        serializer=serializer,
         **kwargs
     )
 
@@ -196,9 +200,21 @@
 
     config = QUEUES_LIST[int(index)]
     return get_queue_class(config)(
-        config['name'], 
connection=get_redis_connection(config['connection_config']), 
is_async=config.get('ASYNC', True)
+        config['name'],
+        connection=get_redis_connection(config['connection_config']),
+        is_async=config.get('ASYNC', True),
+        serializer=config['connection_config'].get('SERIALIZER')
     )
 
+def get_scheduler_by_index(index):
+    """
+    Returns an rq-scheduler Scheduler using parameters defined in 
``QUEUES_LIST``
+    """
+    from .settings import QUEUES_LIST
+
+    config = QUEUES_LIST[int(index)]
+    return get_scheduler(config['name'])
+
 
 def filter_connection_params(queue_params):
     """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-rq-2.8.1/django_rq/settings.py 
new/django-rq-2.9.0/django_rq/settings.py
--- old/django-rq-2.8.1/django_rq/settings.py   2023-05-14 03:31:42.000000000 
+0200
+++ new/django-rq-2.9.0/django_rq/settings.py   2023-11-26 12:34:09.000000000 
+0100
@@ -15,8 +15,10 @@
 
 # All queues in list format so we can get them by index, includes failed queues
 QUEUES_LIST = []
+QUEUES_MAP = {}
 for key, value in sorted(QUEUES.items(), key=itemgetter(0)):
     QUEUES_LIST.append({'name': key, 'connection_config': value})
+    QUEUES_MAP[key] = len(QUEUES_LIST) - 1
 
 # Get exception handlers
 EXCEPTION_HANDLERS = getattr(settings, 'RQ_EXCEPTION_HANDLERS', [])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/django-rq-2.8.1/django_rq/templates/django_rq/clear_failed_queue.html 
new/django-rq-2.9.0/django_rq/templates/django_rq/clear_failed_queue.html
--- old/django-rq-2.8.1/django_rq/templates/django_rq/clear_failed_queue.html   
1970-01-01 01:00:00.000000000 +0100
+++ new/django-rq-2.9.0/django_rq/templates/django_rq/clear_failed_queue.html   
2023-11-26 12:34:09.000000000 +0100
@@ -0,0 +1,43 @@
+{% extends "admin/base_site.html" %}
+
+{% load static %}
+
+{% block extrastyle %}
+    {{ block.super }}
+    <style>
+        .data {
+            display: inline-block;
+            float: left;
+            width: 80%;
+        }
+    </style>
+    <link href="{% static 'admin/css/forms.css' %}" type="text/css" 
rel="stylesheet">
+{% endblock %}
+
+{% block breadcrumbs %}
+    <div class="breadcrumbs">
+        <a href="{% url 'admin:index' %}">Home</a> &rsaquo;
+        <a href="{% url 'rq_home' %}">Django RQ</a> &rsaquo;
+        <a href = "{% url 'rq_jobs' queue_index %}">{{ queue.name }}</a> 
&rsaquo;
+        Delete All
+    </div>
+{% endblock %}
+
+{% block content_title %}<h1>Are you sure?</h1>{% endblock %}
+
+{% block content %}
+
+<div id="content-main">
+    <p>
+        Are you sure you want to delete {{ total_jobs }} failed job{{ 
total_jobs|pluralize }} in the <a href = "{% url 'rq_jobs' queue_index %}">{{ 
queue.name }}</a> queue?
+        This action can not be undone.
+    </p>
+    <form action="" method="post">
+        {% csrf_token %}
+        <div>
+            <input type="submit" value="Yes, I'm sure" />
+        </div>
+    </form>
+</div>
+
+{% endblock %}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/django-rq-2.8.1/django_rq/templates/django_rq/job_detail.html 
new/django-rq-2.9.0/django_rq/templates/django_rq/job_detail.html
--- old/django-rq-2.8.1/django_rq/templates/django_rq/job_detail.html   
2023-05-14 03:31:42.000000000 +0200
+++ new/django-rq-2.9.0/django_rq/templates/django_rq/job_detail.html   
2023-11-26 12:34:09.000000000 +0100
@@ -139,7 +139,7 @@
                     {% if data_is_valid %}
                         {% if job.kwargs %}
                             <ul>
-                                {% for key, value in job.kwargs.items %}
+                                {% for key, value in job.kwargs|items %}
                                     <li>{{ key }}: {{ value|force_escape 
}}</li>
                                 {% endfor %}
                             </ul>
@@ -171,7 +171,7 @@
                 </div>
             </div>
         {% endif %}
-        
+
         {% if job.legacy_result %}
         <div class="form-row">
             <div>
@@ -182,10 +182,10 @@
         {% endif %}
 
     </fieldset>
-        
+
 
     <div class="submit-row">
-        <p class="deletelink-box"><a href="delete/" 
class="deletelink">Delete</a></p>
+        <div class="deletelink-box"><a href="delete/" 
class="deletelink">Delete</a></div>
         {% if job.is_started %}
             <form method = 'POST' action = "{% url 'rq_stop_job' queue_index 
job.id %}">
                 {% csrf_token %}
@@ -212,37 +212,37 @@
         {% for result in job.results %}
         <h2>Result {{ result.id }}</h2>
         <div class="inline-related">
-          
-            <fieldset class="module aligned ">    
-                <div class="form-row field-choice_text">                      
+
+            <fieldset class="module aligned ">
+                <div class="form-row field-choice_text">
                     <div>
                         <label>Type:</label>
                         <div class="readonly">{{ result.type.name }}</div>
-                    </div>                    
+                    </div>
                 </div>
-            
+
                 <div class="form-row field-votes">
                     <div>
                         <label>Created at: {{ result.Type }}</label>
                         <div class="readonly">{{ 
result.created_at|to_localtime|date:"Y-m-d, H:i:s" }}</div>
-                    </div>            
+                    </div>
                 </div>
                 {% if result.type.value == 1 %}
                     <div class="form-row field-votes">
                         <div>
                             <label>Return value:</label>
                             <div><pre>{{ result.return_value }}</pre></div>
-                        </div>            
+                        </div>
                     </div>
                 {% elif result.type.value == 2 %}
                     <div class="form-row field-votes">
                         <div>
                             <label>Exception:</label>
                             <div><pre>{{ result.exc_string }}</pre></div>
-                        </div>            
+                        </div>
                     </div>
                 {% endif %}
-            
+
             </fieldset>
         </div>
         {% endfor %}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/django-rq-2.8.1/django_rq/templates/django_rq/jobs.html 
new/django-rq-2.9.0/django_rq/templates/django_rq/jobs.html
--- old/django-rq-2.8.1/django_rq/templates/django_rq/jobs.html 2023-05-14 
03:31:42.000000000 +0200
+++ new/django-rq-2.9.0/django_rq/templates/django_rq/jobs.html 2023-11-26 
12:34:09.000000000 +0100
@@ -28,7 +28,7 @@
     <div class="breadcrumbs">
         <a href="{% url 'admin:index' %}">Home</a> &rsaquo;
         <a href="{% url 'rq_home' %}">Django RQ</a> &rsaquo;
-        <a href = "{% url 'rq_jobs' queue_index %}">{{ queue.name }}</a>
+        <a href="{% url 'rq_jobs' queue_index %}">{{ queue.name }}</a>
     </div>
 {% endblock %}
 
@@ -40,105 +40,130 @@
     <ul class="object-tools">
         {% if job_status == 'Failed' %}
             <li><a href="{% url 'rq_requeue_all' queue_index %}" 
class="requeuelink">Requeue All</a></li>
+            <li><a href="{% url 'rq_delete_failed_jobs' queue_index %}" 
class="requeuelink">Delete All</a></li>
+        {% elif job_status == 'Queued' %}
+            <li><a href="{% url 'rq_clear' queue_index %}" 
class="deletelink">Empty Queue</a></li>
         {% endif %}
-        <li><a href="{% url 'rq_clear' queue_index %}" 
class="deletelink">Empty Queue</a></li>
     </ul>
     <div class="module" id="changelist">
-        <form id="changelist-form" action="{% url 'rq_confirm_action' 
queue_index %}" method="post">
-            {% csrf_token %}
-            <div class="actions">
-                <label>Actions:
-                    <select name="action">
-                        <option value="" selected="selected">---------</option>
-                        <option value="delete">Delete</option>
-                        {% if job_status == 'Failed' %}
-                            <option value="requeue">Requeue</option>
-                        {% endif %}
-                        {% if job_status == 'Started' %}
-                            <option value="stop">Stop</option>
-                        {% endif %}
-                    </select>
-                </label>
-                <button type="submit" class="button" title="Execute selected 
action" name="index" value="0">Go</button>
-            </div>
-            <div class="results">
-                <table id="result_list">
-                    <thead>
-                        <tr>
-                            <th scope="col" class="action-checkbox-column">
-                                <div class="text">
-                                    <span><input type="checkbox" 
id="action-toggle" style="display: inline-block;"></span>
-                                </div>
-                                <div class="clear"></div>
-                            </th>
-                            <th><div class = 'text'><span>ID</span></div></th>
-                            <th><div class = 
'text'><span>Created</span></div></th>
-                            {% if job_status == 'Scheduled' %}
-                                <th><div class = 
'text'><span>Scheduled</span></div></th>
+        <div class="changelist-form-container">
+            <form id="changelist-form" action="{% url 'rq_confirm_action' 
queue_index %}" method="post">
+                {% csrf_token %}
+                <div class="actions">
+                    <label>Actions:
+                        <select name="action" required>
+                            <option value="" selected>---------</option>
+                            <option value="delete">Delete</option>
+                            {% if job_status == 'Failed' %}
+                                <option value="requeue">Requeue</option>
                             {% endif %}
-                            <th><div class = 
'text'><span>Enqueued</span></div></th>
-                            <th><div class = 
'text'><span>Ended</span></div></th>
-                            <th><div class = 
'text'><span>Status</span></div></th>
-                            <th><div class = 
'text'><span>Callable</span></div></th>
-                            {% block extra_columns %}
-                            {% endblock extra_columns %}
-                        </tr>
-                    </thead>
-                    <tbody>
-                        {% for job in jobs %}
-                            <tr class = "{% cycle 'row1' 'row2' %}">
-                                <td class="action-checkbox">
-                                    <input class="action-select" 
name="_selected_action" type="checkbox" value="{{ job.id }}">
-                                </td>
-                                <th>
-                                    <a href = "{% url 'rq_job_detail' 
queue_index job.id %}">
-                                        {{ job.id }}
-                                    </a>
-                                </th>
-                                <td>
-                                    {% if job.created_at %}
-                                        {{ 
job.created_at|to_localtime|date:"Y-m-d, H:i:s" }}
-                                    {% endif %}
-                                </td>
+                            {% if job_status == 'Started' %}
+                                <option value="stop">Stop</option>
+                            {% endif %}
+                        </select>
+                    </label>
+                    <button type="submit" class="button" title="Execute 
selected action" name="index" value="0">Go</button>
+                </div>
+                <div class="results">
+                    <table id="result_list">
+                        <thead>
+                            <tr>
+                                <th scope="col" class="action-checkbox-column">
+                                    <div class="text">
+                                        <span><input type="checkbox" 
id="action-toggle"></span>
+                                    </div>
+                                    <div class="clear"></div>
+                                </th>
+                                <th scope="col" class="sortable">
+                                    <div class='text'><span>ID</span></div>
+                                    <div class="clear"></div>
+                                </th>
+                                <th scope="col" class="sortable">
+                                    <div 
class='text'><span>Created</span></div>
+                                    <div class="clear"></div>
+                                </th>
                                 {% if job_status == 'Scheduled' %}
+                                    <th scope="col" class="sortable">
+                                        <div 
class='text'><span>Scheduled</span></div>
+                                        <div class="clear"></div>
+                                    </th>
+                                {% endif %}
+                                <th scope="col" class="sortable">
+                                    <div 
class='text'><span>Enqueued</span></div>
+                                    <div class="clear"></div>
+                                </th>
+                                <th scope="col" class="sortable">
+                                    <div class='text'><span>Ended</span></div>
+                                    <div class="clear"></div>
+                                </th>
+                                <th scope="col" class="sortable">
+                                    <div class='text'><span>Status</span></div>
+                                    <div class="clear"></div>
+                                </th>
+                                <th scope="col" class="sortable">
+                                    <div 
class='text'><span>Callable</span></div>
+                                    <div class="clear"></div>
+                                </th>
+                                {% block extra_columns %}
+                                {% endblock extra_columns %}
+                            </tr>
+                        </thead>
+                        <tbody>
+                            {% for job in jobs %}
+                                <tr>
+                                    <td class="action-checkbox">
+                                        <input class="action-select" 
name="_selected_action" type="checkbox" value="{{ job.id }}">
+                                    </td>
+                                    <th>
+                                        <a href="{% url 'rq_job_detail' 
queue_index job.id %}">
+                                            {{ job.id }}
+                                        </a>
+                                    </th>
+                                    <td>
+                                        {% if job.created_at %}
+                                            {{ 
job.created_at|to_localtime|date:"Y-m-d, H:i:s" }}
+                                        {% endif %}
+                                    </td>
+                                    {% if job_status == 'Scheduled' %}
                                     <td>
                                         {% if job.scheduled_at %}
                                             {{ 
job.scheduled_at|to_localtime|date:"Y-m-d, H:i:s" }}
                                         {% endif %}
                                     </td>
-                                {% endif %}
-                                <td>
-                                    {% if job.enqueued_at %}
-                                        {{ 
job.enqueued_at|to_localtime|date:"Y-m-d, H:i:s" }}
                                     {% endif %}
-                                </td>
-                                <td>
-                                    {% if job.ended_at %}
-                                        {{ 
job.ended_at|to_localtime|date:"Y-m-d, H:i:s" }}
-                                    {% endif %}
-                                </td>
-                                <td>{{ job.get_status }}</td>
-                                <td>{{ job|show_func_name }}</td>
-                                {% block extra_columns_values %}
-                                {% endblock extra_columns_values %}
-                            </tr>
-                        {% endfor %}
-                    </tbody>
-                </table>
-            </div>
-            <p class="paginator">
-                {% for p in page_range %}
-                    {% if p == page %}
-                        <span class="this-page">{{ p }}</span>
-                    {% elif forloop.last %}
-                        <a href="?page={{ p }}" class="end">{{ p }}</a>
-                    {% else %}
-                        <a href="?page={{ p }}">{{ p }}</a>
-                    {% endif %}
-                {% endfor %}
-                {{ num_jobs }} jobs
-            </p>
-        </form>
+                                    <td>
+                                        {% if job.enqueued_at %}
+                                            {{ 
job.enqueued_at|to_localtime|date:"Y-m-d, H:i:s" }}
+                                        {% endif %}
+                                    </td>
+                                    <td>
+                                        {% if job.ended_at %}
+                                            {{ 
job.ended_at|to_localtime|date:"Y-m-d, H:i:s" }}
+                                        {% endif %}
+                                    </td>
+                                    <td>{{ job.get_status }}</td>
+                                    <td>{{ job|show_func_name }}</td>
+                                    {% block extra_columns_values %}
+                                    {% endblock extra_columns_values %}
+                                </tr>
+                            {% endfor %}
+                        </tbody>
+                    </table>
+                </div>
+                <p class="paginator">
+                    {% for p in page_range %}
+                        {% if p == page %}
+                            <span class="this-page">{{ p }}</span>
+                        {% elif forloop.last %}
+                            <a href="?page={{ p }}" class="end">{{ p }}</a>
+                        {% else %}
+                            <a href="?page={{ p }}">{{ p }}</a>
+                        {% endif %}
+                    {% endfor %}
+                    {{ num_jobs }} jobs
+                </p>
+            </form>
+        </div>
     </div>
 </div>
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/django-rq-2.8.1/django_rq/templates/django_rq/scheduler.html 
new/django-rq-2.9.0/django_rq/templates/django_rq/scheduler.html
--- old/django-rq-2.8.1/django_rq/templates/django_rq/scheduler.html    
1970-01-01 01:00:00.000000000 +0100
+++ new/django-rq-2.9.0/django_rq/templates/django_rq/scheduler.html    
2023-11-26 12:34:09.000000000 +0100
@@ -0,0 +1,119 @@
+{% extends "admin/base_site.html" %}
+
+{% load static jquery_path django_rq %}
+
+{% block title %}Scheduler Jobs in {{ scheduler.name }} {{ block.super }}{% 
endblock %}
+
+{% block extrastyle %}
+    {{ block.super }}
+    <link rel="stylesheet" type="text/css" href="{% static 
"admin/css/changelists.css" %}">
+{% endblock %}
+
+{% block extrahead %}
+    {{ block.super }}
+    <script type="text/javascript" src="{% get_jquery_path as jquery_path %}{% 
static jquery_path %}"></script>
+    <script type="text/javascript" src="{% static "admin/js/jquery.init.js" 
%}"></script>
+    <script type="text/javascript" src="{% static "admin/js/actions.js" 
%}"></script>
+    <script type="text/javascript">
+    (function($) {
+        $(document).ready(function($) {
+            $("tr input.action-select").actions();
+        });
+    })(django.jQuery);
+    </script>
+{% endblock %}
+
+
+{% block breadcrumbs %}
+    <div class="breadcrumbs">
+        <a href="{% url 'admin:index' %}">Home</a> &rsaquo;
+        <a href="{% url 'rq_home' %}">Django RQ</a> &rsaquo;
+    </div>
+{% endblock %}
+
+{% block content_title %}<h1>Scheduler Managed Jobs</h1>{% endblock %}
+
+{% block content %}
+
+<div id="content-main">
+    <ul class="object-tools">
+    </ul>
+    <div class="module" id="changelist">
+        <form id="changelist-form" action="" method="post">
+            {% csrf_token %}
+            <div class="actions">
+                <label>Actions:
+                    <select name="action">
+                        <option value="" selected="selected">---------</option>
+                        <option value="delete">Delete</option>
+                        {% if job_status == 'Failed' %}
+                            <option value="requeue">Requeue</option>
+                        {% endif %}
+                    </select>
+                </label>
+                <button type="submit" class="button" title="Execute selected 
action" name="index" value="0">Go</button>
+            </div>
+            <div class="results">
+                <table id="result_list">
+                    <thead>
+                        <tr>
+                            <th scope="col" class="action-checkbox-column">
+                                <div class="text">
+                                    <span><input type="checkbox" 
id="action-toggle" style="display: inline-block;"></span>
+                                </div>
+                                <div class="clear"></div>
+                            </th>
+                            <th><div class = 'text'><span>ID</span></div></th>
+                            <th><div class = 
'text'><span>Schedule</span></div></th>
+                            <th><div class = 'text'><span>Next 
Run</span></div></th>
+                            <th><div class = 'text'><span>Last 
Ended</span></div></th>
+                            <th><div class = 'text'><span>Last 
Status</span></div></th>
+                            <th><div class = 
'text'><span>Callable</span></div></th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        {% for job in jobs %}
+                            <tr class = "{% cycle 'row1' 'row2' %}">
+                                <td class="action-checkbox">
+                                    <input class="action-select" 
name="_selected_action" type="checkbox" value="{{ job.id }}">
+                                </td>
+                                <td>
+                                    <a href = "{% url 'rq_job_detail' 
job.queue_index job.id %}">
+                                        {{ job.id }}
+                                    </a>
+                                </td>
+                                <td>{{ job.schedule }}</td>
+                                <td>
+                                    {% if job.next_run %}
+                                        {{ 
job.next_run|to_localtime|date:"Y-m-d, H:i:s" }}
+                                    {% endif %}
+                                </td>
+                                <td>
+                                    {% if job.ended_at %}
+                                        {{ 
job.ended_at|to_localtime|date:"Y-m-d, H:i:s" }}
+                                    {% endif %}
+                                </td>
+                                <td>{{ job.get_status }}</td>
+                                <td>{{ job|show_func_name }}</td>
+                            </tr>
+                        {% endfor %}
+                    </tbody>
+                </table>
+            </div>
+            <p class="paginator">
+                {% for p in page_range %}
+                    {% if p == page %}
+                        <span class="this-page">{{ p }}</span>
+                    {% elif forloop.last %}
+                        <a href="?page={{ p }}" class="end">{{ p }}</a>
+                    {% else %}
+                        <a href="?page={{ p }}">{{ p }}</a>
+                    {% endif %}
+                {% endfor %}
+                {{ num_jobs }} jobs
+            </p>
+        </form>
+    </div>
+</div>
+
+{% endblock %}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/django-rq-2.8.1/django_rq/templates/django_rq/stats.html 
new/django-rq-2.9.0/django_rq/templates/django_rq/stats.html
--- old/django-rq-2.8.1/django_rq/templates/django_rq/stats.html        
2023-05-14 03:31:42.000000000 +0200
+++ new/django-rq-2.9.0/django_rq/templates/django_rq/stats.html        
2023-11-26 12:34:09.000000000 +0100
@@ -1,12 +1,18 @@
 {% extends "admin/base_site.html" %}
-
-{% block title %}Queues {{ block.super }}{% endblock %}
+{% load static %}
 
 {% block extrastyle %}
     {{ block.super }}
-    <style>table {width: 100%;}</style>
+    <link rel="stylesheet" type="text/css" href="{% static 
"admin/css/changelists.css" %}">
+    <style>
+        #changelist table thead th:first-child {
+            width: inherit
+        }
+    </style>
 {% endblock %}
 
+{% block title %}Queues{% endblock %}
+
 {% block content_title %}<h1>Queues</h1>{% endblock %}
 
 {% block breadcrumbs %}
@@ -19,84 +25,148 @@
 {% block content %}
 
 <div id="content-main">
-
-    <div class="module">
-        <table>
-            <thead>
-                <tr>
-                    <th>Name</th>
-                    <th>Queued Jobs</th>
-                    <th>Oldest Queued Job</th>
-                    <th>Active Jobs</th>
-                    <th>Deferred Jobs</th>
-                    <th>Finished Jobs</th>
-                    <th>Failed Jobs</th>
-                    <th>Scheduled Jobs</th>
-                    <th>Workers</th>
-                    <th>Host</th>
-                    <th>Port</th>
-                    <th>DB</th>
-                    {% if queue.scheduler_pid is not False %}
-                        <th>Scheduler PID</th>
-                    {% endif%}
-                </tr>
-            </thead>
-            <tbody>
-                {% for queue in queues %}
-                    <tr class = "{% cycle 'row1' 'row2' %}">
-                        <th>
-                            <a href = "{% url 'rq_jobs' queue.index %}">
-                                {{ queue.name }}
-                            </a>
-                        </th>
-                        <td>
-                                <a href = "{% url 'rq_jobs' queue.index %}">
-                                    {{ queue.jobs }}
-                                </a>
-                        </td>
-                        <td>{{ queue.oldest_job_timestamp }}</td>
-                        <th>
-                            <a href = "{% url 'rq_started_jobs' queue.index 
%}">
-                                {{ queue.started_jobs }}
-                            </a>
-                        </th>
-                        <th>
-                            <a href = "{% url 'rq_deferred_jobs' queue.index 
%}">
-                                {{ queue.deferred_jobs }}
-                            </a>
-                        </th>
-                        <th>
-                              <a href = "{% url 'rq_finished_jobs' queue.index 
%}">
-                                  {{ queue.finished_jobs }}
-                              </a>
-                        </th>
-                        <th>
-                              <a href = "{% url 'rq_failed_jobs' queue.index 
%}">
-                                  {{ queue.failed_jobs }}
-                              </a>
-                        </th>
-                        <th>
-                            <a href = "{% url 'rq_scheduled_jobs' queue.index 
%}">
-                                {{ queue.scheduled_jobs }}
-                            </a>
-                        </th>
-                        <th><a href = "{% url 'rq_workers' queue.index %}">
-                                {{ queue.workers }}
-                            </a>
-                        </th>
-                        <td>{{ queue.connection_kwargs.host }}</td>
-                        <td>{{ queue.connection_kwargs.port }}</td>
-                        <td>{{ queue.connection_kwargs.db }}</td>
-                        {% if queue.scheduler_pid is not False %}
-                        <td>{{ queue.scheduler_pid|default_if_none:"Inactive" 
}}</td>
-                        {% endif %}
-                    </tr>
-                {% endfor %}
-            </tbody>
-        </table>
-        <br />
-        <a href="{% url 'rq_home_json' %}">View as JSON</a>
+    <div class="module" id="changelist">
+        <div class="changelist-form-container">
+            <form id="changelist-form" method="post" {% if cl.formset and 
cl.formset.is_multipart %}
+                enctype="multipart/form-data" {% endif %} novalidate>{% 
csrf_token %}
+                <div class="results">
+                    <table id="results_list">
+                        <thead>
+                            <tr>
+                                <th scope="col" class="sortable">
+                                    <div class="text"><span>Name</span></div>
+                                    <div class="clear"></div>
+                                </th>
+                                <th scope="col" class="sortable">
+                                    <div class="text"><span>Queued 
Jobs</span></div>
+                                    <div class="clear"></div>
+                                </th>
+                                <th scope="col" class="sortable">
+                                    <div class="text"><span>Oldest Queued 
Job</span></div>
+                                    <div class="clear"></div>
+                                </th>
+                                <th scope="col" class="sortable">
+                                    <div class="text"><span>Active 
Jobs</span></div>
+                                    <div class="clear"></div>
+                                </th>
+                                <th scope="col" class="sortable">
+                                    <div class="text"><span>Deferred 
Jobs</span></div>
+                                    <div class="clear"></div>
+                                </th>
+                                <th scope="col" class="sortable">
+                                    <div class="text"><span>Finished 
Jobs</span></div>
+                                    <div class="clear"></div>
+                                </th>
+                                <th scope="col" class="sortable">
+                                    <div class="text"><span>Failed 
Jobs</span></div>
+                                    <div class="clear"></div>
+                                </th>
+                                <th scope="col" class="sortable">
+                                    <div class="text"><span>Scheduled 
Jobs</span></div>
+                                    <div class="clear"></div>
+                                </th>
+                                <th scope="col" class="sortable">
+                                    <div 
class="text"><span>Workers</span></div>
+                                    <div class="clear"></div>
+                                </th>
+                                <th scope="col" class="sortable">
+                                    <div class="text"><span>Host</span></div>
+                                    <div class="clear"></div>
+                                </th>
+                                <th scope="col" class="sortable">
+                                    <div class="text"><span>Port</span></div>
+                                    <div class="clear"></div>
+                                </th>
+                                <th scope="col" class="sortable">
+                                    <div class="text"><span>DB</span></div>
+                                    <div class="clear"></div>
+                                </th>
+                                {% if queue.scheduler_pid is not False %}
+                                <th scope="col" class="sortable">
+                                    <div class="text"><span>Scheduler 
PID</span></div>
+                                    <div class="clear"></div>
+                                </th>
+                                {% endif%}
+                            </tr>
+                        </thead>
+                        <tbody>
+                            {% for queue in queues %}
+                            <tr class="{% cycle 'row1' 'row2' %}">
+                                <th>
+                                    <a href="{% url 'rq_jobs' queue.index %}">
+                                        {{ queue.name }}
+                                    </a>
+                                </th>
+                                <th>
+                                    <a href="{% url 'rq_jobs' queue.index %}">
+                                        {{ queue.jobs }}
+                                    </a>
+                                </th>
+                                <td>{{ queue.oldest_job_timestamp }}</td>
+                                <th>
+                                    <a href="{% url 'rq_started_jobs' 
queue.index %}">
+                                        {{ queue.started_jobs }}
+                                    </a>
+                                </th>
+                                <th>
+                                    <a href="{% url 'rq_deferred_jobs' 
queue.index %}">
+                                        {{ queue.deferred_jobs }}
+                                    </a>
+                                </th>
+                                <th>
+                                    <a href="{% url 'rq_finished_jobs' 
queue.index %}">
+                                        {{ queue.finished_jobs }}
+                                    </a>
+                                </th>
+                                <th>
+                                    <a href="{% url 'rq_failed_jobs' 
queue.index %}">
+                                        {{ queue.failed_jobs }}
+                                    </a>
+                                </th>
+                                <th>
+                                    <a href="{% url 'rq_scheduled_jobs' 
queue.index %}">
+                                        {{ queue.scheduled_jobs }}
+                                    </a>
+                                </th>
+                                <th><a href="{% url 'rq_workers' queue.index 
%}">
+                                        {{ queue.workers }}
+                                    </a>
+                                </th>
+                                <td>{{ queue.connection_kwargs.host }}</td>
+                                <td>{{ queue.connection_kwargs.port }}</td>
+                                <td>{{ queue.connection_kwargs.db }}</td>
+                                {% if queue.scheduler_pid is not False %}
+                                <td>{{ 
queue.scheduler_pid|default_if_none:"Inactive" }}</td>
+                                {% endif %}
+                            </tr>
+                            {% endfor %}
+                        </tbody>
+                    </table>
+                </div>
+                <p class="paginator">
+                    <a href="{% url 'rq_home_json' %}" class="showall">View as 
JSON</a>
+                </p>
+            </form>
+        </div>
     </div>
+
+    {% if schedulers %}
+    <h2>RQ Scheduler</h2>
+    <table>
+        <thead>
+            <tr>
+                <th>Redis Connection</th>
+                <th>Recurring Jobs</th>
+            </tr>
+        </thead>
+        {% for connection, scheduler in schedulers.items %}
+        <tr class="{% cycle 'row1' 'row2' %}">
+            <td><a href="{% url 'rq_scheduler_jobs' scheduler.index %}">{{ 
connection }}</a></td>
+            <td><a href="{% url 'rq_scheduler_jobs' scheduler.index %}">{{ 
scheduler.count }}</a></td>
+        </tr>
+        {% endfor %}
+    </table>
+    {% endif %}
 </div>
 
 {% endblock %}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-rq-2.8.1/django_rq/templatetags/django_rq.py 
new/django-rq-2.9.0/django_rq/templatetags/django_rq.py
--- old/django-rq-2.8.1/django_rq/templatetags/django_rq.py     2023-05-14 
03:31:42.000000000 +0200
+++ new/django-rq-2.9.0/django_rq/templatetags/django_rq.py     2023-11-26 
12:34:09.000000000 +0100
@@ -8,7 +8,7 @@
 
 @register.filter
 def to_localtime(time):
-    '''Converts naive datetime to localtime based on settings'''
+    """Converts naive datetime to localtime based on settings"""
 
     utc_time = time.replace(tzinfo=timezone.utc)
     to_zone = timezone.get_default_timezone()
@@ -17,7 +17,7 @@
 
 @register.filter
 def show_func_name(job):
-    '''Shows job.func_name and handles errors during deserialization'''
+    """Shows job.func_name and handles errors during deserialization"""
     try:
         return job.func_name
     except Exception as e:
@@ -27,3 +27,12 @@
 @register.filter
 def force_escape(text):
     return escape(text)
+
+
[email protected]
+def items(dictionary):
+    """
+    Explicitly calls `dictionary.items` function
+    to avoid django from accessing the key `items` if any.
+    """
+    return dictionary.items()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-rq-2.8.1/django_rq/tests/settings.py 
new/django-rq-2.9.0/django_rq/tests/settings.py
--- old/django-rq-2.8.1/django_rq/tests/settings.py     2023-05-14 
03:31:42.000000000 +0200
+++ new/django-rq-2.9.0/django_rq/tests/settings.py     2023-11-26 
12:34:09.000000000 +0100
@@ -187,6 +187,12 @@
         'DB': 0,
         'DEFAULT_TIMEOUT': 400,
     },
+    'test_serializer': {
+        'HOST': REDIS_HOST,
+        'PORT': 6379,
+        'DB': 0,
+        'SERIALIZER': 'rq.serializers.JSONSerializer',
+    },
 }
 RQ = {
     'AUTOCOMMIT': False,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-rq-2.8.1/django_rq/tests/test_views.py 
new/django-rq-2.9.0/django_rq/tests/test_views.py
--- old/django-rq-2.8.1/django_rq/tests/test_views.py   2023-05-14 
03:31:42.000000000 +0200
+++ new/django-rq-2.9.0/django_rq/tests/test_views.py   2023-11-26 
12:34:09.000000000 +0100
@@ -1,5 +1,5 @@
 import uuid
-from datetime import datetime
+from datetime import datetime, timedelta, timezone
 from unittest.mock import PropertyMock, patch
 
 
@@ -17,6 +17,7 @@
 )
 
 from django_rq import get_queue
+from django_rq.queues import get_scheduler
 from django_rq.workers import get_worker
 
 from .fixtures import access_self, failing_job
@@ -395,3 +396,55 @@
 
         for job_id in job_ids:
             self.assertIn(job_id, canceled_job_registry)
+
+    def test_scheduler_jobs(self):
+        # Override testing RQ_QUEUES
+        queues = [
+            {
+                "connection_config": {
+                    "DB": 0,
+                    "HOST": "localhost",
+                    "PORT": 6379,
+                },
+                "name": "default",
+            }
+        ]
+        with patch(
+            "django_rq.utils.QUEUES_LIST",
+            new_callable=PropertyMock(return_value=queues),
+        ):
+            scheduler = get_scheduler("default")
+            scheduler_index = get_queue_index("default")
+
+            # Enqueue some jobs
+            cron_job = scheduler.cron("10 9 * * *", func=access_self, 
id="cron-job")
+            forever_job = scheduler.schedule(
+                scheduled_time=datetime.now() + timedelta(minutes=10),
+                interval=600,
+                func=access_self,
+                id="forever-repeat",
+            )
+            repeat_job = scheduler.schedule(
+                scheduled_time=datetime.now() + timedelta(minutes=30),
+                repeat=30,
+                func=access_self,
+                interval=600,
+                id="thirty-repeat",
+            )
+
+            response = self.client.get(
+                reverse("rq_scheduler_jobs", args=[scheduler_index])
+            )
+            self.assertEqual(response.context["num_jobs"], 3)
+            context_jobs = {job.id: job for job in response.context["jobs"]}
+            self.assertEqual(context_jobs["cron-job"].schedule, "cron: '10 9 * 
* *'")
+            self.assertEqual(context_jobs["forever-repeat"].schedule, 
"interval: 600")
+            self.assertEqual(
+                context_jobs["thirty-repeat"].schedule, "interval: 600 repeat: 
30"
+            )
+
+            index_response = self.client.get(reverse("rq_home"))
+            self.assertEqual(
+                index_response.context["schedulers"],
+                {"localhost:6379/1": {"count": 3, "index": 0}},
+            )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-rq-2.8.1/django_rq/tests/tests.py 
new/django-rq-2.9.0/django_rq/tests/tests.py
--- old/django-rq-2.8.1/django_rq/tests/tests.py        2023-05-14 
03:31:42.000000000 +0200
+++ new/django-rq-2.9.0/django_rq/tests/tests.py        2023-11-26 
12:34:09.000000000 +0100
@@ -13,6 +13,7 @@
 
 from redis.exceptions import ConnectionError
 from rq import get_current_job, Queue
+import rq
 from rq.exceptions import NoSuchJobError
 from rq.job import Job
 from rq.registry import FinishedJobRegistry, ScheduledJobRegistry
@@ -457,6 +458,14 @@
         queue = get_queue('test1')
         self.assertEqual(queue._default_timeout, 400)
 
+    def test_get_queue_serializer(self):
+        """
+        Test that the correct serializer is set on the queue.
+        """
+        queue = get_queue('test_serializer')
+        self.assertEqual(queue.name, 'test_serializer')
+        self.assertEqual(queue.serializer, rq.serializers.JSONSerializer)
+
 
 @override_settings(RQ={'AUTOCOMMIT': True})
 class DecoratorTest(TestCase):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-rq-2.8.1/django_rq/urls.py 
new/django-rq-2.9.0/django_rq/urls.py
--- old/django-rq-2.8.1/django_rq/urls.py       2023-05-14 03:31:42.000000000 
+0200
+++ new/django-rq-2.9.0/django_rq/urls.py       2023-11-26 12:34:09.000000000 
+0100
@@ -10,6 +10,7 @@
     re_path(r'^workers/(?P<queue_index>[\d]+)/(?P<key>[-\w\.\:\$]+)/$', 
views.worker_details, name='rq_worker_details'),
     re_path(r'^queues/(?P<queue_index>[\d]+)/finished/$', views.finished_jobs, 
name='rq_finished_jobs'),
     re_path(r'^queues/(?P<queue_index>[\d]+)/failed/$', views.failed_jobs, 
name='rq_failed_jobs'),
+    re_path(r'^queues/(?P<queue_index>[\d]+)/failed/clear/$', 
views.delete_failed_jobs, name='rq_delete_failed_jobs'),
     re_path(r'^queues/(?P<queue_index>[\d]+)/scheduled/$', 
views.scheduled_jobs, name='rq_scheduled_jobs'),
     re_path(r'^queues/(?P<queue_index>[\d]+)/started/$', views.started_jobs, 
name='rq_started_jobs'),
     re_path(r'^queues/(?P<queue_index>[\d]+)/deferred/$', views.deferred_jobs, 
name='rq_deferred_jobs'),
@@ -32,4 +33,5 @@
     re_path(
         r'^queues/(?P<queue_index>[\d]+)/(?P<job_id>[^/]+)/stop/$', 
views.stop_job, name='rq_stop_job'
     ),
+    re_path(r'^schedulers/(?P<scheduler_index>[\d]+)/$', views.scheduler_jobs, 
name='rq_scheduler_jobs'),
 ]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-rq-2.8.1/django_rq/utils.py 
new/django-rq-2.9.0/django_rq/utils.py
--- old/django-rq-2.8.1/django_rq/utils.py      2023-05-14 03:31:42.000000000 
+0200
+++ new/django-rq-2.9.0/django_rq/utils.py      2023-11-26 12:34:09.000000000 
+0100
@@ -1,3 +1,5 @@
+from django.core.exceptions import ImproperlyConfigured
+from rq.command import send_stop_job_command
 from rq.job import Job
 from rq.registry import (
     DeferredJobRegistry,
@@ -7,15 +9,13 @@
     StartedJobRegistry,
     clean_registries,
 )
-from rq.command import send_stop_job_command
 from rq.worker import Worker
 from rq.worker_registration import clean_worker_registry
 
-
 from .queues import get_connection, get_queue_by_index, get_scheduler
 from .settings import QUEUES_LIST
 from .templatetags.django_rq import to_localtime
-from django.core.exceptions import ImproperlyConfigured
+
 
 def get_scheduler_pid(queue):
     '''Checks whether there's a scheduler-lock on a particular queue, and 
returns the PID.
@@ -30,6 +30,7 @@
         return False  # Not possible to give useful information without 
creating a performance issue (redis.keys())
     except ImproperlyConfigured:
         from rq.scheduler import RQScheduler
+
         # When a scheduler acquires a lock it adds an expiring key: (e.g: 
rq:scheduler-lock:<queue.name>)
         #TODO: (RQ>= 1.13) return queue.scheduler_pid 
         pid = queue.connection.get(RQScheduler.get_locking_key(queue.name))
@@ -89,15 +90,37 @@
         queue_data['scheduled_jobs'] = len(scheduled_job_registry)
 
         queues.append(queue_data)
+
     return {'queues': queues}
 
 
+def get_scheduler_statistics():
+    schedulers = {}
+    for index, config in enumerate(QUEUES_LIST):
+        # there is only one scheduler per redis connection, so we use the 
connection as key
+        # to handle the possibility of a configuration with multiple redis 
connections and scheduled
+        # jobs in more than one of them
+        queue = get_queue_by_index(index)
+        connection_kwargs = queue.connection.connection_pool.connection_kwargs
+        conn_key = 
f"{connection_kwargs['host']}:{connection_kwargs['port']}/{connection_kwargs['db']}"
+        if conn_key not in schedulers:
+            try:
+                scheduler = get_scheduler(config['name'])
+                schedulers[conn_key] ={
+                    'count': scheduler.count(),
+                    'index': index,
+                }
+            except ImproperlyConfigured:
+                pass
+    return {'schedulers': schedulers}
+
+
 def get_jobs(queue, job_ids, registry=None):
     """Fetch jobs in bulk from Redis.
     1. If job data is not present in Redis, discard the result
     2. If `registry` argument is supplied, delete empty jobs from registry
     """
-    jobs = Job.fetch_many(job_ids, connection=queue.connection)
+    jobs = Job.fetch_many(job_ids, connection=queue.connection, 
serializer=queue.serializer)
     valid_jobs = []
     for i, job in enumerate(jobs):
         if job is None:
@@ -108,6 +131,7 @@
 
     return valid_jobs
 
+
 def stop_jobs(queue, job_ids):
     job_ids = job_ids if isinstance(job_ids, (list, tuple)) else [job_ids]
     stopped_job_ids = []
@@ -119,4 +143,4 @@
             failed_to_stop_job_ids.append(job_id)
             continue
         stopped_job_ids.append(job_id)
-    return stopped_job_ids, failed_to_stop_job_ids
\ No newline at end of file
+    return stopped_job_ids, failed_to_stop_job_ids
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-rq-2.8.1/django_rq/views.py 
new/django-rq-2.9.0/django_rq/views.py
--- old/django-rq-2.8.1/django_rq/views.py      2023-05-14 03:31:42.000000000 
+0200
+++ new/django-rq-2.9.0/django_rq/views.py      2023-11-26 12:34:09.000000000 
+0100
@@ -9,7 +9,6 @@
 from django.urls import reverse
 from django.views.decorators.cache import never_cache
 from django.views.decorators.http import require_POST
-
 from redis.exceptions import ResponseError
 from rq import requeue_job
 from rq.exceptions import NoSuchJobError
@@ -23,17 +22,20 @@
 )
 from rq.worker import Worker
 from rq.worker_registration import clean_worker_registry
-from rq.command import send_stop_job_command
 
-from .queues import get_queue_by_index
-from .settings import API_TOKEN
-from .utils import get_statistics, get_jobs, stop_jobs
+from .queues import get_queue_by_index, get_scheduler_by_index
+from .settings import API_TOKEN, QUEUES_MAP
+from .utils import get_jobs, get_scheduler_statistics, get_statistics, 
stop_jobs
 
 
 @never_cache
 @staff_member_required
 def stats(request):
-    context_data = {**admin.site.each_context(request), 
**get_statistics(run_maintenance_tasks=True)}
+    context_data = {
+        **admin.site.each_context(request),
+        **get_statistics(run_maintenance_tasks=True),
+        **get_scheduler_statistics(),
+    }
     return render(request, 'django_rq/stats.html', context_data)
 
 
@@ -287,7 +289,7 @@
 
         for job_id in job_ids:
             try:
-                jobs.append(Job.fetch(job_id, connection=queue.connection))
+                jobs.append(Job.fetch(job_id, connection=queue.connection, 
serializer=queue.serializer))
             except NoSuchJobError:
                 pass
 
@@ -314,7 +316,7 @@
     queue = get_queue_by_index(queue_index)
 
     try:
-        job = Job.fetch(job_id, connection=queue.connection)
+        job = Job.fetch(job_id, connection=queue.connection, 
serializer=queue.serializer)
     except NoSuchJobError:
         raise Http404("Couldn't find job with this ID: %s" % job_id)
 
@@ -351,7 +353,7 @@
 def delete_job(request, queue_index, job_id):
     queue_index = int(queue_index)
     queue = get_queue_by_index(queue_index)
-    job = Job.fetch(job_id, connection=queue.connection)
+    job = Job.fetch(job_id, connection=queue.connection, 
serializer=queue.serializer)
 
     if request.method == 'POST':
         # Remove job id from queue and delete the actual job
@@ -374,10 +376,10 @@
 def requeue_job_view(request, queue_index, job_id):
     queue_index = int(queue_index)
     queue = get_queue_by_index(queue_index)
-    job = Job.fetch(job_id, connection=queue.connection)
+    job = Job.fetch(job_id, connection=queue.connection, 
serializer=queue.serializer)
 
     if request.method == 'POST':
-        requeue_job(job_id, connection=queue.connection)
+        requeue_job(job_id, connection=queue.connection, 
serializer=queue.serializer)
         messages.info(request, 'You have successfully requeued %s' % job.id)
         return redirect('rq_job_detail', queue_index, job_id)
 
@@ -431,7 +433,7 @@
         # Confirmation received
         for job_id in job_ids:
             try:
-                requeue_job(job_id, connection=queue.connection)
+                requeue_job(job_id, connection=queue.connection, 
serializer=queue.serializer)
                 count += 1
             except NoSuchJobError:
                 pass
@@ -451,6 +453,35 @@
 
 @never_cache
 @staff_member_required
+def delete_failed_jobs(request, queue_index):
+    queue_index = int(queue_index)
+    queue = get_queue_by_index(queue_index)
+    registry = FailedJobRegistry(queue=queue)
+
+    if request.method == 'POST':
+        job_ids = registry.get_job_ids()
+        jobs = Job.fetch_many(job_ids, connection=queue.connection)
+        count = 0
+        for job in jobs:
+            if job:
+                job.delete()
+                count += 1
+
+        messages.info(request, 'You have successfully deleted %d jobs!' % 
count)
+        return redirect('rq_home')
+
+    context_data = {
+        **admin.site.each_context(request),
+        'queue_index': queue_index,
+        'queue': queue,
+        'total_jobs': len(registry),
+    }
+
+    return render(request, 'django_rq/clear_failed_queue.html', context_data)
+
+
+@never_cache
+@staff_member_required
 def confirm_action(request, queue_index):
     queue_index = int(queue_index)
     queue = get_queue_by_index(queue_index)
@@ -486,14 +517,14 @@
 
             if request.POST['action'] == 'delete':
                 for job_id in job_ids:
-                    job = Job.fetch(job_id, connection=queue.connection)
+                    job = Job.fetch(job_id, connection=queue.connection, 
serializer=queue.serializer)
                     # Remove job id from queue and delete the actual job
                     queue.connection.lrem(queue.key, 0, job.id)
                     job.delete()
                 messages.info(request, 'You have successfully deleted %s 
jobs!' % len(job_ids))
             elif request.POST['action'] == 'requeue':
                 for job_id in job_ids:
-                    requeue_job(job_id, connection=queue.connection)
+                    requeue_job(job_id, connection=queue.connection, 
serializer=queue.serializer)
                 messages.info(request, 'You have successfully requeued %d  
jobs!' % len(job_ids))
             elif request.POST['action'] == 'stop':
                 stopped, failed_to_stop = stop_jobs(queue, job_ids)
@@ -511,7 +542,7 @@
     """Enqueue deferred jobs"""
     queue_index = int(queue_index)
     queue = get_queue_by_index(queue_index)
-    job = Job.fetch(job_id, connection=queue.connection)
+    job = Job.fetch(job_id, connection=queue.connection, 
serializer=queue.serializer)
 
     if request.method == 'POST':
         try:
@@ -557,4 +588,45 @@
         return redirect('rq_job_detail', queue_index, job_id)
     else:
         messages.error(request, 'Failed to stop %s' % job_id)
-        return redirect('rq_job_detail', queue_index, job_id)
\ No newline at end of file
+        return redirect('rq_job_detail', queue_index, job_id)
+
+
+@never_cache
+@staff_member_required
+def scheduler_jobs(request, scheduler_index):
+    scheduler = get_scheduler_by_index(scheduler_index)
+
+    items_per_page = 100
+    num_jobs = scheduler.count()
+    page = int(request.GET.get('page', 1))
+    jobs = []
+
+    if num_jobs > 0:
+        last_page = int(ceil(num_jobs / items_per_page))
+        page_range = range(1, last_page + 1)
+        offset = items_per_page * (page - 1)
+        jobs_times = scheduler.get_jobs(with_times=True, offset=offset, 
length=items_per_page)
+        for job, time in jobs_times:
+            job.next_run = time
+            job.queue_index = QUEUES_MAP.get(job.origin, 0)
+            if 'cron_string' in job.meta:
+                job.schedule = f"cron: '{job.meta['cron_string']}'"
+            elif 'interval' in job.meta:
+                job.schedule = f"interval: {job.meta['interval']}"
+                if 'repeat' in job.meta:
+                    job.schedule += f" repeat: {job.meta['repeat']}"
+            else:
+                job.schedule = 'unknown'
+            jobs.append(job)
+    else:
+        page_range = []
+
+    context_data = {
+        **admin.site.each_context(request),
+        'scheduler': scheduler,
+        'jobs': jobs,
+        'num_jobs': num_jobs,
+        'page': page,
+        'page_range': page_range,
+    }
+    return render(request, 'django_rq/scheduler.html', context_data)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/django-rq-2.8.1/integration_test/integration_app/views.py 
new/django-rq-2.9.0/integration_test/integration_app/views.py
--- old/django-rq-2.8.1/integration_test/integration_app/views.py       
2023-05-14 03:31:42.000000000 +0200
+++ new/django-rq-2.9.0/integration_test/integration_app/views.py       
2023-11-26 12:34:09.000000000 +0100
@@ -13,4 +13,3 @@
         return HttpResponse("Enqueued")
     names = [m.name for m in MyModel.objects.order_by("name")]
     return HttpResponse("Entries: {}".format(",".join(names)))
-        
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-rq-2.8.1/integration_test/requirements.txt 
new/django-rq-2.9.0/integration_test/requirements.txt
--- old/django-rq-2.8.1/integration_test/requirements.txt       2023-05-14 
03:31:42.000000000 +0200
+++ new/django-rq-2.9.0/integration_test/requirements.txt       2023-11-26 
12:34:09.000000000 +0100
@@ -1,5 +1,5 @@
 -e ..
-Django==3.2.19
+Django==3.2.20
 gunicorn==20.1.0
-psycopg2==2.9.6
-requests==2.30.0
+psycopg2==2.9.7
+requests==2.31.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-rq-2.8.1/setup.py new/django-rq-2.9.0/setup.py
--- old/django-rq-2.8.1/setup.py        2023-05-14 03:31:42.000000000 +0200
+++ new/django-rq-2.9.0/setup.py        2023-11-26 12:34:09.000000000 +0100
@@ -3,7 +3,7 @@
 
 setup(
     name='django-rq',
-    version='2.8.1',
+    version='2.9.0',
     author='Selwin Ong',
     author_email='[email protected]',
     packages=['django_rq'],
@@ -14,7 +14,7 @@
     zip_safe=False,
     include_package_data=True,
     package_data={'': ['README.rst']},
-    install_requires=['django>=2.0', 'rq>=1.14', 'redis>=3'],
+    install_requires=['django>=3.2', 'rq>=1.14', 'redis>=3'],
     extras_require={
         'Sentry': ['raven>=6.1.0'],
         'testing': ['mock>=2.0.0'],
@@ -28,12 +28,11 @@
         'Operating System :: OS Independent',
         'Programming Language :: Python',
         'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.3',
-        'Programming Language :: Python :: 3.4',
-        'Programming Language :: Python :: 3.5',
-        'Programming Language :: Python :: 3.6',
-        'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',
+        'Programming Language :: Python :: 3.9',
+        'Programming Language :: Python :: 3.10',
+        'Programming Language :: Python :: 3.11',
+        'Programming Language :: Python :: 3.12',
         'Topic :: Internet :: WWW/HTTP',
         'Topic :: Software Development :: Libraries :: Python Modules',
     ],

Reply via email to