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
This commit is contained in:
Daniel Vaz Gaspar 2020-07-15 19:09:32 +01:00 committed by GitHub
parent ac85aebe4a
commit 74cb82e1ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 109 additions and 30 deletions

View File

@ -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)"`,
);
});
});

View File

@ -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', () => {

View File

@ -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',

View File

@ -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',

View File

@ -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 });

View File

@ -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",

View File

@ -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",

View File

@ -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)

View File

@ -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

View File

@ -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"])