This is an automated email from the ASF dual-hosted git repository.

villebro pushed a commit to branch 0.37
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git

commit c57df314d7bab3720f4378e2909b2604f2151a3b
Author: Daniel Vaz Gaspar <danielvazgas...@gmail.com>
AuthorDate: Wed Jul 15 19:09:32 2020 +0100

    fix: humanised changed on UTC on dashboards and charts (#10321)
    
    * fix: API marshmallow3 drop utc for naive datetime fields
    
    * fix: API marshmallow3 drop utc for naive datetime fields
    
    * fix, tests
    
    * isort and test
    
    * black
    
    * add and fix test
    
    * fix comment
---
 .../javascripts/views/chartList/ChartList_spec.jsx |  2 +-
 .../views/dashboardList/DashboardList_spec.jsx     |  5 +--
 .../src/views/chartList/ChartList.tsx              |  9 +++--
 .../src/views/dashboardList/DashboardList.tsx      | 11 +++---
 superset-frontend/src/welcome/DashboardTable.tsx   | 11 +++---
 superset/charts/api.py                             |  8 ++---
 superset/dashboards/api.py                         | 12 +++++--
 superset/models/helpers.py                         | 10 ++++++
 tests/charts/api_tests.py                          | 30 ++++++++++++++++
 tests/dashboards/api_tests.py                      | 41 +++++++++++++++++++---
 10 files changed, 109 insertions(+), 30 deletions(-)

diff --git 
a/superset-frontend/spec/javascripts/views/chartList/ChartList_spec.jsx 
b/superset-frontend/spec/javascripts/views/chartList/ChartList_spec.jsx
index a385faf..695b74e 100644
--- a/superset-frontend/spec/javascripts/views/chartList/ChartList_spec.jsx
+++ b/superset-frontend/spec/javascripts/views/chartList/ChartList_spec.jsx
@@ -105,7 +105,7 @@ describe('ChartList', () => {
     const callsD = fetchMock.calls(/chart\/\?q/);
     expect(callsD).toHaveLength(1);
     expect(callsD[0][0]).toMatchInlineSnapshot(
-      
`"http://localhost/api/v1/chart/?q=(order_column:changed_on,order_direction:desc,page:0,page_size:25)"`,
+      
`"http://localhost/api/v1/chart/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25)"`,
     );
   });
 });
diff --git 
a/superset-frontend/spec/javascripts/views/dashboardList/DashboardList_spec.jsx 
b/superset-frontend/spec/javascripts/views/dashboardList/DashboardList_spec.jsx
index ca35c5d..5de6ba3 100644
--- 
a/superset-frontend/spec/javascripts/views/dashboardList/DashboardList_spec.jsx
+++ 
b/superset-frontend/spec/javascripts/views/dashboardList/DashboardList_spec.jsx
@@ -43,7 +43,8 @@ const mockDashboards = [...new Array(3)].map((_, i) => ({
   changed_by_url: 'changed_by_url',
   changed_by_fk: 1,
   published: true,
-  changed_on: new Date().toISOString(),
+  changed_on_utc: new Date().toISOString(),
+  changed_on_delta_humanized: '5 minutes ago',
   owners: [{ first_name: 'admin', last_name: 'admin_user' }],
 }));
 
