Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Args+kwargs with redis 3 #35

Open
wants to merge 33 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
83cc432
add seconds option to interval_units with tests
Oct 10, 2018
5506b97
add seconds option to interval_units with tests
Oct 10, 2018
70b2928
Proposed handling of intervals in seconds.
tom-price May 26, 2019
46fa4b6
Added Django delete permission check introduced in 2.1
tom-price May 27, 2019
5aa1893
Admin site improvements, added bulk enable and disable
tom-price May 28, 2019
921fa8c
Changed schedule functions
tom-price May 28, 2019
f017817
Simplified Job factories through inheritance of base factory class.
tom-price May 28, 2019
80cc705
Overall compatibility bump by changes in the RQ package.
tom-price May 29, 2019
24cd808
Re-written tests
tom-price May 29, 2019
c386c71
Made better use of job factories' kwargs and de-duped some tests
tom-price May 29, 2019
6b78dd9
Added fakeredis server for testing and cleanup of unused code in models
tom-price May 29, 2019
9fb95ec
Updated README versions
tom-price May 29, 2019
9ee9beb
Ensures RepeatableJob result_ttls are long enough so next is rescheduled
Mar 12, 2019
6fc3443
Added Run Now action to admin.
tom-price May 29, 2019
56317d6
Merge remote-tracking branch 'upstream/master' into bug_fixes_and_imp…
tom-price May 29, 2019
e3b5874
Fixed incorrectly failing test of clean_result_ttl
tom-price Jun 3, 2019
63db7c9
Fixed incorrectly failing test of clean_result_ttl and Missed AlterField
tom-price Jun 3, 2019
8319d9c
Merge branch 'bug_fixes_and_improvements' into args+kwargs_with_redis_3
tom-price Jun 3, 2019
4bb624e
Added Arg and Kwarg models plus tests.
tom-price Jun 3, 2019
a0019b5
Hiding the unselected args / kwargs.
tom-price Jun 4, 2019
161a5ce
An unset result_ttl also avoids the issue with a too short interval
tom-price Jun 6, 2019
3554486
Updated documentation and bumped version to 1.2 due to (kw)args addition
tom-price Jun 6, 2019
cbd6d13
Merge pull request #1 from tom-price/seconds_interval_units
mattjegan Mar 6, 2020
7dd8cfc
Merge pull request #2 from isl-x/master
mattjegan Mar 9, 2020
7cbc198
Merge remote-tracking branch 'remotes/upstream/master' into seconds_i…
tom-price Sep 17, 2020
861c1ab
Fixed duplicate migration and added disclaimer to README.md
tom-price Sep 17, 2020
a3ac34b
Merge branch 'seconds_option_pr_29' into seconds_interval_units
tom-price Sep 17, 2020
b132ce6
Merge remote-tracking branch 'remotes/origin/seconds_interval_units' …
tom-price Sep 17, 2020
8b263e2
Removed FakeRedis due to issues it was causing and restricted Django 3.0
tom-price Sep 17, 2020
9894b14
Removed FakeRedis due to issues it was causing and restricted Django …
tom-price Sep 17, 2020
ba49edf
Merge branch 'bug_fixes_and_improvements' into args+kwargs_with_redis_3
tom-price Sep 17, 2020
15189ed
Added passthrough of args and kwargs to run_job_now admin action.
tom-price Sep 17, 2020
3f59fe7
Merge branch 'master' into args+kwargs_with_redis_3
tom-price Sep 17, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,7 @@ target/

# SQLite
*.sqlite3

# Pipenv
Pipfile
Pipfile.lock
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
include MANIFEST.in
include README.md
recursive-include scheduler *.py
recursive-include scheduler *.py *.js
exclude testproject19
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ Currently, when you pip install Django RQ Scheduler the following packages are a

