mirror of https://github.com/apache/superset.git
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:
parent
ac85aebe4a
commit
74cb82e1ad
|
@ -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)"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"])
|
||||
|
|
Loading…
Reference in New Issue