@@ -95,7 +96,7 @@ describe('DashboardList', () => {
     const callsD = fetchMock.calls(/dashboard\/\?q/);
     expect(callsD).toHaveLength(1);
     expect(callsD[0][0]).toMatchInlineSnapshot(
-      
`"http://localhost/api/v1/dashboard/?q=(order_column:changed_on,order_direction:desc,page:0,page_size:25)"`,
+      
`"http://localhost/api/v1/dashboard/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25)"`,
     );
   });
   it('edits', () => {
diff --git a/superset-frontend/src/views/chartList/ChartList.tsx 
b/superset-frontend/src/views/chartList/ChartList.tsx
index 57cfd82..fc5f992 100644
--- a/superset-frontend/src/views/chartList/ChartList.tsx
+++ b/superset-frontend/src/views/chartList/ChartList.tsx
@@ -19,7 +19,6 @@
 import { SupersetClient } from '@superset-ui/connection';
 import { t } from '@superset-ui/translation';
 import { getChartMetadataRegistry } from '@superset-ui/chart';
-import moment from 'moment';
 import PropTypes from 'prop-types';
 import React from 'react';
 import rison from 'rison';
@@ -108,7 +107,7 @@ class ChartList extends React.PureComponent<Props, State> {
     return isFeatureEnabled(FeatureFlag.LIST_VIEWS_SIP34_FILTER_UI);
   }
 
-  initialSort = [{ id: 'changed_on', desc: true }];
+  initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
 
   columns = [
     {
@@ -153,11 +152,11 @@ class ChartList extends React.PureComponent<Props, State> 
{
     {
       Cell: ({
         row: {
-          original: { changed_on: changedOn },
+          original: { changed_on_delta_humanized: changedOn },
         },
-      }: any) => <span 
className="no-wrap">{moment(changedOn).fromNow()}</span>,
+      }: any) => <span className="no-wrap">{changedOn}</span>,
       Header: t('Last Modified'),
-      accessor: 'changed_on',
+      accessor: 'changed_on_delta_humanized',
     },
     {
       accessor: 'description',
diff --git a/superset-frontend/src/views/dashboardList/DashboardList.tsx 
b/superset-frontend/src/views/dashboardList/DashboardList.tsx
index 438c964..95de66a 100644
--- a/superset-frontend/src/views/dashboardList/DashboardList.tsx
+++ b/superset-frontend/src/views/dashboardList/DashboardList.tsx
@@ -18,7 +18,6 @@
  */
 import { SupersetClient } from '@superset-ui/connection';
 import { t } from '@superset-ui/translation';
-import moment from 'moment';
 import PropTypes from 'prop-types';
 import React from 'react';
 import rison from 'rison';
@@ -60,7 +59,7 @@ interface Dashboard {
   changed_by: string;
   changed_by_name: string;
   changed_by_url: string;
-  changed_on: string;
+  changed_on_delta_humanized: string;
   dashboard_title: string;
   published: boolean;
   url: string;
@@ -123,7 +122,7 @@ class DashboardList extends React.PureComponent<Props, 
State> {
     return isFeatureEnabled(FeatureFlag.LIST_VIEWS_SIP34_FILTER_UI);
   }
 
-  initialSort = [{ id: 'changed_on', desc: true }];
+  initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
 
   columns = [
     {
@@ -181,11 +180,11 @@ class DashboardList extends React.PureComponent<Props, 
State> {
     {
       Cell: ({
         row: {
-          original: { changed_on: changedOn },
+          original: { changed_on_delta_humanized: changedOn },
         },
-      }: any) => <span 
className="no-wrap">{moment(changedOn).fromNow()}</span>,
+      }: any) => <span className="no-wrap">{changedOn}</span>,
       Header: t('Modified'),
-      accessor: 'changed_on',
+      accessor: 'changed_on_delta_humanized',
     },
     {
       accessor: 'slug',
diff --git a/superset-frontend/src/welcome/DashboardTable.tsx 
b/superset-frontend/src/welcome/DashboardTable.tsx
index 704cdc3..430548a 100644
--- a/superset-frontend/src/welcome/DashboardTable.tsx
+++ b/superset-frontend/src/welcome/DashboardTable.tsx
@@ -19,7 +19,6 @@
 import React from 'react';
 import { t } from '@superset-ui/translation';
 import { SupersetClient } from '@superset-ui/connection';
-import moment from 'moment';
 import { debounce } from 'lodash';
 import ListView from 'src/components/ListView/ListView';
 import withToasts from 'src/messageToasts/enhancers/withToasts';
@@ -94,23 +93,23 @@ class DashboardTable extends React.PureComponent<
       }) => <a href={changedByUrl}>{changedByName}</a>,
     },
     {
-      accessor: 'changed_on',
+      accessor: 'changed_on_delta_humanized',
       Header: 'Modified',
       Cell: ({
         row: {
-          original: { changed_on: changedOn },
+          original: { changed_on_delta_humanized: changedOn },
         },
       }: {
         row: {
           original: {
-            changed_on: string;
+            changed_on_delta_humanized: string;
           };
         };
-      }) => <span className="no-wrap">{moment(changedOn).fromNow()}</span>,
+      }) => <span className="no-wrap">{changedOn}</span>,
     },
   ];
 
-  initialSort = [{ id: 'changed_on', desc: true }];
+  initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
 
   fetchData = ({ pageIndex, pageSize, sortBy, filters }: FetchDataConfig) => {
     this.setState({ loading: true });
diff --git a/superset/charts/api.py b/superset/charts/api.py
index 1afc030..da4f0ff 100644
--- a/superset/charts/api.py
+++ b/superset/charts/api.py
@@ -101,7 +101,6 @@ class ChartRestApi(BaseSupersetModelRestApi):
         "cache_timeout",
     ]
     show_select_columns = show_columns + ["table.id"]
-
     list_columns = [
         "id",
         "slice_name",
@@ -113,7 +112,8 @@ class ChartRestApi(BaseSupersetModelRestApi):
         "changed_by_url",
         "changed_by.first_name",
         "changed_by.last_name",
-        "changed_on",
+        "changed_on_utc",
+        "changed_on_delta_humanized",
         "datasource_id",
         "datasource_type",
         "datasource_name_text",
@@ -124,13 +124,13 @@ class ChartRestApi(BaseSupersetModelRestApi):
         "params",
         "cache_timeout",
     ]
-
+    list_select_columns = list_columns + ["changed_on"]
     order_columns = [
         "slice_name",
         "viz_type",
         "datasource_name",
         "changed_by_fk",
-        "changed_on",
+        "changed_on_delta_humanized",
     ]
     search_columns = (
         "slice_name",
diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py
index 2e94e28..3310998 100644
--- a/superset/dashboards/api.py
+++ b/superset/dashboards/api.py
@@ -96,7 +96,6 @@ class DashboardRestApi(BaseSupersetModelRestApi):
         "table_names",
         "thumbnail_url",
     ]
-    order_columns = ["dashboard_title", "changed_on", "published", 
"changed_by_fk"]
     list_columns = [
         "id",
         "published",
@@ -112,13 +111,22 @@ class DashboardRestApi(BaseSupersetModelRestApi):
         "changed_by.id",
         "changed_by_name",
         "changed_by_url",
-        "changed_on",
+        "changed_on_utc",
+        "changed_on_delta_humanized",
         "dashboard_title",
         "owners.id",
         "owners.username",
         "owners.first_name",
         "owners.last_name",
     ]
+    list_select_columns = list_columns + ["changed_on"]
+    order_columns = [
+        "dashboard_title",
+        "changed_on_delta_humanized",
+        "published",
+        "changed_by_fk",
+    ]
+
     add_columns = [
         "dashboard_title",
         "slug",
diff --git a/superset/models/helpers.py b/superset/models/helpers.py
index d003f55..969669c 100644
--- a/superset/models/helpers.py
+++ b/superset/models/helpers.py
@@ -25,6 +25,7 @@ from typing import Any, Dict, List, Optional, Set, Union
 # pylint: disable=ungrouped-imports
 import humanize
 import pandas as pd
+import pytz
 import sqlalchemy as sa
 import yaml
 from flask import escape, g, Markup
@@ -381,6 +382,15 @@ class AuditMixinNullable(AuditMixin):
     def changed_on_(self) -> Markup:
         return Markup(f'<span class="no-wrap">{self.changed_on}</span>')
 
+    @renders("changed_on")
+    def changed_on_delta_humanized(self) -> str:
+        return self.changed_on_humanized
+
+    @renders("changed_on")
+    def changed_on_utc(self) -> str:
+        # Convert naive datetime to UTC
+        return 
self.changed_on.astimezone(pytz.utc).strftime("%Y-%m-%dT%H:%M:%S.%f%z")
+
     @property
     def changed_on_humanized(self) -> str:
         return humanize.naturaltime(datetime.now() - self.changed_on)
diff --git a/tests/charts/api_tests.py b/tests/charts/api_tests.py
index 909b8b1..c115b1d 100644
--- a/tests/charts/api_tests.py
+++ b/tests/charts/api_tests.py
@@ -18,9 +18,11 @@
 """Unit tests for Superset"""
 import json
 from typing import List, Optional
+from datetime import datetime
 from unittest import mock
 
 import prison
+import humanize
 from sqlalchemy.sql import func
 
 from tests.test_app import app
@@ -543,6 +545,34 @@ class TestChartApi(SupersetTestCase, 
ApiOwnersTestCaseMixin):
         data = json.loads(rv.data.decode("utf-8"))
         self.assertEqual(data["count"], 33)
 
+    def test_get_charts_changed_on(self):
+        """
+        Dashboard API: Test get charts changed on
+        """
+        admin = self.get_user("admin")
+        start_changed_on = datetime.now()
+        chart = self.insert_chart("foo_a", [admin.id], 1, description="ZY_bar")
+
+        self.login(username="admin")
+
+        arguments = {
+            "order_column": "changed_on_delta_humanized",
+            "order_direction": "desc",
+        }
+        uri = f"api/v1/chart/?q={prison.dumps(arguments)}"
+
+        rv = self.get_assert_metric(uri, "get_list")
+        self.assertEqual(rv.status_code, 200)
+        data = json.loads(rv.data.decode("utf-8"))
+        self.assertEqual(
+            data["result"][0]["changed_on_delta_humanized"],
+            humanize.naturaltime(datetime.now() - start_changed_on),
+        )
+
+        # rollback changes
+        db.session.delete(chart)
+        db.session.commit()
+
     def test_get_charts_filter(self):
         """
         Chart API: Test get charts filter
diff --git a/tests/dashboards/api_tests.py b/tests/dashboards/api_tests.py
index 5637e06..6645e80 100644
--- a/tests/dashboards/api_tests.py
+++ b/tests/dashboards/api_tests.py
@@ -18,8 +18,10 @@
 """Unit tests for Superset"""
 import json
 from typing import List, Optional
+from datetime import datetime
 
 import prison
+import humanize
 from sqlalchemy.sql import func
 
 import tests.test_app
@@ -111,7 +113,7 @@ class TestDashboardApi(SupersetTestCase, 
ApiOwnersTestCaseMixin):
         data = json.loads(rv.data.decode("utf-8"))
         self.assertIn("changed_on", data["result"])
         for key, value in data["result"].items():
-            # We can't assert timestamp
+            # We can't assert timestamp values
             if key != "changed_on":
                 self.assertEqual(value, expected_result[key])
         # rollback changes
@@ -152,6 +154,37 @@ class TestDashboardApi(SupersetTestCase, 
ApiOwnersTestCaseMixin):
         db.session.delete(dashboard)
         db.session.commit()
 
+    def test_get_dashboards_changed_on(self):
+        """
+        Dashboard API: Test get dashboards changed on
+        """
+        from datetime import datetime
+        import humanize
+
+        admin = self.get_user("admin")
+        start_changed_on = datetime.now()
+        dashboard = self.insert_dashboard("title", "slug1", [admin.id])
+
+        self.login(username="admin")
+
+        arguments = {
+            "order_column": "changed_on_delta_humanized",
+            "order_direction": "desc",
+        }
+        uri = f"api/v1/dashboard/?q={prison.dumps(arguments)}"
+
+        rv = self.get_assert_metric(uri, "get_list")
+        self.assertEqual(rv.status_code, 200)
+        data = json.loads(rv.data.decode("utf-8"))
+        self.assertEqual(
+            data["result"][0]["changed_on_delta_humanized"],
+            humanize.naturaltime(datetime.now() - start_changed_on),
+        )
+
+        # rollback changes
+        db.session.delete(dashboard)
+        db.session.commit()
+
     def test_get_dashboards_filter(self):
         """
         Dashboard API: Test get dashboards filter
@@ -214,9 +247,9 @@ class TestDashboardApi(SupersetTestCase, 
ApiOwnersTestCaseMixin):
         self.assertEqual(data["count"], 3)
 
         expected_response = [
-            {"slug": "ZY_bar", "dashboard_title": "foo_a",},
-            {"slug": "slug1zy_", "dashboard_title": "foo_b",},
-            {"slug": "slug1", "dashboard_title": "zy_foo",},
+            {"slug": "ZY_bar", "dashboard_title": "foo_a"},
+            {"slug": "slug1zy_", "dashboard_title": "foo_b"},
+            {"slug": "slug1", "dashboard_title": "zy_foo"},
         ]
         for index, item in enumerate(data["result"]):
             self.assertEqual(item["slug"], expected_response[index]["slug"])

Reply via email to