From 8a2e7b7df2185d9dd285ad0cfbb660863a2050bb Mon Sep 17 00:00:00 2001 From: Siburg Date: Sun, 13 Aug 2023 20:04:25 +1200 Subject: [PATCH 1/7] Ensure Django 4.x compatibility --- chroniker/__init__.py | 6 +++++ chroniker/admin.py | 28 +++++++++++++-------- chroniker/apps.py | 6 +++++ chroniker/constants.py | 6 ++++- chroniker/management/commands/cronserver.py | 5 +++- chroniker/models.py | 13 +++++++--- chroniker/tests/settings.py | 3 +++ chroniker/tests/urls.py | 15 ++++++----- chroniker/widgets.py | 12 ++++----- requirements-test.txt | 4 +++ 10 files changed, 67 insertions(+), 31 deletions(-) create mode 100644 chroniker/apps.py diff --git a/chroniker/__init__.py b/chroniker/__init__.py index 0ea551f..689cd53 100644 --- a/chroniker/__init__.py +++ b/chroniker/__init__.py @@ -1,2 +1,8 @@ +from django import VERSION as DJANGO_VERSION + + VERSION = (1, 0, 23) __version__ = '.'.join(map(str, VERSION)) + +if DJANGO_VERSION < (3, 2): + default_app_config = 'chroniker.apps.ChronikerConfig' diff --git a/chroniker/admin.py b/chroniker/admin.py index 55b6173..b7269b7 100644 --- a/chroniker/admin.py +++ b/chroniker/admin.py @@ -1,20 +1,26 @@ from django import forms from django.conf import settings -from django.conf.urls import url from django.contrib import admin from django.core.management import get_commands -from django.urls import reverse, NoReverseMatch from django.db import models from django.forms import TextInput from django.shortcuts import render -from django.utils.encoding import force_text +from django.urls import reverse, NoReverseMatch +try: + from django.urls import re_path +except ImportError: + from django.conf.urls import url as re_path +from django.utils.encoding import force_str from django.http import HttpResponseRedirect, Http404, HttpResponse from django.utils import dateformat, timezone from django.utils.datastructures import MultiValueDict from django.utils.formats import get_format from django.utils.html import format_html from django.utils.text import capfirst -from django.utils.translation import ugettext_lazy as _ +try: + from django.utils.translation import gettext_lazy as _ +except ImportError: + from django.utils.translation import ugettext_lazy as _ from chroniker.models import Job, Log, JobDependency, Monitor from chroniker import utils @@ -361,7 +367,7 @@ def view_duration_graph(self, request, object_id): media = self.media context = { - 'title': _('Change %s') % force_text(opts.verbose_name), + 'title': _('Change %s') % force_str(opts.verbose_name), 'object_id': object_id, 'original': obj, 'is_popup': False, @@ -378,9 +384,9 @@ def view_duration_graph(self, request, object_id): def get_urls(self): urls = super().get_urls() my_urls = [ - url(r'^(.+)/run/$', self.admin_site.admin_view(self.run_job_view), name="chroniker_job_run"), - url(r'^(.+)/stop/$', self.admin_site.admin_view(self.stop_job_view), name="chroniker_job_stop"), - url(r'^(.+)/graph/duration/$', self.admin_site.admin_view(self.view_duration_graph), name='chroniker_job_duration_graph'), + re_path(r'^(.+)/run/$', self.admin_site.admin_view(self.run_job_view), name="chroniker_job_run"), + re_path(r'^(.+)/stop/$', self.admin_site.admin_view(self.stop_job_view), name="chroniker_job_stop"), + re_path(r'^(.+)/graph/duration/$', self.admin_site.admin_view(self.view_duration_graph), name='chroniker_job_duration_graph'), ] return my_urls + urls @@ -553,8 +559,8 @@ def view_full_stderr(self, request, log_id): def get_urls(self): urls = super().get_urls() my_urls = [ - url(r'^(?P[0-9]+)/stdout/?$', self.admin_site.admin_view(self.view_full_stdout), name='chroniker_log_stdout'), - url(r'^(?P[0-9]+)/stderr/?$', self.admin_site.admin_view(self.view_full_stderr), name='chroniker_log_stderr'), + re_path(r'^(?P[0-9]+)/stdout/?$', self.admin_site.admin_view(self.view_full_stdout), name='chroniker_log_stdout'), + re_path(r'^(?P[0-9]+)/stderr/?$', self.admin_site.admin_view(self.view_full_stderr), name='chroniker_log_stderr'), ] return my_urls + urls @@ -684,7 +690,7 @@ def run_job_view(self, request, pk): def get_urls(self): urls = super().get_urls() my_urls = [ - url(r'^(.+)/run/$', self.admin_site.admin_view(self.run_job_view), name="chroniker_job_run"), + re_path(r'^(.+)/run/$', self.admin_site.admin_view(self.run_job_view), name="chroniker_job_run"), ] return my_urls + urls diff --git a/chroniker/apps.py b/chroniker/apps.py new file mode 100644 index 0000000..3488ddf --- /dev/null +++ b/chroniker/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ChronikerConfig(AppConfig): + name = 'chroniker' + default_auto_field = 'django.db.models.AutoField' diff --git a/chroniker/constants.py b/chroniker/constants.py index 06b9d11..21b93ab 100644 --- a/chroniker/constants.py +++ b/chroniker/constants.py @@ -1,4 +1,8 @@ -from django.utils.translation import ugettext_lazy as _ +try: + from django.utils.translation import gettext_lazy as _ +except ImportError: + from django.utils.translation import ugettext_lazy as _ + YEARLY = 'YEARLY' MONTHLY = 'MONTHLY' diff --git a/chroniker/management/commands/cronserver.py b/chroniker/management/commands/cronserver.py index af0b8d3..69e0fb9 100644 --- a/chroniker/management/commands/cronserver.py +++ b/chroniker/management/commands/cronserver.py @@ -6,7 +6,10 @@ from django.core.management.base import BaseCommand from django.core.management import call_command -from django.utils.translation import ugettext_lazy as _ +try: + from django.utils.translation import gettext_lazy as _ +except ImportError: + from django.utils.translation import ugettext_lazy as _ logger = logging.getLogger('chroniker.commands.cronserver') diff --git a/chroniker/models.py b/chroniker/models.py index 362f6be..6f5005e 100644 --- a/chroniker/models.py +++ b/chroniker/models.py @@ -36,7 +36,12 @@ from django.utils.encoding import smart_str from django.utils.safestring import mark_safe from django.utils.timesince import timeuntil -from django.utils.translation import ungettext, ugettext, ugettext_lazy as _ +try: + from django.utils.translation import ngettext, gettext, gettext_lazy as _ +except ImportError: + from django.utils.translation import ( + ungettext as ngettext, ugettext as gettext, ugettext_lazy as _ + ) from django.core.exceptions import ValidationError from django.utils.html import format_html from toposort import toposort_flatten @@ -756,7 +761,7 @@ def get_timeuntil(self): """ Returns a string representing the time until the next time this Job will be run (actually, the "string" returned - is really an instance of ``ugettext_lazy``). + is really an instance of ``gettext_lazy``). >>> job = Job(next_run=timezone.now()) >>> job.get_timeuntil().translate('en') @@ -776,8 +781,8 @@ def get_timeuntil(self): return _('due') if delta.seconds < 60: # Adapted from django.utils.timesince - count = lambda n: ungettext('second', 'seconds', n) - return ugettext('%(number)d %(type)s') % {'number': delta.seconds, 'type': count(delta.seconds)} + count = lambda n: ngettext('second', 'seconds', n) + return gettext('%(number)d %(type)s') % {'number': delta.seconds, 'type': count(delta.seconds)} return timeuntil(self.next_run) get_timeuntil.short_description = _('time until next run') diff --git a/chroniker/tests/settings.py b/chroniker/tests/settings.py index dc16f6b..ce17708 100644 --- a/chroniker/tests/settings.py +++ b/chroniker/tests/settings.py @@ -45,6 +45,9 @@ def __getitem__(self, item): TIME_ZONE = 'America/New_York' +# See https://docs.djangoproject.com/en/4.2/ref/settings/#use-deprecated-pytz +USE_DEPRECATED_PYTZ = True + AUTH_USER_MODEL = 'auth.User' SECRET_KEY = 'abc123' diff --git a/chroniker/tests/urls.py b/chroniker/tests/urls.py index 5705891..992a3f1 100644 --- a/chroniker/tests/urls.py +++ b/chroniker/tests/urls.py @@ -1,9 +1,3 @@ -try: - # Removed in Django 1.6 - from django.conf.urls.defaults import url, include -except ImportError: - from django.conf.urls import url, include - try: # Relocated in Django 1.6 from django.conf.urls.defaults import patterns @@ -16,17 +10,22 @@ from django.core.exceptions import ImproperlyConfigured from django.contrib import admin +try: + from django.urls import include, re_path +except ImportError: + from django.conf.urls import include, url as re_path + admin.autodiscover() try: _patterns = [ - url(r'^admin/', include(admin.site.urls)), + re_path(r'^admin/', include(admin.site.urls)), ] except ImproperlyConfigured: # Django >= 2.1.7. _patterns = [ - url(r'^admin/', admin.site.urls), + re_path(r'^admin/', admin.site.urls), ] if patterns is None: diff --git a/chroniker/widgets.py b/chroniker/widgets.py index 9bb9c5c..5a6292e 100644 --- a/chroniker/widgets.py +++ b/chroniker/widgets.py @@ -10,11 +10,11 @@ from django.urls import reverse try: # force_unicode was deprecated in Django 1.5. - from django.utils.encoding import force_unicode as force_text - from django.utils.encoding import smart_unicode as smart_text + from django.utils.encoding import force_unicode as force_str + from django.utils.encoding import smart_unicode as smart_str except ImportError: - from django.utils.encoding import force_text - from django.utils.encoding import smart_text + from django.utils.encoding import force_str + from django.utils.encoding import smart_str from django.utils.html import escape from django.utils.safestring import mark_safe @@ -58,7 +58,7 @@ def render(self, name, value, attrs=None, renderer=None): final_attrs = self.build_attrs(attrs, extra_attrs={'type': self.input_type, 'name': name}) if value != '': # Only add the 'value' attribute if a value is non-empty. - final_attrs['value'] = force_text(self._format_value(value)) + final_attrs['value'] = force_str(self._format_value(value)) final_attrs['size'] = 10 t = Template( str( @@ -112,7 +112,7 @@ def label_for_value(self, value): for v in values: try: obj = self.remote_field.model._default_manager.using(self.db).get(**{key: v}) - x = smart_text(obj) + x = smart_str(obj) change_url = reverse("admin:%s_%s_change" % (obj._meta.app_label, obj._meta.object_name.lower()), args=(obj.pk,)) str_values += ['%s' \ % (change_url, escape(x))] diff --git a/requirements-test.txt b/requirements-test.txt index 86a3009..813734d 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -6,3 +6,7 @@ mkdocs==1.4.2 yapf==0.29.0 twine==4.0.2 pre-commit==2.11.0 +# Support for using pytz will be removed in Django 5.0. +# Don't specify a version for it now. It will already be included in +# Django versions prior to 4.0. +pytz From 0f797b6de1fec3160c52550d14c346269b1bd207 Mon Sep 17 00:00:00 2001 From: Siburg Date: Sun, 13 Aug 2023 21:47:52 +1200 Subject: [PATCH 2/7] Include locally updated fields in custom save methods --- chroniker/models.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/chroniker/models.py b/chroniker/models.py index 6f5005e..3c38c2e 100644 --- a/chroniker/models.py +++ b/chroniker/models.py @@ -1,5 +1,6 @@ from __future__ import print_function +import itertools import logging import os import shlex @@ -697,11 +698,13 @@ def clean(self): errors['raw_command'] = errors['command'] raise ValidationError(errors) - def full_clean(self, exclude=None, validate_unique=True): + def full_clean(self, **kwargs): self.clean() def save(self, **kwargs): self.full_clean() + # The `clean` method can update `self.frequency` + save_fields = ['frequency'] tz = timezone.get_default_timezone() @@ -713,6 +716,7 @@ def save(self, **kwargs): if not self.next_run or j.params != self.params: logger.debug("Updating 'next_run") next_run = self.next_run or timezone.now() + save_fields += ['next_run'] try: self.next_run = self.rrule.after(utils.make_aware(next_run, tz)) except ValueError: @@ -723,9 +727,23 @@ def save(self, **kwargs): if not self.is_running: self.current_hostname = None self.current_pid = None + save_fields += ['current_hostname', 'current_pid'] if self.next_run: self.next_run = utils.make_aware(self.next_run, tz) + if 'next_run' not in save_fields: + save_fields += ['next_run'] + + update_fields = kwargs.get('update_fields') + if update_fields is not None: + extra_update_fields = [ + field for field in save_fields if field not in update_fields + ] + # update_fields can be a tuple, or list, or set + extended_update_fields = type(update_fields)( + itertools.chain(update_fields, extra_update_fields) + ) + kwargs['update_fields'] = extended_update_fields super().save(**kwargs) @@ -1237,6 +1255,15 @@ def save(self, **kwargs): assert self.run_start_datetime <= self.run_end_datetime, 'Job must start before it ends.' time_diff = (self.run_end_datetime - self.run_start_datetime) self.duration_seconds = time_diff.total_seconds() + + update_fields = kwargs.get('update_fields') + if update_fields is not None and 'duration_seconds' not in update_fields: + # update_fields can be a tuple, or list, or set + extended_update_fields = type(update_fields)( + itertools.chain(update_fields, ['duration_seconds']) + ) + kwargs['update_fields'] = extended_update_fields + super().save(**kwargs) def duration_str(self): From 969121036b68d9646a6c4a38c8c87724b003a188 Mon Sep 17 00:00:00 2001 From: Siburg Date: Sun, 13 Aug 2023 22:16:54 +1200 Subject: [PATCH 3/7] Replace admin_static template tag --- chroniker/templates/admin/chroniker/job/duration_graph.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chroniker/templates/admin/chroniker/job/duration_graph.html b/chroniker/templates/admin/chroniker/job/duration_graph.html index c9fee81..70935e8 100644 --- a/chroniker/templates/admin/chroniker/job/duration_graph.html +++ b/chroniker/templates/admin/chroniker/job/duration_graph.html @@ -1,6 +1,6 @@ {% extends 'admin/change_form.html' %} {#{% load url from future %}#} -{% load i18n admin_urls admin_static admin_modify %} +{% load i18n admin_urls static admin_modify %} {% block breadcrumbs %}