mirror of
https://github.com/valitydev/redash.git
synced 2024-11-07 01:25:16 +00:00
Add archived queries section to queries list. (#2888)
* Add archived queries section to queries list. * Refactor route building for list based controllers. This also fixes the dashboard empty state page.
This commit is contained in:
parent
b0a11983fa
commit
69e34f048a
@ -181,7 +181,7 @@ body {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-favourite {
|
||||
.btn-favourite, .btn-archive {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
@ -194,7 +194,7 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.btn-favourite {
|
||||
.btn-favourite, .btn-archive {
|
||||
color: #d4d4d4;
|
||||
transition: all .25s ease-in-out;
|
||||
|
||||
@ -207,7 +207,20 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.page-header--new .btn-favourite {
|
||||
.btn-archive {
|
||||
color: #d4d4d4;
|
||||
transition: all .25s ease-in-out;
|
||||
|
||||
&:hover, &:focus {
|
||||
color: @gray-light;
|
||||
}
|
||||
|
||||
.fa-archive {
|
||||
color: @gray-light;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header--new .btn-favourite, .page-header--new .btn-archive {
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
@ -243,7 +256,7 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.navbar li a .btn-favourite .fa {
|
||||
.navbar li a .btn-favourite .fa, .navbar li a .btn-archive .fa {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,37 @@
|
||||
import { bind } from 'lodash';
|
||||
import { bind, each } from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import { LivePaginator } from '@/lib/pagination';
|
||||
import { Query } from '@/services/query';
|
||||
import { Dashboard } from '@/services/dashboard';
|
||||
import { User } from '@/services/user';
|
||||
|
||||
export default class ListCtrl {
|
||||
constructor($scope, $location, currentUser, clientConfig, defaultOrder = '-created_at') {
|
||||
export function buildListRoutes(name, routes, template) {
|
||||
const listRoutes = {};
|
||||
each(routes, (route) => {
|
||||
listRoutes[route.path] = {
|
||||
template,
|
||||
reloadOnSearch: false,
|
||||
title: route.title,
|
||||
resolve: {
|
||||
currentPage: () => route.page,
|
||||
resource: () => {
|
||||
// services that are using the ListCtrl class
|
||||
const listServices = {
|
||||
query: Query,
|
||||
dashboard: Dashboard,
|
||||
user: User,
|
||||
};
|
||||
return listServices[name].query.bind(listServices[name]);
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
return listRoutes;
|
||||
}
|
||||
|
||||
export class ListCtrl {
|
||||
constructor($scope, $location, $route, currentUser, clientConfig, defaultOrder = '-created_at') {
|
||||
this.title = $route.current.title; // will make it available as $ctrl.title
|
||||
this.searchTerm = $location.search().q || '';
|
||||
|
||||
this.page = parseInt($location.search().page || 1, 10);
|
||||
|
@ -1,6 +1,5 @@
|
||||
<div class="container">
|
||||
<page-header title="'Dashboards'"></page-header>
|
||||
|
||||
<page-header title="$ctrl.title"></page-header>
|
||||
<div class="row">
|
||||
<div class="col-md-3 list-control-t">
|
||||
<div class="m-b-10">
|
||||
@ -37,22 +36,22 @@
|
||||
</div>
|
||||
|
||||
<div ng-if="$ctrl.loaded && $ctrl.showEmptyState" class="col-md-9 list-content">
|
||||
<div ng-if="($ctrl.currentPage == 'all') && ($ctrl.searchText.length == 0 || $ctrl.searchText === undefined)">
|
||||
<empty-state
|
||||
icon="'zmdi zmdi-view-quilt'"
|
||||
description="'See the big picture'"
|
||||
illustration="'dashboard'"
|
||||
help-link="'https://help.redash.io/category/22-dashboards'"
|
||||
show-dashboard-step="true"
|
||||
></empty-state>
|
||||
<div ng-switch="$ctrl.emptyType">
|
||||
<div ng-switch-when="default">
|
||||
<empty-state
|
||||
icon="'zmdi zmdi-view-quilt'"
|
||||
description="'See the big picture'"
|
||||
illustration="'dashboard'"
|
||||
help-link="'https://help.redash.io/category/22-dashboards'"
|
||||
show-dashboard-step="true"
|
||||
></empty-state>
|
||||
</div>
|
||||
|
||||
<big-message ng-switch-when="favorites" message="'Mark dashboards as Favorite to list them here.'" icon="'fa-star'" />
|
||||
<big-message ng-switch-when="search" message="'Sorry, we couldn\'t find anything.'" icon="'fa-search'"></big-message>
|
||||
<no-tagged-objects-found ng-switch-when="tags" object-type="'dashboards'" tags="$ctrl.selectedTags" />
|
||||
</div>
|
||||
|
||||
<big-message ng-if="($ctrl.currentPage == 'favorites') && ($ctrl.searchTerm === undefined || $ctrl.searchTerm.length == 0) && $ctrl.selectedTags.size === 0"
|
||||
message="'Mark dashboards as Favorite to list them here.'" icon="'fa-star'" />
|
||||
|
||||
<big-message message="'Sorry, we couldn\'t find anything.'" icon="'fa-search'" ng-if="$ctrl.searchTerm.length > 0"></big-message>
|
||||
|
||||
<no-tagged-objects-found object-type="'dashboards'" tags="$ctrl.selectedTags" ng-if="$ctrl.selectedTags.size > 0" />
|
||||
</div>
|
||||
|
||||
<div ng-if="$ctrl.loaded && !$ctrl.showEmptyState" class="col-md-9 list-content">
|
||||
|
@ -1,12 +1,10 @@
|
||||
import { extend } from 'lodash';
|
||||
|
||||
import ListCtrl from '@/lib/list-ctrl';
|
||||
import { buildListRoutes, ListCtrl } from '@/lib/list-ctrl';
|
||||
import template from './dashboard-list.html';
|
||||
import './dashboard-list.css';
|
||||
|
||||
class DashboardListCtrl extends ListCtrl {
|
||||
constructor($scope, $location, currentUser, clientConfig, Dashboard) {
|
||||
super($scope, $location, currentUser, clientConfig);
|
||||
constructor($scope, $location, $route, currentUser, clientConfig, Dashboard) {
|
||||
super($scope, $location, $route, currentUser, clientConfig);
|
||||
this.Type = Dashboard;
|
||||
}
|
||||
|
||||
@ -14,6 +12,18 @@ class DashboardListCtrl extends ListCtrl {
|
||||
super.processResponse(data);
|
||||
const rows = data.results.map(d => new this.Type(d));
|
||||
this.paginator.updateRows(rows, data.count);
|
||||
|
||||
if (data.count === 0) {
|
||||
if (this.isInSearchMode()) {
|
||||
this.emptyType = 'search';
|
||||
} else if (this.selectedTags.size > 0) {
|
||||
this.emptyType = 'tags';
|
||||
} else if (this.currentPage === 'favorites') {
|
||||
this.emptyType = 'favorites';
|
||||
} else {
|
||||
this.emptyType = 'default';
|
||||
}
|
||||
}
|
||||
this.showEmptyState = data.count === 0;
|
||||
}
|
||||
}
|
||||
@ -23,42 +33,20 @@ export default function init(ngModule) {
|
||||
template,
|
||||
controller: DashboardListCtrl,
|
||||
});
|
||||
const routes = [
|
||||
{
|
||||
page: 'all',
|
||||
title: 'All Dashboards',
|
||||
path: '/dashboards',
|
||||
},
|
||||
{
|
||||
page: 'favorites',
|
||||
title: 'Favorite Dashboards',
|
||||
path: '/dashboards/favorites',
|
||||
},
|
||||
];
|
||||
|
||||
const route = {
|
||||
template: '<page-dashboard-list></page-dashboard-list>',
|
||||
reloadOnSearch: false,
|
||||
};
|
||||
|
||||
return {
|
||||
'/dashboards': extend(
|
||||
{
|
||||
title: 'Dashboards',
|
||||
resolve: {
|
||||
currentPage: () => 'all',
|
||||
resource(Dashboard) {
|
||||
'ngInject';
|
||||
|
||||
return Dashboard.query.bind(Dashboard);
|
||||
},
|
||||
},
|
||||
},
|
||||
route,
|
||||
),
|
||||
'/dashboards/favorites': extend(
|
||||
{
|
||||
title: 'Favorite Dashboards',
|
||||
resolve: {
|
||||
currentPage: () => 'favorites',
|
||||
resource(Dashboard) {
|
||||
'ngInject';
|
||||
|
||||
return Dashboard.favorites.bind(Dashboard);
|
||||
},
|
||||
},
|
||||
},
|
||||
route,
|
||||
),
|
||||
};
|
||||
return buildListRoutes('dashboard', routes, '<page-dashboard-list></page-dashboard-list>');
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
@ -1,14 +1,13 @@
|
||||
import moment from 'moment';
|
||||
import { extend } from 'lodash';
|
||||
|
||||
import ListCtrl from '@/lib/list-ctrl';
|
||||
import { buildListRoutes, ListCtrl } from '@/lib/list-ctrl';
|
||||
import template from './queries-list.html';
|
||||
import './queries-list.css';
|
||||
|
||||
|
||||
class QueriesListCtrl extends ListCtrl {
|
||||
constructor($scope, $location, currentUser, clientConfig, Query) {
|
||||
super($scope, $location, currentUser, clientConfig);
|
||||
constructor($scope, $location, $route, currentUser, clientConfig, Query) {
|
||||
super($scope, $location, $route, currentUser, clientConfig);
|
||||
this.Type = Query;
|
||||
this.showMyQueries = currentUser.hasPermission('create_query');
|
||||
}
|
||||
@ -32,6 +31,8 @@ class QueriesListCtrl extends ListCtrl {
|
||||
this.emptyType = 'favorites';
|
||||
} else if (this.currentPage === 'my') {
|
||||
this.emptyType = 'my';
|
||||
} else if (this.currentPage === 'archive') {
|
||||
this.emptyType = 'archive';
|
||||
} else {
|
||||
this.emptyType = 'default';
|
||||
}
|
||||
@ -46,57 +47,29 @@ export default function init(ngModule) {
|
||||
controller: QueriesListCtrl,
|
||||
});
|
||||
|
||||
const route = {
|
||||
template: '<page-queries-list></page-queries-list>',
|
||||
reloadOnSearch: false,
|
||||
};
|
||||
|
||||
return {
|
||||
'/queries': extend(
|
||||
{
|
||||
title: 'Queries',
|
||||
resolve: {
|
||||
currentPage: () => 'all',
|
||||
resource(Query) {
|
||||
'ngInject';
|
||||
|
||||
return Query.query.bind(Query);
|
||||
},
|
||||
},
|
||||
},
|
||||
route,
|
||||
),
|
||||
'/queries/my': extend(
|
||||
{
|
||||
title: 'My Queries',
|
||||
resolve: {
|
||||
currentPage: () => 'my',
|
||||
resource: (Query) => {
|
||||
'ngInject';
|
||||
|
||||
return Query.myQueries.bind(Query);
|
||||
},
|
||||
},
|
||||
},
|
||||
route,
|
||||
),
|
||||
'/queries/favorites': extend(
|
||||
{
|
||||
title: 'Favorite Queries',
|
||||
resolve: {
|
||||
currentPage: () => 'favorites',
|
||||
resource: (Query) => {
|
||||
'ngInject';
|
||||
|
||||
return Query.favorites.bind(Query);
|
||||
},
|
||||
},
|
||||
},
|
||||
route,
|
||||
),
|
||||
// TODO: setup redirect?
|
||||
// '/queries/search': _.extend(
|
||||
};
|
||||
const routes = [
|
||||
{
|
||||
page: 'all',
|
||||
title: 'All Queries',
|
||||
path: '/queries',
|
||||
},
|
||||
{
|
||||
page: 'my',
|
||||
title: 'My Queries',
|
||||
path: '/queries/my',
|
||||
},
|
||||
{
|
||||
page: 'favorites',
|
||||
title: 'Favorite Queries',
|
||||
path: '/queries/favorites',
|
||||
},
|
||||
{
|
||||
page: 'archive',
|
||||
title: 'Archived Queries',
|
||||
path: '/queries/archive',
|
||||
},
|
||||
];
|
||||
return buildListRoutes('query', routes, '<page-queries-list></page-queries-list>');
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
@ -1,6 +1,5 @@
|
||||
<div class="container">
|
||||
<page-header title="'Queries'"></page-header>
|
||||
|
||||
<page-header title="$ctrl.title"></page-header>
|
||||
<div class="row">
|
||||
<div class="col-md-3 list-control-t">
|
||||
<div class="m-b-10">
|
||||
@ -17,10 +16,19 @@
|
||||
</span>
|
||||
Favorites
|
||||
</a>
|
||||
|
||||
<a href="queries/archive" class="list-group-item" ng-class="{active: $ctrl.currentPage == 'archive'}">
|
||||
<span class="btn-archive">
|
||||
<i class="fa fa-archive" aria-hidden="true"></i>
|
||||
</span>
|
||||
Archive
|
||||
</a>
|
||||
|
||||
<a href="queries/my" class="list-group-item" ng-if="$ctrl.showMyQueries" ng-class="{active: $ctrl.currentPage == 'my'}">
|
||||
<img ng-src="{{$ctrl.currentUser.profile_image_url}}" class="profile__image--navbar" width="13" style="margin-right: 0;"
|
||||
/> My Queries
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<div ng-if="$ctrl.currentPage != 'my'">
|
||||
@ -56,6 +64,7 @@
|
||||
<a href="https://redash.io/help/user-guide/querying/writing-queries">query writing documentation</a>.
|
||||
</div>
|
||||
|
||||
<big-message ng-switch-when="archive" message="'Archived queries will be listed here.'" icon="'fa-archive'" />
|
||||
<big-message ng-switch-when="favorites" message="'Mark queries as Favorite to list them here.'" icon="'fa-star'" />
|
||||
<big-message ng-switch-when="search" message="'Sorry, we couldn\'t find anything.'" icon="'fa-search'"></big-message>
|
||||
<no-tagged-objects-found ng-switch-when="tags" object-type="'queries'" tags="$ctrl.selectedTags" />
|
||||
@ -132,10 +141,19 @@
|
||||
</span>
|
||||
Favorites
|
||||
</a>
|
||||
|
||||
<a href="queries/archive" class="list-group-item" ng-class="{active: $ctrl.currentPage == 'archive'}">
|
||||
<span class="btn-archive">
|
||||
<i class="fa fa-archive" aria-hidden="true"></i>
|
||||
</span>
|
||||
Archive
|
||||
</a>
|
||||
|
||||
<a href="queries/my" class="list-group-item" ng-if="$ctrl.showMyQueries" ng-class="{active: $ctrl.currentPage == 'my'}">
|
||||
<img ng-src="{{$ctrl.currentUser.profile_image_url}}" class="profile__image--navbar" width="13" style="margin-right: 0;"
|
||||
/> My Queries
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<div ng-if="$ctrl.currentPage != 'my'" class="m-b-10">
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { extend } from 'lodash';
|
||||
import { policy } from '@/services/policy';
|
||||
import ListCtrl from '@/lib/list-ctrl';
|
||||
import { buildListRoutes, ListCtrl } from '@/lib/list-ctrl';
|
||||
import settingsMenu from '@/services/settingsMenu';
|
||||
import template from './list.html';
|
||||
|
||||
|
||||
class UsersListCtrl extends ListCtrl {
|
||||
constructor($scope, $location, currentUser, clientConfig, User) {
|
||||
super($scope, $location, currentUser, clientConfig);
|
||||
constructor($scope, $location, $route, currentUser, clientConfig, User) {
|
||||
super($scope, $location, $route, currentUser, clientConfig);
|
||||
this.policy = policy;
|
||||
this.enableUser = user => User.enableUser(user).then(this.update);
|
||||
this.disableUser = user => User.disableUser(user).then(this.update);
|
||||
@ -43,41 +43,20 @@ export default function init(ngModule) {
|
||||
template,
|
||||
});
|
||||
|
||||
const route = {
|
||||
template: '<users-list-page></users-list-page>',
|
||||
reloadOnSearch: false,
|
||||
};
|
||||
const routes = [
|
||||
{
|
||||
page: 'all',
|
||||
title: 'All Users',
|
||||
path: '/users',
|
||||
},
|
||||
{
|
||||
page: 'disabled',
|
||||
title: 'Disabled Users',
|
||||
path: '/users/disabled',
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
'/users': extend(
|
||||
{
|
||||
title: 'Users',
|
||||
resolve: {
|
||||
currentPage: () => 'all',
|
||||
resource(User) {
|
||||
'ngInject';
|
||||
|
||||
return User.query.bind(User);
|
||||
},
|
||||
},
|
||||
},
|
||||
route,
|
||||
),
|
||||
'/users/disabled': extend(
|
||||
{
|
||||
resolve: {
|
||||
currentPage: () => 'disabled',
|
||||
resource(User) {
|
||||
'ngInject';
|
||||
|
||||
return User.query.bind(User);
|
||||
},
|
||||
},
|
||||
title: 'Disabled Users',
|
||||
},
|
||||
route,
|
||||
),
|
||||
};
|
||||
return buildListRoutes('user', routes, '<users-list-page></users-list-page>');
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
@ -335,6 +335,11 @@ function QueryResource(
|
||||
isArray: true,
|
||||
url: 'api/queries/recent',
|
||||
},
|
||||
archive: {
|
||||
method: 'get',
|
||||
isArray: false,
|
||||
url: 'api/queries/archive',
|
||||
},
|
||||
query: {
|
||||
isArray: false,
|
||||
},
|
||||
|
@ -9,7 +9,7 @@ from redash.handlers.alerts import AlertResource, AlertListResource, AlertSubscr
|
||||
from redash.handlers.dashboards import DashboardListResource, DashboardResource, DashboardShareResource, PublicDashboardResource
|
||||
from redash.handlers.data_sources import DataSourceTypeListResource, DataSourceListResource, DataSourceSchemaResource, DataSourceResource, DataSourcePauseResource, DataSourceTestResource
|
||||
from redash.handlers.events import EventsResource
|
||||
from redash.handlers.queries import QueryForkResource, QueryRefreshResource, QueryListResource, QueryRecentResource, QuerySearchResource, QueryResource, MyQueriesResource
|
||||
from redash.handlers.queries import QueryArchiveResource, QueryForkResource, QueryRefreshResource, QueryListResource, QueryRecentResource, QuerySearchResource, QueryResource, MyQueriesResource
|
||||
from redash.handlers.query_results import QueryResultListResource, QueryResultResource, JobResource
|
||||
from redash.handlers.users import UserResource, UserListResource, UserInviteResource, UserResetPasswordResource, UserDisableResource, UserRegenerateApiKeyResource
|
||||
from redash.handlers.visualizations import VisualizationListResource
|
||||
@ -79,6 +79,7 @@ api.add_org_resource(DashboardTagsResource, '/api/dashboards/tags', endpoint='da
|
||||
|
||||
api.add_org_resource(QuerySearchResource, '/api/queries/search', endpoint='queries_search')
|
||||
api.add_org_resource(QueryRecentResource, '/api/queries/recent', endpoint='recent_queries')
|
||||
api.add_org_resource(QueryArchiveResource, '/api/queries/archive', endpoint='queries_archive')
|
||||
api.add_org_resource(QueryListResource, '/api/queries', endpoint='queries')
|
||||
api.add_org_resource(MyQueriesResource, '/api/queries/my', endpoint='my_queries')
|
||||
api.add_org_resource(QueryRefreshResource, '/api/queries/<query_id>/refresh', endpoint='query_refresh')
|
||||
|
@ -13,7 +13,7 @@ def organization_status(org_slug=None):
|
||||
'users': models.User.all(current_org).count(),
|
||||
'alerts': models.Alert.all(group_ids=current_user.group_ids).count(),
|
||||
'data_sources': models.DataSource.all(current_org, group_ids=current_user.group_ids).count(),
|
||||
'queries': models.Query.all_queries(current_user.group_ids, current_user.id, drafts=True).count(),
|
||||
'queries': models.Query.all_queries(current_user.group_ids, current_user.id, include_drafts=True).count(),
|
||||
'dashboards': models.Dashboard.query.filter(models.Dashboard.org==current_org, models.Dashboard.is_archived==False).count(),
|
||||
}
|
||||
|
||||
|
@ -102,7 +102,76 @@ class QueryRecentResource(BaseResource):
|
||||
return QuerySerializer(results, with_last_modified_by=False, with_user=False).serialize()
|
||||
|
||||
|
||||
class QueryListResource(BaseResource):
|
||||
class BaseQueryListResource(BaseResource):
|
||||
|
||||
def get_queries(self, search_term):
|
||||
if search_term:
|
||||
results = models.Query.search(
|
||||
search_term,
|
||||
self.current_user.group_ids,
|
||||
self.current_user.id,
|
||||
include_drafts=True,
|
||||
)
|
||||
else:
|
||||
results = models.Query.all_queries(
|
||||
self.current_user.group_ids,
|
||||
self.current_user.id,
|
||||
include_drafts=True,
|
||||
)
|
||||
return filter_by_tags(results, models.Query.tags)
|
||||
|
||||
@require_permission('view_query')
|
||||
def get(self):
|
||||
"""
|
||||
Retrieve a list of queries.
|
||||
|
||||
:qparam number page_size: Number of queries to return per page
|
||||
:qparam number page: Page number to retrieve
|
||||
:qparam number order: Name of column to order by
|
||||
:qparam number q: Full text search term
|
||||
|
||||
Responds with an array of :ref:`query <query-response-label>` objects.
|
||||
"""
|
||||
# See if we want to do full-text search or just regular queries
|
||||
search_term = request.args.get('q', '')
|
||||
|
||||
queries = self.get_queries(search_term)
|
||||
|
||||
results = filter_by_tags(queries, models.Query.tags)
|
||||
|
||||
# order results according to passed order parameter,
|
||||
# special-casing search queries where the database
|
||||
# provides an order by search rank
|
||||
ordered_results = order_results(results, fallback=bool(search_term))
|
||||
|
||||
page = request.args.get('page', 1, type=int)
|
||||
page_size = request.args.get('page_size', 25, type=int)
|
||||
|
||||
response = paginate(
|
||||
ordered_results,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
serializer=QuerySerializer,
|
||||
with_stats=True,
|
||||
with_last_modified_by=False
|
||||
)
|
||||
|
||||
if search_term:
|
||||
self.record_event({
|
||||
'action': 'search',
|
||||
'object_type': 'query',
|
||||
'term': search_term,
|
||||
})
|
||||
else:
|
||||
self.record_event({
|
||||
'action': 'list',
|
||||
'object_type': 'query',
|
||||
})
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class QueryListResource(BaseQueryListResource):
|
||||
@require_permission('create_query')
|
||||
def post(self):
|
||||
"""
|
||||
@ -161,68 +230,26 @@ class QueryListResource(BaseResource):
|
||||
|
||||
return QuerySerializer(query).serialize()
|
||||
|
||||
@require_permission('view_query')
|
||||
def get(self):
|
||||
"""
|
||||
Retrieve a list of queries.
|
||||
|
||||
:qparam number page_size: Number of queries to return per page
|
||||
:qparam number page: Page number to retrieve
|
||||
:qparam number order: Name of column to order by
|
||||
:qparam number q: Full text search term
|
||||
|
||||
Responds with an array of :ref:`query <query-response-label>` objects.
|
||||
"""
|
||||
# See if we want to do full-text search or just regular queries
|
||||
search_term = request.args.get('q', '')
|
||||
class QueryArchiveResource(BaseQueryListResource):
|
||||
|
||||
def get_queries(self, search_term):
|
||||
if search_term:
|
||||
results = models.Query.search(
|
||||
return models.Query.search(
|
||||
search_term,
|
||||
self.current_user.group_ids,
|
||||
self.current_user.id,
|
||||
include_drafts=True,
|
||||
include_drafts=False,
|
||||
include_archived=True,
|
||||
)
|
||||
else:
|
||||
results = models.Query.all_queries(
|
||||
return models.Query.all_queries(
|
||||
self.current_user.group_ids,
|
||||
self.current_user.id,
|
||||
drafts=True,
|
||||
include_drafts=False,
|
||||
include_archived=True,
|
||||
)
|
||||
|
||||
results = filter_by_tags(results, models.Query.tags)
|
||||
|
||||
# order results according to passed order parameter,
|
||||
# special-casing search queries where the database
|
||||
# provides an order by search rank
|
||||
ordered_results = order_results(results, fallback=bool(search_term))
|
||||
|
||||
page = request.args.get('page', 1, type=int)
|
||||
page_size = request.args.get('page_size', 25, type=int)
|
||||
|
||||
response = paginate(
|
||||
ordered_results,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
serializer=QuerySerializer,
|
||||
with_stats=True,
|
||||
with_last_modified_by=False
|
||||
)
|
||||
|
||||
if search_term:
|
||||
self.record_event({
|
||||
'action': 'search',
|
||||
'object_type': 'query',
|
||||
'term': search_term,
|
||||
})
|
||||
else:
|
||||
self.record_event({
|
||||
'action': 'list',
|
||||
'object_type': 'query',
|
||||
})
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class MyQueriesResource(BaseResource):
|
||||
@require_permission('view_query')
|
||||
|
@ -466,7 +466,7 @@ class Query(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model):
|
||||
return query
|
||||
|
||||
@classmethod
|
||||
def all_queries(cls, group_ids, user_id=None, drafts=False):
|
||||
def all_queries(cls, group_ids, user_id=None, include_drafts=False, include_archived=False):
|
||||
query_ids = (
|
||||
db.session
|
||||
.query(distinct(cls.id))
|
||||
@ -474,10 +474,10 @@ class Query(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model):
|
||||
DataSourceGroup,
|
||||
Query.data_source_id == DataSourceGroup.data_source_id
|
||||
)
|
||||
.filter(Query.is_archived == False)
|
||||
.filter(Query.is_archived.is_(include_archived))
|
||||
.filter(DataSourceGroup.group_id.in_(group_ids))
|
||||
)
|
||||
q = (
|
||||
queries = (
|
||||
cls
|
||||
.query
|
||||
.options(
|
||||
@ -503,19 +503,19 @@ class Query(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model):
|
||||
.order_by(Query.created_at.desc())
|
||||
)
|
||||
|
||||
if not drafts:
|
||||
q = q.filter(
|
||||
if not include_drafts:
|
||||
queries = queries.filter(
|
||||
or_(
|
||||
Query.is_draft == False,
|
||||
Query.is_draft.is_(False),
|
||||
Query.user_id == user_id
|
||||
)
|
||||
)
|
||||
return q
|
||||
return queries
|
||||
|
||||
@classmethod
|
||||
def favorites(cls, user, base_query=None):
|
||||
if base_query is None:
|
||||
base_query = cls.all_queries(user.group_ids, user.id, drafts=True)
|
||||
base_query = cls.all_queries(user.group_ids, user.id, include_drafts=True)
|
||||
return base_query.join((
|
||||
Favorite,
|
||||
and_(
|
||||
@ -529,7 +529,7 @@ class Query(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model):
|
||||
queries = cls.all_queries(
|
||||
group_ids=user.group_ids,
|
||||
user_id=user.id,
|
||||
drafts=include_drafts,
|
||||
include_drafts=include_drafts,
|
||||
)
|
||||
|
||||
tag_column = func.unnest(cls.tags).label('tag')
|
||||
@ -550,11 +550,13 @@ class Query(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model):
|
||||
|
||||
@classmethod
|
||||
def outdated_queries(cls):
|
||||
queries = (Query.query
|
||||
.options(joinedload(Query.latest_query_data).load_only('retrieved_at'))
|
||||
.filter(Query.schedule.isnot(None))
|
||||
.order_by(Query.id))
|
||||
|
||||
queries = (
|
||||
Query.query
|
||||
.options(joinedload(Query.latest_query_data).load_only('retrieved_at'))
|
||||
.filter(Query.schedule.isnot(None))
|
||||
.order_by(Query.id)
|
||||
)
|
||||
|
||||
now = utils.utcnow()
|
||||
outdated_queries = {}
|
||||
scheduled_queries_executions.refresh()
|
||||
@ -582,8 +584,14 @@ class Query(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model):
|
||||
return outdated_queries.values()
|
||||
|
||||
@classmethod
|
||||
def search(cls, term, group_ids, user_id=None, include_drafts=False, limit=None):
|
||||
all_queries = cls.all_queries(group_ids, user_id=user_id, drafts=include_drafts)
|
||||
def search(cls, term, group_ids, user_id=None, include_drafts=False,
|
||||
limit=None, include_archived=False):
|
||||
all_queries = cls.all_queries(
|
||||
group_ids,
|
||||
user_id=user_id,
|
||||
include_drafts=include_drafts,
|
||||
include_archived=include_archived,
|
||||
)
|
||||
# sort the result using the weight as defined in the search vector column
|
||||
return all_queries.search(term, sort=True).limit(limit)
|
||||
|
||||
|
@ -134,6 +134,7 @@ class TestQueryResourcePost(BaseTestCase):
|
||||
self.assertEqual(rv.json['name'], 'Testing')
|
||||
self.assertEqual(rv.json['last_modified_by']['id'], user.id)
|
||||
|
||||
|
||||
class TestQueryListResourceGet(BaseTestCase):
|
||||
def test_returns_queries(self):
|
||||
q1 = self.factory.create_query()
|
||||
@ -147,8 +148,8 @@ class TestQueryListResourceGet(BaseTestCase):
|
||||
|
||||
def test_filters_with_tags(self):
|
||||
q1 = self.factory.create_query(tags=[u'test'])
|
||||
q2 = self.factory.create_query()
|
||||
q3 = self.factory.create_query()
|
||||
self.factory.create_query()
|
||||
self.factory.create_query()
|
||||
|
||||
rv = self.make_request('get', '/api/queries?tags=test')
|
||||
assert len(rv.json['results']) == 1
|
||||
@ -157,12 +158,13 @@ class TestQueryListResourceGet(BaseTestCase):
|
||||
def test_search_term(self):
|
||||
q1 = self.factory.create_query(name="Sales")
|
||||
q2 = self.factory.create_query(name="Q1 sales")
|
||||
q3 = self.factory.create_query(name="Ops")
|
||||
self.factory.create_query(name="Ops")
|
||||
|
||||
rv = self.make_request('get', '/api/queries?q=sales')
|
||||
assert len(rv.json['results']) == 2
|
||||
assert set(map(lambda d: d['id'], rv.json['results'])) == set([q1.id, q2.id])
|
||||
|
||||
|
||||
class TestQueryListResourcePost(BaseTestCase):
|
||||
def test_create_query(self):
|
||||
query_data = {
|
||||
@ -185,6 +187,27 @@ class TestQueryListResourcePost(BaseTestCase):
|
||||
self.assertTrue(query.is_draft)
|
||||
|
||||
|
||||
class TestQueryArchiveResourceGet(BaseTestCase):
|
||||
def test_returns_queries(self):
|
||||
q1 = self.factory.create_query(is_archived=True)
|
||||
q2 = self.factory.create_query(is_archived=True)
|
||||
self.factory.create_query()
|
||||
|
||||
rv = self.make_request('get', '/api/queries/archive')
|
||||
|
||||
assert len(rv.json['results']) == 2
|
||||
assert set(map(lambda d: d['id'], rv.json['results'])) == set([q1.id, q2.id])
|
||||
|
||||
def test_search_term(self):
|
||||
q1 = self.factory.create_query(name="Sales", is_archived=True)
|
||||
q2 = self.factory.create_query(name="Q1 sales", is_archived=True)
|
||||
self.factory.create_query(name="Q2 sales")
|
||||
|
||||
rv = self.make_request('get', '/api/queries/archive?q=sales')
|
||||
assert len(rv.json['results']) == 2
|
||||
assert set(map(lambda d: d['id'], rv.json['results'])) == set([q1.id, q2.id])
|
||||
|
||||
|
||||
class QueryRefreshTest(BaseTestCase):
|
||||
def setUp(self):
|
||||
super(QueryRefreshTest, self).setUp()
|
||||
|
Loading…
Reference in New Issue
Block a user