Skip to content

Commit

Permalink
Convert datetime.now() to django.utils.timezone.now(). Closes #545
Browse files Browse the repository at this point in the history
- datetime.now() isn't very safe for timezone enabled scenarios
  so make use of django.utils.timezone.now() which acts according
  to settings
- add new linter to prevent & discover usage of datetime.now()
- how to configure time zone has already been documented so close
  issue #545 with this commit
  • Loading branch information
atodorov committed Nov 25, 2019
1 parent 22ab2d4 commit 3acf988
Show file tree
Hide file tree
Showing 13 changed files with 56 additions and 26 deletions.
2 changes: 2 additions & 0 deletions kiwi_lint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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))
31 changes: 31 additions & 0 deletions kiwi_lint/datetime.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion tcms/core/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ def settings_processor(_request):


def server_time_processor(_request):
return {'SERVER_TIME': timezone.now() }
return {'SERVER_TIME': timezone.now()}
5 changes: 2 additions & 3 deletions tcms/core/helpers/comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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)

Expand Down
5 changes: 3 additions & 2 deletions tcms/kiwi_auth/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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])
Expand Down
5 changes: 2 additions & 3 deletions tcms/kiwi_auth/views.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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')))

Expand Down
3 changes: 2 additions & 1 deletion tcms/rpc/tests/test_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions tcms/rpc/tests/test_testexecution.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()})
5 changes: 2 additions & 3 deletions tcms/testplans/views.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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'],
Expand Down
4 changes: 2 additions & 2 deletions tcms/testruns/models.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# -*- coding: utf-8 -*-
import datetime
from collections import namedtuple

import vinaigrette
from django.conf import settings
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
Expand Down Expand Up @@ -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

Expand Down
7 changes: 3 additions & 4 deletions tcms/testruns/tests/test_update_caserun_status_view.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand All @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions tcms/testruns/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-

from datetime import datetime
from http import HTTPStatus

from django.conf import settings
Expand All @@ -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
Expand Down Expand Up @@ -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'})
5 changes: 2 additions & 3 deletions tcms/tests/factories.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 3acf988

Please sign in to comment.