* django >= 1.9
* django-model-utils >= 2.4
* django-rq >= 0.9.3 (Django RQ requires RQ >= 0.5.5)
* rq-scheduler >= 0.6.0
* django-rq >= 2.0 (Django RQ requires RQ >= 1.0 due to changes in redis >= 3)
* rq-scheduler >= 0.9.0
* pytz >= 2015.7
* croniter >= 0.3.24

Expand Down Expand Up @@ -45,6 +45,10 @@ pip install django-rq-scheduler


```
If you also wish to run the underpinning **RQ Scheduler** at an interval different from its default of
once every 60 seconds you can do so by setting `DJANGO_RQ_SCHEDULER_INTERVAL` to the new preferred interval.
This is important if you want a job to either run multiple times a minute
or to schedule a job more precisely than within a 60 second window.

2. Configure Django RQ. See https://github.com/ui/django-rq#installation

Expand Down Expand Up @@ -104,6 +108,8 @@ def count():
6. Enter the time the first job is to be executed in the **Scheduled time** field. Side Note: Enter the date via the browser's local timezone, the time will automatically convert UTC.

7. Enter an **Interval**, and choose the **Interval unit**. This will calculate the time before the function is called again.
* The **result TTL** must be either indefinite `-1`, unset, or greater than this interval.
* The **Interval** must be a multiple of the scheduler's interval. With a default config this interval is 60 seconds and is only a consideration when using `seconds` as the **Interval Unit**. If a shorter interval is needed **rq-scheduler**'s interval can be changed by setting `DJANGO_RQ_SCHEDULER_INTERVAL` to something other than `60`; it's advised to set it to a divisor of `60`.

8. In the **Repeat** field, enter the number of time the job is to be ran. Leaving the field empty, means the job will be scheduled to run forever.

Expand Down
2 changes: 1 addition & 1 deletion scheduler/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__version__ = '1.1.3'
__version__ = '1.2.0'

default_app_config = 'scheduler.apps.SchedulerConfig'
175 changes: 121 additions & 54 deletions scheduler/admin.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,140 @@
from __future__ import unicode_literals

from django.conf import settings
from django.contrib import admin
from django.contrib import admin, messages
from django.contrib.contenttypes.admin import GenericStackedInline
from django.templatetags.tz import utc
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _

from scheduler.models import CronJob, RepeatableJob, ScheduledJob

from scheduler.models import CronJob, JobArg, JobKwarg, RepeatableJob, ScheduledJob

QUEUES = [(key, key) for key in settings.RQ_QUEUES.keys()]


class QueueMixin(object):
actions = ['delete_model']
class HiddenMixin(object):
# pass
class Media:
js = ['admin/js/jquery.init.js', 'scheduler/js/base.js']


class JobArgInline(HiddenMixin, GenericStackedInline):
model = JobArg
extra = 0
fieldsets = (
(None, {
'fields': ('arg_type', 'str_val', 'int_val', 'bool_val', 'datetime_val',),
}),
)


class JobKwargInline(HiddenMixin, GenericStackedInline):
model = JobKwarg
extra = 0
fieldsets = (
(None, {
'fields': ('key', 'arg_type', 'str_val', 'int_val', 'bool_val', 'datetime_val',),
}),
)


class JobAdmin(admin.ModelAdmin):
actions = ['delete_model', 'disable_selected', 'enable_selected', 'run_job_now']
inlines = [JobArgInline, JobKwargInline]
list_filter = ('enabled', )
list_display = ('enabled', 'name', 'job_id', 'function_string', 'is_scheduled',)
list_display_links = ('name',)
readonly_fields = ('job_id', )
fieldsets = (
(None, {
'fields': ('name', 'callable', 'enabled',),
}),
(_('RQ Settings'), {
'fields': ('queue', 'job_id',),
}),
)

def get_actions(self, request):
actions = super(QueueMixin, self).get_actions(request)
del actions['delete_selected']
actions = super(JobAdmin, self).get_actions(request)
actions.pop('delete_selected', None)
return actions

def get_form(self, request, obj=None, **kwargs):
queue_field = self.model._meta.get_field('queue')
queue_field.choices = QUEUES
return super(QueueMixin, self).get_form(request, obj, **kwargs)
return super(JobAdmin, self).get_form(request, obj, **kwargs)

def delete_model(self, request, obj):
if hasattr(obj, 'all'):
for o in obj.all():
o.delete()
else:
def delete_model(self, request, queryset):
rows_deleted = 0
for obj in queryset.all().iterator():
obj.delete()
rows_deleted += 1
if rows_deleted == 1:
message_bit = "1 job was"
else:
message_bit = "%s jobs were" % rows_deleted

level = messages.WARNING if not rows_deleted else messages.INFO
self.message_user(request, "%s successfully deleted." % message_bit, level=level)
delete_model.short_description = _("Delete selected %(verbose_name_plural)s")
delete_model.allowed_permissions = ('delete',)

def disable_selected(self, request, queryset):
rows_updated = 0
for obj in queryset.filter(enabled=True).iterator():
obj.enabled = False
obj.save()
rows_updated += 1
if rows_updated == 1:
message_bit = "1 job was"
else:
message_bit = "%s jobs were" % rows_updated

level = messages.WARNING if not rows_updated else messages.INFO
self.message_user(request, "%s successfully disabled." % message_bit, level=level)
disable_selected.short_description = _("Disable selected %(verbose_name_plural)s")
disable_selected.allowed_permissions = ('change',)

def enable_selected(self, request, queryset):
rows_updated = 0
for obj in queryset.filter(enabled=False).iterator():
obj.enabled = True
obj.save()
rows_updated += 1
if rows_updated == 1:
message_bit = "1 job was"
else:
message_bit = "%s jobs were" % rows_updated
level = messages.WARNING if not rows_updated else messages.INFO
self.message_user(request, "%s successfully enabled." % message_bit, level=level)
enable_selected.short_description = _("Enable selected %(verbose_name_plural)s")
enable_selected.allowed_permissions = ('change',)

def run_job_now(self, request, queryset):
job_names = []
for obj in queryset:
kwargs = obj.parse_kwargs()
if obj.timeout:
kwargs['timeout'] = obj.timeout
if hasattr(obj, 'result_ttl') and obj.result_ttl is not None:
kwargs['job_result_ttl'] = obj.result_ttl
obj.scheduler().enqueue_at(
utc(now()),
obj.callable_func(),
*obj.parse_args(),
**kwargs
)
job_names.append(obj.name)
self.message_user(request, "The following jobs have been run: %s" % ', '.join(job_names))
run_job_now.short_description = "Run now"
run_job_now.allowed_permissions = ('change',)


@admin.register(ScheduledJob)
class ScheduledJobAdmin(QueueMixin, admin.ModelAdmin):
list_display = (
'name', 'job_id', 'is_scheduled', 'scheduled_time', 'enabled')
list_filter = ('enabled', )
list_editable = ('enabled', )
class ScheduledJobAdmin(JobAdmin):
list_display = JobAdmin.list_display + ('scheduled_time',)

readonly_fields = ('job_id', )
fieldsets = (
(None, {
'fields': ('name', 'callable', 'enabled', ),
}),
(_('RQ Settings'), {
'fields': ('queue', 'job_id', ),
}),
fieldsets = JobAdmin.fieldsets + (
(_('Scheduling'), {
'fields': (
'scheduled_time',
Expand All @@ -58,48 +146,27 @@ class ScheduledJobAdmin(QueueMixin, admin.ModelAdmin):


@admin.register(RepeatableJob)
class RepeatableJobAdmin(QueueMixin, admin.ModelAdmin):
list_display = (
'name', 'job_id', 'is_scheduled', 'scheduled_time', 'interval_display',
'enabled')
list_filter = ('enabled', )
list_editable = ('enabled', )
class RepeatableJobAdmin(JobAdmin):
list_display = JobAdmin.list_display + ('scheduled_time', 'interval_display')

readonly_fields = ('job_id', )
fieldsets = (
(None, {
'fields': ('name', 'callable', 'enabled', ),
}),
(_('RQ Settings'), {
'fields': ('queue', 'job_id', ),
}),
fieldsets = JobAdmin.fieldsets + (
(_('Scheduling'), {
'fields': (
'scheduled_time',
('interval', 'interval_unit', ),
'repeat',
'timeout',
'result_ttl'
'result_ttl',
),
}),
)


@admin.register(CronJob)
class CronJobAdmin(QueueMixin, admin.ModelAdmin):
list_display = (
'name', 'job_id', 'is_scheduled', 'cron_string', 'enabled')
list_filter = ('enabled', )
list_editable = ('enabled', )
class CronJobAdmin(JobAdmin):
list_display = JobAdmin.list_display + ('cron_string',)

readonly_fields = ('job_id', )
fieldsets = (
(None, {
'fields': ('name', 'callable', 'enabled', ),
}),
(_('RQ Settings'), {
'fields': ('queue', 'job_id', ),
}),
fieldsets = JobAdmin.fieldsets + (
(_('Scheduling'), {
'fields': (
'cron_string',
Expand Down
24 changes: 24 additions & 0 deletions scheduler/migrations/0006_seconds_interval_units.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 2.1.2 on 2018-10-10 01:02

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('scheduler', '0005_added_result_ttl'),
]

operations = [
migrations.AlterField(
model_name='cronjob',
name='cron_string',
field=models.CharField(help_text='Define the schedule in a crontab like syntax. Times are in UTC.',
max_length=64, verbose_name='cron string'),
),
migrations.AlterField(
model_name='repeatablejob',
name='interval_unit',
field=models.CharField(choices=[('seconds', 'seconds'), ('minutes', 'minutes'), ('hours', 'hours'), ('days', 'days'), ('weeks', 'weeks')], default='hours', max_length=12, verbose_name='interval unit'),
),
]
50 changes: 50 additions & 0 deletions scheduler/migrations/0007_added_args_kwargs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Generated by Django 2.2.2 on 2019-06-03 16:09

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('scheduler', '0006_seconds_interval_units'),
]

operations = [
migrations.CreateModel(
name='JobKwarg',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('arg_type', models.CharField(choices=[('str_val', 'string'), ('int_val', 'int'), ('bool_val', 'boolean'), ('datetime_val', 'Datetime')], default='str_val', max_length=12, verbose_name='Argument Type')),
('str_val', models.CharField(blank=True, max_length=255, verbose_name='String Value')),
('int_val', models.IntegerField(blank=True, null=True, verbose_name='Int Value')),
('bool_val', models.BooleanField(default=False, verbose_name='Boolean Value')),
('datetime_val', models.DateTimeField(blank=True, null=True, verbose_name='Datetime Value')),
('object_id', models.PositiveIntegerField()),
('key', models.CharField(max_length=255)),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
],
options={
'ordering': ['id'],
'abstract': False,
},
),
migrations.CreateModel(
name='JobArg',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('arg_type', models.CharField(choices=[('str_val', 'string'), ('int_val', 'int'), ('bool_val', 'boolean'), ('datetime_val', 'Datetime')], default='str_val', max_length=12, verbose_name='Argument Type')),
('str_val', models.CharField(blank=True, max_length=255, verbose_name='String Value')),
('int_val', models.IntegerField(blank=True, null=True, verbose_name='Int Value')),
('bool_val', models.BooleanField(default=False, verbose_name='Boolean Value')),
('datetime_val', models.DateTimeField(blank=True, null=True, verbose_name='Datetime Value')),
('object_id', models.PositiveIntegerField()),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
],
options={
'ordering': ['id'],
'abstract': False,
},
),
]
Loading