mirror of
https://github.com/discourse/discourse-data-explorer.git
synced 2025-03-06 09:29:12 +00:00
FEATURE: Bookmarkable QueryGroups. (#177)
Adds the ability for non-Admin users to bookmark Queries from inside Group > Reports > Query view.
This commit is contained in:
parent
92bdea38b2
commit
45b6e7eb4f
@ -65,7 +65,13 @@ class DataExplorer::QueryController < ::ApplicationController
|
||||
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render_serialized @query, DataExplorer::QuerySerializer, root: 'query'
|
||||
query_group = DataExplorer::QueryGroup.find_by(query_id: @query.id, group_id: @group.id)
|
||||
|
||||
render json: {
|
||||
query: serialize_data(@query, DataExplorer::QuerySerializer, root: nil),
|
||||
query_group: serialize_data(query_group, DataExplorer::QueryGroupSerializer, root: nil),
|
||||
}
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -6,5 +6,7 @@ module DataExplorer
|
||||
|
||||
belongs_to :query
|
||||
belongs_to :group
|
||||
|
||||
has_many :bookmarks, as: :bookmarkable
|
||||
end
|
||||
end
|
||||
|
25
app/serializers/data_explorer/query_group_serializer.rb
Normal file
25
app/serializers/data_explorer/query_group_serializer.rb
Normal file
@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class DataExplorer::QueryGroupSerializer < ActiveModel::Serializer
|
||||
attributes :id, :group_id, :query_id, :bookmark,
|
||||
|
||||
def query_group_bookmark
|
||||
@query_group_bookmark ||= Bookmark.find_by(user: scope.user, bookmarkable: object)
|
||||
end
|
||||
|
||||
def include_bookmark?
|
||||
query_group_bookmark.present?
|
||||
end
|
||||
|
||||
def bookmark
|
||||
{
|
||||
id: query_group_bookmark.id,
|
||||
reminder_at: query_group_bookmark.reminder_at,
|
||||
name: query_group_bookmark.name,
|
||||
auto_delete_preference: query_group_bookmark.auto_delete_preference,
|
||||
bookmarkable_id: query_group_bookmark.bookmarkable_id,
|
||||
bookmarkable_type: query_group_bookmark.bookmarkable_type
|
||||
}
|
||||
end
|
||||
|
||||
end
|
@ -0,0 +1,38 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class UserDataExplorerQueryGroupBookmarkSerializer < UserBookmarkBaseSerializer
|
||||
def title
|
||||
fancy_title
|
||||
end
|
||||
|
||||
def fancy_title
|
||||
data_explorer_query.name
|
||||
end
|
||||
|
||||
def cooked
|
||||
data_explorer_query.description
|
||||
end
|
||||
|
||||
def bookmarkable_user
|
||||
@bookmarkable_user ||= data_explorer_query.user
|
||||
end
|
||||
|
||||
def bookmarkable_url
|
||||
"/g/#{data_explorer_query_group.group.name}/reports/#{data_explorer_query_group.query_id}"
|
||||
end
|
||||
|
||||
def excerpt
|
||||
return nil unless cooked
|
||||
@excerpt ||= PrettyText.excerpt(cooked, 300, keep_emoji_images: true)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def data_explorer_query
|
||||
data_explorer_query_group.query
|
||||
end
|
||||
|
||||
def data_explorer_query_group
|
||||
object.bookmarkable
|
||||
end
|
||||
end
|
@ -1,5 +1,11 @@
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import Bookmark, {
|
||||
NO_REMINDER_ICON,
|
||||
WITH_REMINDER_ICON,
|
||||
} from "discourse/models/bookmark";
|
||||
import { openBookmarkModal } from "discourse/controllers/bookmark";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
showResults: false,
|
||||
@ -35,5 +41,50 @@ export default Ember.Controller.extend({
|
||||
})
|
||||
.finally(() => this.set("loading", false));
|
||||
},
|
||||
|
||||
toggleBookmark() {
|
||||
return openBookmarkModal(
|
||||
this.queryGroup.bookmark ||
|
||||
Bookmark.create({
|
||||
bookmarkable_type: "DataExplorer::QueryGroup",
|
||||
bookmarkable_id: this.queryGroup.id,
|
||||
user_id: this.currentUser.id,
|
||||
}),
|
||||
{
|
||||
onAfterSave: (savedData) => {
|
||||
const bookmark = Bookmark.create(savedData);
|
||||
this.set("queryGroup.bookmark", bookmark);
|
||||
this.appEvents.trigger(
|
||||
"bookmarks:changed",
|
||||
savedData,
|
||||
bookmark.attachedTo()
|
||||
);
|
||||
},
|
||||
onAfterDelete: () => {
|
||||
this.set("queryGroup.bookmark", null);
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
}, // actions
|
||||
|
||||
@discourseComputed("queryGroup.bookmark")
|
||||
bookmarkLabel(bookmark) {
|
||||
return bookmark ? "bookmarked.edit_bookmark" : "bookmarked.title";
|
||||
},
|
||||
|
||||
@discourseComputed("queryGroup.bookmark")
|
||||
bookmarkIcon(bookmark) {
|
||||
if (bookmark && bookmark.reminder_at) {
|
||||
return WITH_REMINDER_ICON;
|
||||
}
|
||||
return NO_REMINDER_ICON;
|
||||
},
|
||||
|
||||
@discourseComputed("queryGroup.bookmark")
|
||||
bookmarkClassName(bookmark) {
|
||||
return bookmark
|
||||
? ["bookmark", "bookmarked", "query-group-bookmark"].join(" ")
|
||||
: ["bookmark", "query-group-bookmark"].join(" ");
|
||||
},
|
||||
});
|
||||
|
@ -9,15 +9,20 @@ export default DiscourseRoute.extend({
|
||||
const group = this.modelFor("group");
|
||||
return ajax(`/g/${group.name}/reports/${params.query_id}`)
|
||||
.then((response) => {
|
||||
const queryParamInfo = response.query.param_info;
|
||||
let query = response.query;
|
||||
let queryGroup = response.query_group;
|
||||
|
||||
const queryParamInfo = query.param_info;
|
||||
|
||||
const queryParams = queryParamInfo.reduce((acc, param) => {
|
||||
acc[param.identifier] = param.default;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
model: Object.assign({ params: queryParams }, response.query),
|
||||
model: Object.assign({ params: queryParams }, query),
|
||||
group,
|
||||
queryGroup,
|
||||
};
|
||||
})
|
||||
.catch(() => {
|
||||
|
@ -10,6 +10,14 @@
|
||||
</div>
|
||||
{{/if}}
|
||||
{{d-button action=(action "run") icon="play" label="explorer.run" class="btn-primary" type="submit"}}
|
||||
|
||||
{{d-button
|
||||
action=(action "toggleBookmark")
|
||||
label=bookmarkLabel
|
||||
icon=bookmarkIcon
|
||||
class=bookmarkClassName
|
||||
}}
|
||||
|
||||
</form>
|
||||
{{conditional-loading-spinner condition=loading}}
|
||||
{{#if results}}
|
||||
|
@ -436,3 +436,9 @@ table.group-reports {
|
||||
.right-buttons .btn {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.query-group-bookmark {
|
||||
&.bookmark .d-icon {
|
||||
color: var(--tertiary);
|
||||
}
|
||||
}
|
||||
|
57
lib/data_explorer_query_group_bookmarkable.rb
Normal file
57
lib/data_explorer_query_group_bookmarkable.rb
Normal file
@ -0,0 +1,57 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class DataExplorerQueryGroupBookmarkable < BaseBookmarkable
|
||||
def self.model
|
||||
DataExplorer::QueryGroup
|
||||
end
|
||||
|
||||
def self.serializer
|
||||
UserDataExplorerQueryGroupBookmarkSerializer
|
||||
end
|
||||
|
||||
def self.preload_associations
|
||||
[:data_explorer_queries, :groups]
|
||||
end
|
||||
|
||||
def self.list_query(user, guardian)
|
||||
group_ids = []
|
||||
if !user.admin?
|
||||
group_ids = user.visible_groups.pluck(:id)
|
||||
return if group_ids.empty?
|
||||
end
|
||||
|
||||
query = user.bookmarks_of_type("DataExplorer::QueryGroup")
|
||||
.joins("INNER JOIN data_explorer_query_groups ON data_explorer_query_groups.id = bookmarks.bookmarkable_id")
|
||||
.joins("LEFT JOIN data_explorer_queries ON data_explorer_queries.id = data_explorer_query_groups.query_id")
|
||||
query = query.where("data_explorer_query_groups.group_id IN (?)", group_ids) if !user.admin?
|
||||
query
|
||||
end
|
||||
|
||||
# Searchable only by data_explorer_queries name
|
||||
def self.search_query(bookmarks, query, ts_query, &bookmarkable_search)
|
||||
bookmarkable_search.call(bookmarks,
|
||||
"data_explorer_queries.name ILIKE :q")
|
||||
end
|
||||
|
||||
def self.reminder_handler(bookmark)
|
||||
bookmark.user.notifications.create!(
|
||||
notification_type: Notification.types[:bookmark_reminder],
|
||||
data: {
|
||||
title: bookmark.bookmarkable.query.name,
|
||||
display_username: bookmark.user.username,
|
||||
bookmark_name: bookmark.name,
|
||||
bookmarkable_url: "/g/#{bookmark.bookmarkable.group.name}/reports/#{bookmark.bookmarkable.query.id}"
|
||||
}.to_json
|
||||
)
|
||||
end
|
||||
|
||||
def self.reminder_conditions(bookmark)
|
||||
bookmark.bookmarkable.present?
|
||||
end
|
||||
|
||||
def self.can_see?(guardian, bookmark)
|
||||
return false if !bookmark.bookmarkable.group
|
||||
guardian.user_is_a_member_of_group?(bookmark.bookmarkable.group)
|
||||
end
|
||||
|
||||
end
|
@ -875,6 +875,12 @@ SQL
|
||||
end
|
||||
end
|
||||
|
||||
load File.expand_path('../lib/data_explorer_query_group_bookmarkable.rb', __FILE__)
|
||||
load File.expand_path('../app/serializers/user_data_explorer_query_group_bookmark_serializer.rb', __FILE__)
|
||||
|
||||
# Making DataExplorer::QueryGroup Bookmarkable.
|
||||
Bookmark.register_bookmarkable(DataExplorerQueryGroupBookmarkable)
|
||||
|
||||
require_dependency 'application_controller'
|
||||
require_dependency File.expand_path('../lib/queries.rb', __FILE__)
|
||||
|
||||
|
13
spec/fabricators/query_fabricator.rb
Normal file
13
spec/fabricators/query_fabricator.rb
Normal file
@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
Fabricator(:query, from: "DataExplorer::Query") do
|
||||
name
|
||||
description
|
||||
sql
|
||||
user
|
||||
end
|
||||
|
||||
Fabricator(:query_group, from: "DataExplorer::QueryGroup") do
|
||||
query
|
||||
group
|
||||
end
|
148
spec/lib/data_explorer_query_group_bookmarkable_spec.rb
Normal file
148
spec/lib/data_explorer_query_group_bookmarkable_spec.rb
Normal file
@ -0,0 +1,148 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe DataExplorerQueryGroupBookmarkable do
|
||||
fab!(:admin_user) { Fabricate(:admin) }
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
fab!(:guardian) { Guardian.new(user) }
|
||||
fab!(:group0) { Fabricate(:group) }
|
||||
fab!(:group1) { Fabricate(:group) }
|
||||
fab!(:group2) { Fabricate(:group) }
|
||||
fab!(:group3) { Fabricate(:group) }
|
||||
fab!(:query1) { Fabricate(:query, name: "My First Query",
|
||||
description: "This is the description of my 1st query.",
|
||||
sql: "Not really important",
|
||||
user: admin_user) }
|
||||
fab!(:query2) { Fabricate(:query, name: "My Second Query",
|
||||
description: "This is my 2nd query's description.",
|
||||
sql: "Not really important",
|
||||
user: admin_user) }
|
||||
|
||||
before do
|
||||
SiteSetting.data_explorer_enabled = true
|
||||
Bookmark.register_bookmarkable(DataExplorerQueryGroupBookmarkable)
|
||||
end
|
||||
|
||||
# Groups 0 and 1 have access to the Query 1.
|
||||
let!(:query_group1) { Fabricate(:query_group, query: query1, group: group0) }
|
||||
let!(:query_group2) { Fabricate(:query_group, query: query1, group: group1) }
|
||||
# User is member of both groups.
|
||||
let!(:group_user1) { Fabricate(:group_user, user: user, group: group0) }
|
||||
let!(:group_user2) { Fabricate(:group_user, user: user, group: group1) }
|
||||
|
||||
# Group 1 also has access to query2.
|
||||
let!(:query_group3) { Fabricate(:query_group, query: query2, group: group1) }
|
||||
|
||||
# Group 2 has access to query 1. User is NOT a member of this group.
|
||||
let!(:query_group4) { Fabricate(:query_group, query: query1, group: group2) }
|
||||
|
||||
# User is a member of Group 3, which has no access to Query 1.
|
||||
let!(:group_user3) { Fabricate(:group_user, user: user, group: group3) }
|
||||
|
||||
# User bookmarked the same Query 1 twice, from different Groups (0 and 1)
|
||||
let!(:bookmark1) { Fabricate(:bookmark, user: user,
|
||||
bookmarkable: query_group1,
|
||||
name: "something i gotta do") }
|
||||
let!(:bookmark2) { Fabricate(:bookmark, user: user,
|
||||
bookmarkable: query_group2,
|
||||
name: "something else i have to do") }
|
||||
|
||||
# User also bookmarked Query 2 from Group 1.
|
||||
let!(:bookmark3) { Fabricate(:bookmark, user: user,
|
||||
bookmarkable: query_group3,
|
||||
name: "this is the other query I needed.") }
|
||||
|
||||
# User previously bookmarked Query 1 from Group 2, of which she is no longer a member.
|
||||
let!(:bookmark4) { Fabricate(:bookmark, user: user,
|
||||
bookmarkable: query_group4,
|
||||
name: "something i gotta do also") }
|
||||
|
||||
subject { RegisteredBookmarkable.new(DataExplorerQueryGroupBookmarkable) }
|
||||
|
||||
describe "#perform_list_query" do
|
||||
it "returns all the user's bookmarks" do
|
||||
expect(subject.perform_list_query(user, guardian).map(&:id)).to match_array([bookmark1.id, bookmark2.id, bookmark3.id])
|
||||
end
|
||||
|
||||
it "does not return bookmarks made from groups that the user is no longer a member of" do
|
||||
expect(subject.perform_list_query(user, guardian).map(&:id)).not_to include(bookmark4.id)
|
||||
|
||||
# remove user from the other groups from which they bookmarked a query
|
||||
group_user1.delete
|
||||
group_user2.delete
|
||||
|
||||
# bookmarks is now empty, because user is not a member of any Groups with permission to see the query
|
||||
expect(subject.perform_list_query(user, guardian)).to be_empty
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "#perform_search_query" do
|
||||
before do
|
||||
SearchIndexer.enable
|
||||
end
|
||||
|
||||
it "returns bookmarks that match by name" do
|
||||
ts_query = Search.ts_query(term: "gotta", ts_config: "simple")
|
||||
expect(subject.perform_search_query(subject.perform_list_query(user, guardian), "%gotta%", ts_query).map(&:id)).to match_array([bookmark1.id])
|
||||
end
|
||||
|
||||
it "returns bookmarks that match by Query name" do
|
||||
ts_query = Search.ts_query(term: "First", ts_config: "simple")
|
||||
expect(subject.perform_search_query(subject.perform_list_query(user, guardian), "%First%", ts_query).map(&:id)).to match_array([bookmark1.id, bookmark2.id])
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "#can_send_reminder?" do
|
||||
it "cannot send the reminder if the group is revoked access to the query" do
|
||||
expect(subject.can_send_reminder?(bookmark1)).to eq(true)
|
||||
bookmark1.bookmarkable.delete
|
||||
bookmark1.reload
|
||||
expect(subject.can_send_reminder?(bookmark1)).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#reminder_handler" do
|
||||
it "creates a notification for the user with the correct details" do
|
||||
expect { subject.send_reminder_notification(bookmark1) }.to change { Notification.count }.by(1)
|
||||
notif = user.notifications.last
|
||||
expect(notif.notification_type).to eq(Notification.types[:bookmark_reminder])
|
||||
expect(notif.data).to eq(
|
||||
{
|
||||
title: bookmark1.bookmarkable.query.name,
|
||||
display_username: bookmark1.user.username,
|
||||
bookmark_name: bookmark1.name,
|
||||
bookmarkable_url: "/g/#{bookmark1.bookmarkable.group.name}/reports/#{bookmark1.bookmarkable.query.id}"
|
||||
}.to_json
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#can_see?" do
|
||||
it "returns false if the user is not a member of the group from which they created the bookmark" do
|
||||
expect(subject.can_see?(guardian, bookmark1)).to eq(true) #Query 1, Group 0
|
||||
expect(subject.can_see?(guardian, bookmark2)).to eq(true) #Query 1, Group 1
|
||||
expect(subject.can_see?(guardian, bookmark3)).to eq(true) #Query 2, Group 1
|
||||
expect(subject.can_see?(guardian, bookmark4)).to eq(false) #Query 1, Group 2
|
||||
|
||||
# remove from Groups 0 and 1
|
||||
group_user1.delete # Group 0
|
||||
group_user2.delete # Group 1
|
||||
guardian.user.reload
|
||||
|
||||
expect(subject.can_see?(guardian, bookmark1)).to eq(false)
|
||||
expect(subject.can_see?(guardian, bookmark2)).to eq(false)
|
||||
expect(subject.can_see?(guardian, bookmark3)).to eq(false)
|
||||
expect(subject.can_see?(guardian, bookmark4)).to eq(false)
|
||||
|
||||
# And adding the user back to the group, just to be sure.
|
||||
Fabricate(:group_user, user: user, group: group2)
|
||||
guardian.user.reload
|
||||
|
||||
expect(subject.can_see?(guardian, bookmark4)).to eq(true)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
Loading…
x
Reference in New Issue
Block a user