diff --git a/kiwi_lint/__init__.py b/kiwi_lint/__init__.py index 14f6527667..0be1f11a8a 100644 --- a/kiwi_lint/__init__.py +++ b/kiwi_lint/__init__.py @@ -19,6 +19,7 @@ from .auto_field import AutoFieldChecker from .one_to_one_field import OneToOneFieldChecker from .views import ClassBasedViewChecker +from .datetime import DatetimeChecker def register(linter): @@ -38,3 +39,4 @@ def register(linter): linter.register_checker(AutoFieldChecker(linter)) linter.register_checker(OneToOneFieldChecker(linter)) linter.register_checker(ClassBasedViewChecker(linter)) + linter.register_checker(DatetimeChecker(linter)) diff --git a/kiwi_lint/datetime.py b/kiwi_lint/datetime.py new file mode 100644 index 0000000000..2124346b9f --- /dev/null +++ b/kiwi_lint/datetime.py @@ -0,0 +1,31 @@ +""" + Warns against usage of datetime.now() which may not be fully timezone + safe. Instead we should use django.utils.timezone.now() which acts + according to settings! +""" + +from astroid import nodes + +from pylint import checkers +from pylint import interfaces + + +class DatetimeChecker(checkers.BaseChecker): + __implements__ = (interfaces.IAstroidChecker, ) + + name = 'datetime-checker' + + msgs = { + 'R4711': ('Do not use datetime.now(), use django.utils.timezone.now()', + 'datetime-now-used', + 'Use django.utils.timezone.now()! See: ' + 'https://docs.djangoproject.com/en/2.2/topics/i18n/timezones/' + '#interpretation-of-naive-datetime-objects') + } + + def visit_name(self, node): + parent = node.parent + while node.name == 'datetime' and isinstance(parent, nodes.Attribute): + if parent.attrname == 'now': + self.add_message('datetime-now-used', node=node) + parent = parent.parent diff --git a/tcms/core/context_processors.py b/tcms/core/context_processors.py index 4e986b4cd9..da98d6171f 100644 --- a/tcms/core/context_processors.py +++ b/tcms/core/context_processors.py @@ -18,4 +18,4 @@ def settings_processor(_request): def server_time_processor(_request): - return {'SERVER_TIME': timezone.now() } + return {'SERVER_TIME': timezone.now()} diff --git a/tcms/core/helpers/comments.py b/tcms/core/helpers/comments.py index 44a1b91909..83c90c8d36 100644 --- a/tcms/core/helpers/comments.py +++ b/tcms/core/helpers/comments.py @@ -3,11 +3,10 @@ Functions that help access comments of objects. """ -from datetime import datetime - from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.contrib.sites.models import Site +from django.utils import timezone from django_comments.models import Comment @@ -41,7 +40,7 @@ def add_comment(objs, comments, user, submit_date=None): object_pk=obj.pk, user=user, comment=comments, - submit_date=submit_date or datetime.now(), + submit_date=submit_date or timezone.now(), user_email=user.email, user_name=user.username) diff --git a/tcms/kiwi_auth/tests.py b/tcms/kiwi_auth/tests.py index 5e0e0e051f..9586cbfd7a 100644 --- a/tcms/kiwi_auth/tests.py +++ b/tcms/kiwi_auth/tests.py @@ -8,6 +8,7 @@ from django.contrib.sites.models import Site from django.test import TestCase, override_settings from django.urls import reverse +from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from mock import patch @@ -28,7 +29,7 @@ def setUpTestData(cls): @patch('tcms.kiwi_auth.models.datetime') def test_set_random_key(self, mock_datetime): - now = datetime.datetime.now() + now = timezone.now() in_7_days = datetime.timedelta(7) mock_datetime.datetime.today.return_value = now @@ -229,7 +230,7 @@ def test_fail_if_activation_key_expired(self): with patch('tcms.kiwi_auth.models.secrets') as _secrets: _secrets.token_hex.return_value = fake_activation_key key = UserActivationKey.set_random_key_for_user(self.new_user) - key.key_expires = datetime.datetime.now() - datetime.timedelta(days=10) + key.key_expires = timezone.now() - datetime.timedelta(days=10) key.save() confirm_url = reverse('tcms-confirm', args=[fake_activation_key]) diff --git a/tcms/kiwi_auth/views.py b/tcms/kiwi_auth/views.py index a89be491dd..be9efcfd26 100644 --- a/tcms/kiwi_auth/views.py +++ b/tcms/kiwi_auth/views.py @@ -1,14 +1,13 @@ # -*- coding: utf-8 -*- # pylint: disable=missing-permission-required -from datetime import datetime - from django.conf import settings from django.contrib import messages from django.contrib.auth import get_user_model, views from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, render from django.urls import reverse +from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from django.views.decorators.http import require_GET, require_http_methods @@ -105,7 +104,7 @@ def confirm(request, activation_key): ) return HttpResponseRedirect(request.GET.get('next', reverse('core-views-index'))) - if _activation_key.key_expires <= datetime.now(): + if _activation_key.key_expires <= timezone.now(): messages.add_message(request, messages.ERROR, _('This activation key has expired')) return HttpResponseRedirect(request.GET.get('next', reverse('core-views-index'))) diff --git a/tcms/rpc/tests/test_serializer.py b/tcms/rpc/tests/test_serializer.py index 0568f14fbc..07b169564e 100644 --- a/tcms/rpc/tests/test_serializer.py +++ b/tcms/rpc/tests/test_serializer.py @@ -4,6 +4,7 @@ import unittest from django import test +from django.utils import timezone from tcms.management.models import Product from tcms.rpc.serializer import (QuerySetBasedXMLRPCSerializer, Serializer, @@ -51,7 +52,7 @@ def test_datetime_to_str(self): self.assertEqual(value, None) from datetime import datetime - now = datetime.now() + now = timezone.now() value = datetime_to_str(now) expected_value = datetime.strftime(now, '%Y-%m-%d %H:%M:%S') self.assertEqual(expected_value, value) diff --git a/tcms/rpc/tests/test_testexecution.py b/tcms/rpc/tests/test_testexecution.py index 76835e95d5..eb2ddd9b6e 100644 --- a/tcms/rpc/tests/test_testexecution.py +++ b/tcms/rpc/tests/test_testexecution.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- # pylint: disable=invalid-name, attribute-defined-outside-init, objects-update-used -from datetime import datetime from xmlrpc.client import Fault as XmlRPCFault from xmlrpc.client import ProtocolError from django.test import override_settings +from django.utils import timezone from tcms.core.contrib.linkreference.models import LinkReference from tcms.core.helpers.comments import get_comments @@ -310,4 +310,4 @@ def test_update_with_no_perm(self): self.rpc_client.exec.Auth.logout() with self.assertRaisesRegex(ProtocolError, '403 Forbidden'): self.rpc_client.exec.TestExecution.update(self.case_run_1.pk, - {"close_date": datetime.now()}) + {"close_date": timezone.now()}) diff --git a/tcms/testplans/views.py b/tcms/testplans/views.py index 2ccc3fdbcd..f9518407b4 100644 --- a/tcms/testplans/views.py +++ b/tcms/testplans/views.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -import datetime - from django.contrib import messages from django.contrib.auth.decorators import permission_required from django.core.exceptions import ObjectDoesNotExist @@ -10,6 +8,7 @@ HttpResponseRedirect, JsonResponse) from django.shortcuts import get_object_or_404, render from django.urls import reverse +from django.utils import timezone from django.utils.decorators import method_decorator from django.utils.translation import ugettext_lazy as _ from django.views.decorators.http import (require_GET, require_http_methods, @@ -69,7 +68,7 @@ def post(self, request): product_version=form.cleaned_data['product_version'], type=form.cleaned_data['type'], name=form.cleaned_data['name'], - create_date=datetime.datetime.now(), + create_date=timezone.now(), extra_link=form.cleaned_data['extra_link'], parent=form.cleaned_data['parent'], text=form.cleaned_data['text'], diff --git a/tcms/testruns/models.py b/tcms/testruns/models.py index 1dfe61e8b5..31b22071e5 100755 --- a/tcms/testruns/models.py +++ b/tcms/testruns/models.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -import datetime from collections import namedtuple import vinaigrette @@ -7,6 +6,7 @@ from django.db import models from django.db.models import Count from django.urls import reverse +from django.utils import timezone from django.utils.translation import override from tcms.core.contrib.linkreference.models import LinkReference @@ -174,7 +174,7 @@ def _get_total_case_run_num(self): def update_completion_status(self, is_finished): if is_finished: - self.stop_date = datetime.datetime.now() + self.stop_date = timezone.now() else: self.stop_date = None diff --git a/tcms/testruns/tests/test_update_caserun_status_view.py b/tcms/testruns/tests/test_update_caserun_status_view.py index 5a9586cb14..0a4cc517f1 100644 --- a/tcms/testruns/tests/test_update_caserun_status_view.py +++ b/tcms/testruns/tests/test_update_caserun_status_view.py @@ -1,10 +1,9 @@ # -*- coding: utf-8 -*- # pylint: disable=too-many-ancestors -from datetime import datetime - from django.conf import settings from django.urls import reverse +from django.utils import timezone from django.utils.translation import override from tcms.testruns.models import TestExecutionStatus @@ -20,7 +19,7 @@ def setUpTestData(cls): cls.url = reverse('testruns-update_caserun_status') def test_update_status_positive_scenario(self): - before_update = datetime.now() + before_update = timezone.now() status_passed = TestExecutionStatus.objects.get(name=TestExecutionStatus.PASSED) post_data = { 'status_id': status_passed.pk, @@ -38,7 +37,7 @@ def test_update_status_positive_scenario(self): self.assertEqual(caserun.status_id, status_passed.pk) self.assertEqual(caserun.tested_by, self.tester) self.assertGreater(caserun.close_date, before_update) - self.assertLess(caserun.close_date, datetime.now()) + self.assertLess(caserun.close_date, timezone.now()) # verify we didn't update the last TCR by mistake self.execution_3.refresh_from_db() diff --git a/tcms/testruns/views.py b/tcms/testruns/views.py index 8eddb6d4ff..0ea9f929b4 100755 --- a/tcms/testruns/views.py +++ b/tcms/testruns/views.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -from datetime import datetime from http import HTTPStatus from django.conf import settings @@ -13,6 +12,7 @@ from django.http import Http404, HttpResponseRedirect, JsonResponse from django.shortcuts import get_object_or_404, render from django.urls import reverse +from django.utils import timezone from django.utils.decorators import method_decorator from django.utils.translation import ugettext_lazy as _ from django.views.generic.base import TemplateView, View @@ -628,7 +628,7 @@ def post(self, request): execution = get_object_or_404(TestExecution, pk=int(caserun_pk)) execution.status_id = status_id execution.tested_by = request.user - execution.close_date = datetime.now() + execution.close_date = timezone.now() execution.save() return JsonResponse({'rc': 0, 'response': 'ok'}) diff --git a/tcms/tests/factories.py b/tcms/tests/factories.py index 5966fd4725..9a952220d8 100644 --- a/tcms/tests/factories.py +++ b/tcms/tests/factories.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- # pylint: disable=too-few-public-methods, unused-argument -from datetime import datetime - import factory from django.conf import settings from django.db.models import signals +from django.utils import timezone from factory.django import DjangoModelFactory from tcms.management.models import Priority @@ -122,7 +121,7 @@ class Meta: name = factory.Sequence(lambda n: 'Plan name %d' % n) text = factory.Sequence(lambda n: 'Plan document %d' % n) - create_date = factory.LazyFunction(datetime.now) + create_date = factory.LazyFunction(timezone.now) product_version = factory.SubFactory(VersionFactory) author = factory.SubFactory(UserFactory) product = factory.SubFactory(ProductFactory)