From patchwork Fri Jan 12 13:24:37 2018 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Veronika Kabatova X-Patchwork-Id: 859918 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Received: from lists.ozlabs.org (lists.ozlabs.org [IPv6:2401:3900:2:1::3]) (using TLSv1.2 with cipher ADH-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 3zJ3QR6nmfz9ryQ for ; Sat, 13 Jan 2018 00:25:19 +1100 (AEDT) Received: from lists.ozlabs.org (lists.ozlabs.org [IPv6:2401:3900:2:1::3]) by lists.ozlabs.org (Postfix) with ESMTP id 3zJ3QQ5Hb5zF0bq for ; Sat, 13 Jan 2018 00:25:18 +1100 (AEDT) X-Original-To: patchwork@lists.ozlabs.org Delivered-To: patchwork@lists.ozlabs.org Authentication-Results: ozlabs.org; spf=pass (mailfrom) smtp.mailfrom=redhat.com (client-ip=209.132.183.28; helo=mx1.redhat.com; envelope-from=vkabatov@redhat.com; receiver=) Received: from mx1.redhat.com (mx1.redhat.com [209.132.183.28]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by lists.ozlabs.org (Postfix) with ESMTPS id 3zJ3Q71pf0zF0Zb for ; Sat, 13 Jan 2018 00:25:03 +1100 (AEDT) Received: from smtp.corp.redhat.com (int-mx04.intmail.prod.int.phx2.redhat.com [10.5.11.14]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by mx1.redhat.com (Postfix) with ESMTPS id 47AD220273 for ; Fri, 12 Jan 2018 13:25:01 +0000 (UTC) Received: from vkabatova.usersys.redhat.com (unknown [10.40.205.147]) by smtp.corp.redhat.com (Postfix) with ESMTP id 1E7A85D9CC; Fri, 12 Jan 2018 13:24:59 +0000 (UTC) From: vkabatov@redhat.com To: patchwork@lists.ozlabs.org Subject: [PATCH] Avoid timezone confusion Date: Fri, 12 Jan 2018 14:24:37 +0100 Message-Id: <20180112132437.8805-1-vkabatov@redhat.com> X-Scanned-By: MIMEDefang 2.79 on 10.5.11.14 X-Greylist: Sender IP whitelisted, not delayed by milter-greylist-4.5.16 (mx1.redhat.com [10.5.110.29]); Fri, 12 Jan 2018 13:25:01 +0000 (UTC) X-BeenThere: patchwork@lists.ozlabs.org X-Mailman-Version: 2.1.24 Precedence: list List-Id: Patchwork development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , MIME-Version: 1.0 Errors-To: patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org Sender: "Patchwork" From: Veronika Kabatova Patchwork saves patches, comments etc with UTC timezone and reports this time when opening the patch details. However, internally generated processes such as events are reported with the instance's local time. There's nothing wrong with that and making PW timezone-aware would add useless complexity, but in a world-wide collaboration a lot of confusion may arise as the timezone is not reported at all. Instance's local time might be very different from the local time of CI integrating with PW, which is different from the local time of person dealing with it etc. For submission views, just add 'UTC' strings. For API responses, change the serializers to still use ISO 8601 format but with timezone information suffix [Z+-HHMM] - UTC for submissions and PW's local timezone for internal events. Signed-off-by: Veronika Kabatova --- docs/api/rest.rst | 12 ++++++++++-- docs/deployment/installation.rst | 3 ++- patchwork/api/check.py | 2 ++ patchwork/api/cover.py | 5 +++++ patchwork/api/embedded.py | 15 +++++++++++++++ patchwork/api/event.py | 2 ++ patchwork/api/patch.py | 5 +++++ patchwork/api/series.py | 6 ++++++ patchwork/templates/patchwork/submission.html | 4 ++-- patchwork/views/xmlrpc.py | 7 ++++--- releasenotes/notes/report-timezones-47e2095efcb37a84.yaml | 4 ++++ requirements-prod.txt | 1 + requirements-test.txt | 1 + 13 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/report-timezones-47e2095efcb37a84.yaml diff --git a/docs/api/rest.rst b/docs/api/rest.rst index 3d7292e..71a09cf 100644 --- a/docs/api/rest.rst +++ b/docs/api/rest.rst @@ -107,9 +107,17 @@ Schema ------ Responses are returned as JSON. Blank fields are returned as ``null``, rather -than being omitted. Timestamps use the ISO 8601 format:: +than being omitted. Returned timestamps use the ISO 8601 format including the +timezone information:: - YYYY-MM-DDTHH:MM:SSZ + YYYY-MM-DDTHH:MM:SS[Z+-HH:MM] + +.. note:: + + Timezone suffix is only provided in responses to avoid users' confusion and + to allow proper coordination with CI tooling. Underlying data are + timezone-naive and can't be filtered based on timezones, therefore requests + made to the API should not use the timezone suffix. Requests should use either query parameters or form-data, depending on the method. Further information is provided `below `__. diff --git a/docs/deployment/installation.rst b/docs/deployment/installation.rst index a570dd8..38e12d1 100644 --- a/docs/deployment/installation.rst +++ b/docs/deployment/installation.rst @@ -173,7 +173,8 @@ this also: .. code-block:: shell $ sudo apt-get install -y python3-django python3-psycopg2 \ - python3-djangorestframework python3-django-filters + python3-djangorestframework python3-django-filters \ + python3-tz .. tip:: diff --git a/patchwork/api/check.py b/patchwork/api/check.py index b37d6e0..4854a82 100644 --- a/patchwork/api/check.py +++ b/patchwork/api/check.py @@ -17,6 +17,7 @@ # along with Patchwork; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +from django.utils import timezone from rest_framework.exceptions import PermissionDenied from rest_framework.generics import ListCreateAPIView from rest_framework.generics import RetrieveAPIView @@ -63,6 +64,7 @@ class CheckSerializer(HyperlinkedModelSerializer): return super(CheckSerializer, self).run_validation(data) def to_representation(self, instance): + instance.date = timezone.make_aware(instance.date) data = super(CheckSerializer, self).to_representation(instance) data['state'] = instance.get_state_display() return data diff --git a/patchwork/api/cover.py b/patchwork/api/cover.py index 1064504..2de4ea2 100644 --- a/patchwork/api/cover.py +++ b/patchwork/api/cover.py @@ -19,6 +19,7 @@ import email.parser +from pytz import UTC from rest_framework.generics import ListAPIView from rest_framework.generics import RetrieveAPIView from rest_framework.serializers import HyperlinkedModelSerializer @@ -37,6 +38,10 @@ class CoverLetterListSerializer(HyperlinkedModelSerializer): submitter = PersonSerializer(read_only=True) mbox = SerializerMethodField() series = SeriesSerializer(many=True, read_only=True) + date = SerializerMethodField() + + def get_date(self, instance): + return instance.date.replace(tzinfo=UTC) def get_mbox(self, instance): request = self.context.get('request') diff --git a/patchwork/api/embedded.py b/patchwork/api/embedded.py index 7b5090a..bc915fe 100644 --- a/patchwork/api/embedded.py +++ b/patchwork/api/embedded.py @@ -23,6 +23,8 @@ A collection of serializers. None of the serializers here should reference nested fields. """ +from django.utils import timezone +from pytz import UTC from rest_framework.serializers import CharField from rest_framework.serializers import HyperlinkedModelSerializer from rest_framework.serializers import SerializerMethodField @@ -61,6 +63,7 @@ class CheckSerializer(HyperlinkedModelSerializer): url = CheckHyperlinkedIdentityField('api-check-detail') def to_representation(self, instance): + instance.date = timezone.make_aware(instance.date) data = super(CheckSerializer, self).to_representation(instance) data['state'] = instance.get_state_display() return data @@ -77,6 +80,10 @@ class CheckSerializer(HyperlinkedModelSerializer): class CoverLetterSerializer(MboxMixin, HyperlinkedModelSerializer): + def to_representation(self, instance): + instance.date = instance.date.replace(tzinfo=UTC) + return super(CoverLetterSerializer, self).to_representation(instance) + class Meta: model = models.CoverLetter fields = ('id', 'url', 'msgid', 'date', 'name', 'mbox') @@ -88,6 +95,10 @@ class CoverLetterSerializer(MboxMixin, HyperlinkedModelSerializer): class PatchSerializer(MboxMixin, HyperlinkedModelSerializer): + def to_representation(self, instance): + instance.date = instance.date.replace(tzinfo=UTC) + return super(PatchSerializer, self).to_representation(instance) + class Meta: model = models.Patch fields = ('id', 'url', 'msgid', 'date', 'name', 'mbox') @@ -126,6 +137,10 @@ class ProjectSerializer(HyperlinkedModelSerializer): class SeriesSerializer(MboxMixin, HyperlinkedModelSerializer): + def to_representation(self, instance): + instance.date = instance.date.replace(tzinfo=UTC) + return super(SeriesSerializer, self).to_representation(instance) + class Meta: model = models.Series fields = ('id', 'url', 'date', 'name', 'version', 'mbox') diff --git a/patchwork/api/event.py b/patchwork/api/event.py index 0d97af2..22c747e 100644 --- a/patchwork/api/event.py +++ b/patchwork/api/event.py @@ -19,6 +19,7 @@ from collections import OrderedDict +from django.utils import timezone from rest_framework.generics import ListAPIView from rest_framework.serializers import ModelSerializer from rest_framework.serializers import SerializerMethodField @@ -61,6 +62,7 @@ class EventSerializer(ModelSerializer): } def to_representation(self, instance): + instance.date = timezone.make_aware(instance.date) data = super(EventSerializer, self).to_representation(instance) payload = OrderedDict() kept_fields = self._category_map[instance.category] + [ diff --git a/patchwork/api/patch.py b/patchwork/api/patch.py index 115feff..57aa97d 100644 --- a/patchwork/api/patch.py +++ b/patchwork/api/patch.py @@ -20,6 +20,7 @@ import email.parser from django.utils.translation import ugettext_lazy as _ +from pytz import UTC from rest_framework.generics import ListAPIView from rest_framework.generics import RetrieveUpdateAPIView from rest_framework.relations import RelatedField @@ -86,6 +87,10 @@ class PatchListSerializer(HyperlinkedModelSerializer): check = SerializerMethodField() checks = SerializerMethodField() tags = SerializerMethodField() + date = SerializerMethodField() + + def get_date(self, instance): + return instance.date.replace(tzinfo=UTC) def get_mbox(self, instance): request = self.context.get('request') diff --git a/patchwork/api/series.py b/patchwork/api/series.py index b5f4450..562bf45 100644 --- a/patchwork/api/series.py +++ b/patchwork/api/series.py @@ -17,6 +17,8 @@ # along with Patchwork; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from pytz import UTC from rest_framework.generics import ListAPIView from rest_framework.generics import RetrieveAPIView from rest_framework.serializers import HyperlinkedModelSerializer @@ -38,6 +40,10 @@ class SeriesSerializer(HyperlinkedModelSerializer): mbox = SerializerMethodField() cover_letter = CoverLetterSerializer(read_only=True) patches = PatchSerializer(read_only=True, many=True) + date = SerializerMethodField() + + def get_date(self, instance): + return instance.date.replace(tzinfo=UTC) def get_mbox(self, instance): request = self.context.get('request') diff --git a/patchwork/templates/patchwork/submission.html b/patchwork/templates/patchwork/submission.html index 6ed20c3..e817713 100644 --- a/patchwork/templates/patchwork/submission.html +++ b/patchwork/templates/patchwork/submission.html @@ -255,7 +255,7 @@ function toggle_div(link_id, headers_id)
{{ submission.submitter|personify:project }} - {{ submission.date }} + {{ submission.date }} UTC
 {{ submission|commentsyntax }}
@@ -271,7 +271,7 @@ function toggle_div(link_id, headers_id)
 
{{ item.submitter|personify:project }} - {{ item.date }} | {{ item.date }} UTC | #{{ forloop.counter }}
diff --git a/patchwork/views/xmlrpc.py b/patchwork/views/xmlrpc.py
index 93f6035..d59779d 100644
--- a/patchwork/views/xmlrpc.py
+++ b/patchwork/views/xmlrpc.py
@@ -38,6 +38,7 @@ from django.views.decorators.csrf import csrf_exempt
 from django.utils import six
 from django.utils.six.moves import xmlrpc_client
 from django.utils.six.moves.xmlrpc_server import SimpleXMLRPCDispatcher
+from pytz import UTC
 
 from patchwork.compat import reverse
 from patchwork.models import Check
@@ -248,7 +249,7 @@ def patch_to_dict(obj):
 
     {
         'id': 1
-        'date': '2000-12-31 00:11:22',
+        'date': '2000-12-31 00:11:22+00:00',
         'filename': 'Fix-all-the-bugs.patch',
         'msgid': '',
         'name': "Fix all the bugs",
@@ -273,7 +274,7 @@ def patch_to_dict(obj):
     """
     return {
         'id': obj.id,
-        'date': six.text_type(obj.date).encode('utf-8'),
+        'date': six.text_type(obj.date.replace(tzinfo=UTC)).encode('utf-8'),
         'filename': obj.filename,
         'msgid': obj.msgid,
         'name': obj.name,
@@ -319,7 +320,7 @@ def check_to_dict(obj):
     object which is OK to send to the client."""
     return {
         'id': obj.id,
-        'date': six.text_type(obj.date).encode('utf-8'),
+        'date': six.text_type(obj.date.replace(tzinfo=UTC)).encode('utf-8'),
         'patch': six.text_type(obj.patch).encode('utf-8'),
         'patch_id': obj.patch_id,
         'user': six.text_type(obj.user).encode('utf-8'),
diff --git a/releasenotes/notes/report-timezones-47e2095efcb37a84.yaml b/releasenotes/notes/report-timezones-47e2095efcb37a84.yaml
new file mode 100644
index 0000000..ad1dae4
--- /dev/null
+++ b/releasenotes/notes/report-timezones-47e2095efcb37a84.yaml
@@ -0,0 +1,4 @@
+---
+other:
+  - |
+    Report timezones in web UI and APIs.
diff --git a/requirements-prod.txt b/requirements-prod.txt
index dd2040d..1e5207e 100644
--- a/requirements-prod.txt
+++ b/requirements-prod.txt
@@ -3,3 +3,4 @@ djangorestframework>=3.4,<3.7
 django-filter>=1.0,<1.1
 psycopg2>=2.7,<2.8
 sqlparse==0.2.3
+pytz>=2016.10
diff --git a/requirements-test.txt b/requirements-test.txt
index 141cf66..632eb4c 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -3,3 +3,4 @@ psycopg2>=2.7,<2.8
 django-debug-toolbar==1.8
 python-dateutil>2.0,<3.0
 selenium>=3.0,<4.0
+pytz>=2016.10