diff --git a/django_celery_beat/apps.py b/django_celery_beat/apps.py index 0a783d21..65084ef2 100644 --- a/django_celery_beat/apps.py +++ b/django_celery_beat/apps.py @@ -12,3 +12,7 @@ class BeatConfig(AppConfig): label = 'django_celery_beat' verbose_name = _('Periodic Tasks') default_auto_field = 'django.db.models.AutoField' + + def ready(self): + from .signals import signals_connect + signals_connect() diff --git a/django_celery_beat/models.py b/django_celery_beat/models.py index 7cb8d0b4..7e01fbfe 100644 --- a/django_celery_beat/models.py +++ b/django_celery_beat/models.py @@ -11,7 +11,6 @@ from django.core.exceptions import MultipleObjectsReturned, ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models -from django.db.models import signals from django.utils.translation import gettext_lazy as _ from . import managers, validators @@ -586,6 +585,11 @@ def save(self, *args, **kwargs): self._clean_expires() self.validate_unique() super().save(*args, **kwargs) + PeriodicTasks.changed(self) + + def delete(self, *args, **kwargs): + super().delete(*args, **kwargs) + PeriodicTasks.changed(self) def _clean_expires(self): if self.expire_seconds is not None and self.expires: @@ -619,23 +623,3 @@ def schedule(self): return self.solar.schedule if self.clocked: return self.clocked.schedule - - -signals.pre_delete.connect(PeriodicTasks.changed, sender=PeriodicTask) -signals.pre_save.connect(PeriodicTasks.changed, sender=PeriodicTask) -signals.pre_delete.connect( - PeriodicTasks.update_changed, sender=IntervalSchedule) -signals.post_save.connect( - PeriodicTasks.update_changed, sender=IntervalSchedule) -signals.post_delete.connect( - PeriodicTasks.update_changed, sender=CrontabSchedule) -signals.post_save.connect( - PeriodicTasks.update_changed, sender=CrontabSchedule) -signals.post_delete.connect( - PeriodicTasks.update_changed, sender=SolarSchedule) -signals.post_save.connect( - PeriodicTasks.update_changed, sender=SolarSchedule) -signals.post_delete.connect( - PeriodicTasks.update_changed, sender=ClockedSchedule) -signals.post_save.connect( - PeriodicTasks.update_changed, sender=ClockedSchedule) diff --git a/django_celery_beat/signals.py b/django_celery_beat/signals.py new file mode 100644 index 00000000..9fd68a95 --- /dev/null +++ b/django_celery_beat/signals.py @@ -0,0 +1,48 @@ +def signals_connect(): + """ + Connect to signals. + """ + from django.db.models import signals + from .models import ( + ClockedSchedule, + PeriodicTask, + PeriodicTasks, + IntervalSchedule, + CrontabSchedule, + SolarSchedule + ) + + signals.pre_save.connect( + PeriodicTasks.changed, sender=PeriodicTask + ) + signals.pre_delete.connect( + PeriodicTasks.changed, sender=PeriodicTask + ) + + signals.post_save.connect( + PeriodicTasks.update_changed, sender=IntervalSchedule + ) + signals.pre_delete.connect( + PeriodicTasks.update_changed, sender=IntervalSchedule + ) + + signals.post_save.connect( + PeriodicTasks.update_changed, sender=CrontabSchedule + ) + signals.post_delete.connect( + PeriodicTasks.update_changed, sender=CrontabSchedule + ) + + signals.post_save.connect( + PeriodicTasks.update_changed, sender=SolarSchedule + ) + signals.post_delete.connect( + PeriodicTasks.update_changed, sender=SolarSchedule + ) + + signals.post_save.connect( + PeriodicTasks.update_changed, sender=ClockedSchedule + ) + signals.post_delete.connect( + PeriodicTasks.update_changed, sender=ClockedSchedule + ) diff --git a/t/proj/migrations/0001_initial.py b/t/proj/migrations/0001_initial.py new file mode 100644 index 00000000..d157fe81 --- /dev/null +++ b/t/proj/migrations/0001_initial.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.7 on 2022-10-13 05:09 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('django_celery_beat', '0016_alter_crontabschedule_timezone'), + ] + + operations = [ + migrations.CreateModel( + name='O2OToPeriodicTasks', + fields=[ + ('periodictask_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='django_celery_beat.periodictask')), + ], + bases=('django_celery_beat.periodictask',), + ), + ] diff --git a/t/proj/migrations/__init__.py b/t/proj/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/t/proj/models.py b/t/proj/models.py new file mode 100644 index 00000000..597c46eb --- /dev/null +++ b/t/proj/models.py @@ -0,0 +1,8 @@ +from django_celery_beat.models import PeriodicTask + + +class O2OToPeriodicTasks(PeriodicTask): + """ + The test-case model of OneToOne relation. + """ + pass diff --git a/t/proj/settings.py b/t/proj/settings.py index a09c4e33..abf8f4f4 100644 --- a/t/proj/settings.py +++ b/t/proj/settings.py @@ -48,6 +48,7 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'django_celery_beat', + 't.proj', ] MIDDLEWARE = [ @@ -79,7 +80,6 @@ WSGI_APPLICATION = 't.proj.wsgi.application' - # Database # https://docs.djangoproject.com/en/1.9/ref/settings/#databases @@ -93,7 +93,6 @@ } } - # Password validation # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators @@ -102,7 +101,6 @@ AUTH_PASSWORD_VALIDATORS = [ ] - # Internationalization # https://docs.djangoproject.com/en/1.9/topics/i18n/ @@ -116,7 +114,6 @@ USE_TZ = True - # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.9/howto/static-files/ diff --git a/t/unit/test_models.py b/t/unit/test_models.py index fd592ade..e975f9bc 100644 --- a/t/unit/test_models.py +++ b/t/unit/test_models.py @@ -1,10 +1,13 @@ import datetime import os + try: from zoneinfo import available_timezones, ZoneInfo except ImportError: from backports.zoneinfo import available_timezones, ZoneInfo +import pytest + from celery import schedules from django.test import TestCase, override_settings from django.apps import apps @@ -22,7 +25,10 @@ CrontabSchedule, ClockedSchedule, IntervalSchedule, + PeriodicTasks, + DAYS, ) +from t.proj.models import O2OToPeriodicTasks class MigrationTests(TestCase): @@ -159,3 +165,51 @@ def test_timezone_format(self): clocked_time=tz_info) # testnig str(schedule) calls make_aware() internally assert str(schedule.clocked_time) == str(schedule) + + +@pytest.mark.django_db() +class OneToOneRelTestCase(TestCase): + """ + Make sure that when OneToOne relation Model changed, + the `PeriodicTasks.last_update` will be update. + """ + + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.interval_schedule = IntervalSchedule.objects.create( + every=10, period=DAYS + ) + + def test_trigger_update_when_saved(self): + o2o_to_periodic_tasks = O2OToPeriodicTasks.objects.create( + name='name1', + task='task1', + enabled=True, + interval=self.interval_schedule + ) + not_changed_dt = PeriodicTasks.last_change() + o2o_to_periodic_tasks.enabled = True # Change something on instance. + o2o_to_periodic_tasks.save() + has_changed_dt = PeriodicTasks.last_change() + self.assertTrue( + not_changed_dt != has_changed_dt, + 'The `PeriodicTasks.last_update` has not be update.' + ) + # Check the `PeriodicTasks` does be updated. + + def test_trigger_update_when_deleted(self): + o2o_to_periodic_tasks = O2OToPeriodicTasks.objects.create( + name='name1', + task='task1', + enabled=True, + interval=self.interval_schedule + ) + not_changed_dt = PeriodicTasks.last_change() + o2o_to_periodic_tasks.delete() + has_changed_dt = PeriodicTasks.last_change() + self.assertTrue( + not_changed_dt != has_changed_dt, + 'The `PeriodicTasks.last_update` has not be update.' + ) + # Check the `PeriodicTasks` does be updated.