mirror of
https://github.com/discourse/discourse-data-explorer.git
synced 2025-03-09 13:24:53 +00:00
FEATURE: introduce data-explorer tables (#61)
Instead of using `PluginStoreRow` we should use plugin-specific models like `DataExplorer::Query` and `DataExplorer::QueryGroup`
This commit is contained in:
parent
fe420931ba
commit
fe0806eb2a
228
app/controllers/data_explorer/query_controller.rb
Normal file
228
app/controllers/data_explorer/query_controller.rb
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
class DataExplorer::QueryController < ::ApplicationController
|
||||||
|
requires_plugin DataExplorer.plugin_name
|
||||||
|
|
||||||
|
before_action :set_group, only: %i(group_reports_index group_reports_show group_reports_run)
|
||||||
|
before_action :set_query, only: %i(group_reports_show group_reports_run show update)
|
||||||
|
skip_before_action :check_xhr, only: %i(show group_reports_run run)
|
||||||
|
|
||||||
|
def index
|
||||||
|
queries = DataExplorer::Query.where(hidden: false).order(:last_run_at, :name).includes(:groups).to_a
|
||||||
|
|
||||||
|
database_queries_ids = DataExplorer::Query.pluck(:id)
|
||||||
|
Queries.default.each do |params|
|
||||||
|
attributes = params.last
|
||||||
|
next if database_queries_ids.include?(attributes["id"])
|
||||||
|
query = DataExplorer::Query.new
|
||||||
|
query.id = attributes["id"]
|
||||||
|
query.sql = attributes["sql"]
|
||||||
|
query.name = attributes["name"]
|
||||||
|
query.description = attributes["description"]
|
||||||
|
query.user_id = Discourse::SYSTEM_USER_ID.to_s
|
||||||
|
queries << query
|
||||||
|
end
|
||||||
|
|
||||||
|
render_serialized queries, DataExplorer::QuerySerializer, root: 'queries'
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
check_xhr unless params[:export]
|
||||||
|
|
||||||
|
if params[:export]
|
||||||
|
response.headers['Content-Disposition'] = "attachment; filename=#{@query.slug}.dcquery.json"
|
||||||
|
response.sending_file = true
|
||||||
|
end
|
||||||
|
|
||||||
|
return raise Discourse::NotFound if !guardian.user_can_access_query?(@query) || @query.hidden
|
||||||
|
render_serialized @query, DataExplorer::QuerySerializer, root: 'query'
|
||||||
|
end
|
||||||
|
|
||||||
|
def groups
|
||||||
|
render json: Group.all.select(:id, :name), root: false
|
||||||
|
end
|
||||||
|
|
||||||
|
def group_reports_index
|
||||||
|
return raise Discourse::NotFound unless guardian.user_is_a_member_of_group?(@group)
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.json do
|
||||||
|
queries = DataExplorer::Query
|
||||||
|
.where(hidden: false)
|
||||||
|
.joins("INNER JOIN data_explorer_query_groups
|
||||||
|
ON data_explorer_query_groups.query_id = data_explorer_queries.id
|
||||||
|
AND data_explorer_query_groups.group_id = #{@group.id}")
|
||||||
|
render_serialized(queries, DataExplorer::QuerySerializer, root: 'queries')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def group_reports_show
|
||||||
|
return raise Discourse::NotFound if !guardian.group_and_user_can_access_query?(@group, @query) || @query.hidden
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.json do
|
||||||
|
render_serialized @query, DataExplorer::QuerySerializer, root: 'query'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def group_reports_run
|
||||||
|
return raise Discourse::NotFound if !guardian.group_and_user_can_access_query?(@group, @query) || @query.hidden
|
||||||
|
|
||||||
|
run
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
query = DataExplorer::Query.create!(params.require(:query).permit(:name, :description, :sql).merge(user_id: current_user.id, last_run_at: Time.now))
|
||||||
|
group_ids = params.require(:query)[:group_ids]
|
||||||
|
group_ids&.each do |group_id|
|
||||||
|
query.query_groups.find_or_create_by!(group_id: group_id)
|
||||||
|
end
|
||||||
|
render_serialized query, DataExplorer::QuerySerializer, root: 'query'
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
@query.update!(params.require(:query).permit(:name, :sql, :description).merge(hidden: false))
|
||||||
|
|
||||||
|
group_ids = params.require(:query)[:group_ids]
|
||||||
|
DataExplorer::QueryGroup.where.not(group_id: group_ids).where(query_id: @query.id).delete_all
|
||||||
|
group_ids&.each do |group_id|
|
||||||
|
@query.query_groups.find_or_create_by!(group_id: group_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
render_serialized @query, DataExplorer::QuerySerializer, root: 'query'
|
||||||
|
rescue DataExplorer::ValidationError => e
|
||||||
|
render_json_error e.message
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
query = DataExplorer::Query.where(id: params[:id]).first_or_initialize
|
||||||
|
query.update!(hidden: true)
|
||||||
|
|
||||||
|
render json: { success: true, errors: [] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def schema
|
||||||
|
schema_version = DB.query_single("SELECT max(version) AS tag FROM schema_migrations").first
|
||||||
|
if stale?(public: true, etag: schema_version, template: false)
|
||||||
|
render json: DataExplorer.schema
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Return value:
|
||||||
|
# success - true/false. if false, inspect the errors value.
|
||||||
|
# errors - array of strings.
|
||||||
|
# params - hash. Echo of the query parameters as executed.
|
||||||
|
# duration - float. Time to execute the query, in milliseconds, to 1 decimal place.
|
||||||
|
# columns - array of strings. Titles of the returned columns, in order.
|
||||||
|
# explain - string. (Optional - pass explain=true in the request) Postgres query plan, UNIX newlines.
|
||||||
|
# rows - array of array of strings. Results of the query. In the same order as 'columns'.
|
||||||
|
def run
|
||||||
|
check_xhr unless params[:download]
|
||||||
|
|
||||||
|
query = DataExplorer::Query.find(params[:id].to_i)
|
||||||
|
query.update!(last_run_at: Time.now)
|
||||||
|
|
||||||
|
if params[:download]
|
||||||
|
response.sending_file = true
|
||||||
|
end
|
||||||
|
|
||||||
|
params[:params] = params[:_params] if params[:_params] # testing workaround
|
||||||
|
query_params = {}
|
||||||
|
query_params = MultiJson.load(params[:params]) if params[:params]
|
||||||
|
|
||||||
|
opts = { current_user: current_user.username }
|
||||||
|
opts[:explain] = true if params[:explain] == "true"
|
||||||
|
|
||||||
|
opts[:limit] =
|
||||||
|
if params[:format] == "csv"
|
||||||
|
if params[:limit].present?
|
||||||
|
limit = params[:limit].to_i
|
||||||
|
limit = DataExplorer::QUERY_RESULT_MAX_LIMIT if limit > DataExplorer::QUERY_RESULT_MAX_LIMIT
|
||||||
|
limit
|
||||||
|
else
|
||||||
|
DataExplorer::QUERY_RESULT_MAX_LIMIT
|
||||||
|
end
|
||||||
|
elsif params[:limit].present?
|
||||||
|
params[:limit] == "ALL" ? "ALL" : params[:limit].to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
result = DataExplorer.run_query(query, query_params, opts)
|
||||||
|
|
||||||
|
if result[:error]
|
||||||
|
err = result[:error]
|
||||||
|
|
||||||
|
# Pretty printing logic
|
||||||
|
err_class = err.class
|
||||||
|
err_msg = err.message
|
||||||
|
if err.is_a? ActiveRecord::StatementInvalid
|
||||||
|
err_class = err.original_exception.class
|
||||||
|
err_msg.gsub!("#{err_class}:", '')
|
||||||
|
else
|
||||||
|
err_msg = "#{err_class}: #{err_msg}"
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
success: false,
|
||||||
|
errors: [err_msg]
|
||||||
|
}, status: 422
|
||||||
|
else
|
||||||
|
pg_result = result[:pg_result]
|
||||||
|
cols = pg_result.fields
|
||||||
|
respond_to do |format|
|
||||||
|
format.json do
|
||||||
|
if params[:download]
|
||||||
|
response.headers['Content-Disposition'] =
|
||||||
|
"attachment; filename=#{query.slug}@#{Slug.for(Discourse.current_hostname, 'discourse')}-#{Date.today}.dcqresult.json"
|
||||||
|
end
|
||||||
|
json = {
|
||||||
|
success: true,
|
||||||
|
errors: [],
|
||||||
|
duration: (result[:duration_secs].to_f * 1000).round(1),
|
||||||
|
result_count: pg_result.values.length || 0,
|
||||||
|
params: query_params,
|
||||||
|
columns: cols,
|
||||||
|
default_limit: DataExplorer::QUERY_RESULT_DEFAULT_LIMIT
|
||||||
|
}
|
||||||
|
json[:explain] = result[:explain] if opts[:explain]
|
||||||
|
|
||||||
|
if !params[:download]
|
||||||
|
relations, colrender = DataExplorer.add_extra_data(pg_result)
|
||||||
|
json[:relations] = relations
|
||||||
|
json[:colrender] = colrender
|
||||||
|
end
|
||||||
|
|
||||||
|
json[:rows] = pg_result.values
|
||||||
|
|
||||||
|
render json: json
|
||||||
|
end
|
||||||
|
format.csv do
|
||||||
|
response.headers['Content-Disposition'] =
|
||||||
|
"attachment; filename=#{query.slug}@#{Slug.for(Discourse.current_hostname, 'discourse')}-#{Date.today}.dcqresult.csv"
|
||||||
|
|
||||||
|
require 'csv'
|
||||||
|
text = CSV.generate do |csv|
|
||||||
|
csv << cols
|
||||||
|
pg_result.values.each do |row|
|
||||||
|
csv << row
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
render plain: text
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_group
|
||||||
|
@group = Group.find_by(name: params["group_name"])
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_query
|
||||||
|
@query = DataExplorer::Query.find_by(id: params[:id])
|
||||||
|
raise Discourse::NotFound unless @query
|
||||||
|
end
|
||||||
|
end
|
40
app/models/data_explorer/query.rb
Normal file
40
app/models/data_explorer/query.rb
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DataExplorer
|
||||||
|
class Query < ActiveRecord::Base
|
||||||
|
self.table_name = 'data_explorer_queries'
|
||||||
|
has_many :query_groups
|
||||||
|
has_many :groups, through: :query_groups
|
||||||
|
belongs_to :user
|
||||||
|
|
||||||
|
def params
|
||||||
|
@params ||= DataExplorer::Parameter.create_from_sql(sql)
|
||||||
|
end
|
||||||
|
|
||||||
|
def cast_params(input_params)
|
||||||
|
result = {}.with_indifferent_access
|
||||||
|
self.params.each do |pobj|
|
||||||
|
result[pobj.identifier] = pobj.cast_to_ruby input_params[pobj.identifier]
|
||||||
|
end
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
def slug
|
||||||
|
Slug.for(name).presence || "query-#{id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.find(id)
|
||||||
|
if id.to_i < 0
|
||||||
|
default_query = Queries.default[id.to_s]
|
||||||
|
return raise ActiveRecord::RecordNotFound unless default_query
|
||||||
|
query = Query.find_by(id: id) || Query.new
|
||||||
|
query.attributes = default_query
|
||||||
|
query.user_id = Discourse::SYSTEM_USER_ID.to_s
|
||||||
|
query
|
||||||
|
else
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
11
app/models/data_explorer/query_group.rb
Normal file
11
app/models/data_explorer/query_group.rb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DataExplorer
|
||||||
|
class QueryGroup < ActiveRecord::Base
|
||||||
|
self.table_name = 'data_explorer_query_groups'
|
||||||
|
|
||||||
|
belongs_to :query
|
||||||
|
belongs_to :group
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
16
app/serializers/data_explorer/query_serializer.rb
Normal file
16
app/serializers/data_explorer/query_serializer.rb
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
class DataExplorer::QuerySerializer < ActiveModel::Serializer
|
||||||
|
attributes :id, :sql, :name, :description, :param_info, :created_at, :username, :group_ids, :last_run_at, :hidden, :user_id
|
||||||
|
|
||||||
|
def param_info
|
||||||
|
object&.params&.map(&:to_hash)
|
||||||
|
end
|
||||||
|
|
||||||
|
def username
|
||||||
|
object&.user&.username
|
||||||
|
end
|
||||||
|
|
||||||
|
def group_ids
|
||||||
|
object.groups.map(&:id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
3
app/serializers/data_explorer/small_badge_serializer.rb
Normal file
3
app/serializers/data_explorer/small_badge_serializer.rb
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
class DataExplorer::SmallBadgeSerializer < ApplicationSerializer
|
||||||
|
attributes :id, :name, :badge_type, :description, :icon
|
||||||
|
end
|
@ -0,0 +1,12 @@
|
|||||||
|
class DataExplorer::SmallPostWithExcerptSerializer < ApplicationSerializer
|
||||||
|
attributes :id, :topic_id, :post_number, :excerpt, :username, :avatar_template
|
||||||
|
def excerpt
|
||||||
|
Post.excerpt(object.cooked, 70)
|
||||||
|
end
|
||||||
|
def username
|
||||||
|
object.user && object.user.username
|
||||||
|
end
|
||||||
|
def avatar_template
|
||||||
|
object.user && object.user.avatar_template
|
||||||
|
end
|
||||||
|
end
|
@ -67,7 +67,7 @@ export default Ember.Controller.extend({
|
|||||||
@computed("groups")
|
@computed("groups")
|
||||||
groupOptions(groups) {
|
groupOptions(groups) {
|
||||||
return groups.map(g => {
|
return groups.map(g => {
|
||||||
return { id: g.id.toString(), name: g.name };
|
return { id: g.id, name: g.name };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@ Query.reopenClass({
|
|||||||
"name",
|
"name",
|
||||||
"description",
|
"description",
|
||||||
"sql",
|
"sql",
|
||||||
"created_by",
|
"user_id",
|
||||||
"created_at",
|
"created_at",
|
||||||
"group_ids",
|
"group_ids",
|
||||||
"last_run_at"
|
"last_run_at"
|
||||||
|
92
db/migrate/20200810053843_create_data_explorer_queries.rb
Normal file
92
db/migrate/20200810053843_create_data_explorer_queries.rb
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateDataExplorerQueries < ActiveRecord::Migration[6.0]
|
||||||
|
def up
|
||||||
|
create_table :data_explorer_queries do |t|
|
||||||
|
t.string :name
|
||||||
|
t.text :description
|
||||||
|
t.text :sql, default: "SELECT 1", null: false
|
||||||
|
t.integer :user_id
|
||||||
|
t.datetime :last_run_at
|
||||||
|
t.boolean :hidden, default: false, null: false
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table :data_explorer_query_groups do |t|
|
||||||
|
t.integer :query_id
|
||||||
|
t.integer :group_id
|
||||||
|
t.index :query_id
|
||||||
|
t.index :group_id
|
||||||
|
end
|
||||||
|
add_index(:data_explorer_query_groups, [:query_id, :group_id], unique: true)
|
||||||
|
|
||||||
|
DB.exec <<~SQL, now: Time.zone.now
|
||||||
|
INSERT INTO data_explorer_queries(id, name, description, sql, user_id, last_run_at, hidden, created_at, updated_at)
|
||||||
|
SELECT
|
||||||
|
(replace(key, 'q:',''))::integer,
|
||||||
|
value::json->>'name',
|
||||||
|
value::json->>'description',
|
||||||
|
value::json->>'sql',
|
||||||
|
(value::json->>'created_by')::integer,
|
||||||
|
CASE WHEN (value::json->'last_run_at')::text = 'null' THEN
|
||||||
|
null
|
||||||
|
WHEN (value::json->'last_run_at')::text = '""' THEN
|
||||||
|
null
|
||||||
|
ELSE
|
||||||
|
(value::json->'last_run_at')::text::timestamptz
|
||||||
|
END,
|
||||||
|
CASE WHEN (value::json->'hidden')::text = 'null' THEN
|
||||||
|
false
|
||||||
|
ELSE
|
||||||
|
(value::json->'hidden')::text::boolean
|
||||||
|
END,
|
||||||
|
:now,
|
||||||
|
:now
|
||||||
|
FROM plugin_store_rows
|
||||||
|
WHERE plugin_name = 'discourse-data-explorer' AND type_name = 'JSON' AND (replace(key, 'q:',''))::integer < 0
|
||||||
|
SQL
|
||||||
|
|
||||||
|
DB.exec <<~SQL, now: Time.zone.now
|
||||||
|
INSERT INTO data_explorer_queries(name, description, sql, user_id, last_run_at, hidden, created_at, updated_at)
|
||||||
|
SELECT
|
||||||
|
value::json->>'name',
|
||||||
|
value::json->>'description',
|
||||||
|
value::json->>'sql',
|
||||||
|
(value::json->>'created_by')::integer,
|
||||||
|
CASE WHEN (value::json->'last_run_at')::text = 'null' THEN
|
||||||
|
null
|
||||||
|
WHEN (value::json->'last_run_at')::text = '""' THEN
|
||||||
|
null
|
||||||
|
ELSE
|
||||||
|
(value::json->'last_run_at')::text::timestamptz
|
||||||
|
END,
|
||||||
|
CASE WHEN (value::json->'hidden')::text = 'null' THEN
|
||||||
|
false
|
||||||
|
ELSE
|
||||||
|
(value::json->'hidden')::text::boolean
|
||||||
|
END,
|
||||||
|
:now,
|
||||||
|
:now
|
||||||
|
FROM plugin_store_rows
|
||||||
|
WHERE plugin_name = 'discourse-data-explorer' AND type_name = 'JSON' AND (replace(key, 'q:',''))::integer > 0
|
||||||
|
SQL
|
||||||
|
|
||||||
|
DB.query("SELECT * FROM plugin_store_rows WHERE plugin_name = 'discourse-data-explorer' AND type_name = 'JSON'").each do |row|
|
||||||
|
json = JSON.parse(row.value)
|
||||||
|
next if json['group_ids'].blank?
|
||||||
|
query_id = DB.query("SELECT id FROM data_explorer_queries WHERE
|
||||||
|
name = ? AND sql = ?", json['name'], json['sql']).first.id
|
||||||
|
|
||||||
|
json['group_ids'].each do |group_id|
|
||||||
|
DB.exec <<~SQL
|
||||||
|
INSERT INTO data_explorer_query_groups(query_id, group_id)
|
||||||
|
VALUES(#{query_id}, #{group_id})
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
raise ActiveRecord::IrreversibleMigration
|
||||||
|
end
|
||||||
|
end
|
@ -4,12 +4,9 @@
|
|||||||
desc "Shows a list of hidden queries"
|
desc "Shows a list of hidden queries"
|
||||||
task('data_explorer:list_hidden_queries').clear
|
task('data_explorer:list_hidden_queries').clear
|
||||||
task 'data_explorer:list_hidden_queries' => :environment do |t|
|
task 'data_explorer:list_hidden_queries' => :environment do |t|
|
||||||
hidden_queries = []
|
|
||||||
puts "\nHidden Queries\n\n"
|
puts "\nHidden Queries\n\n"
|
||||||
|
|
||||||
DataExplorer::Query.all.each do |query|
|
hidden_queries = DataExplorer::Query.where(hidden: false)
|
||||||
hidden_queries.push(query) if query.hidden
|
|
||||||
end
|
|
||||||
|
|
||||||
hidden_queries.each do |query|
|
hidden_queries.each do |query|
|
||||||
puts "Name: #{query.name}"
|
puts "Name: #{query.name}"
|
||||||
@ -25,18 +22,13 @@ task('data_explorer').clear
|
|||||||
task 'data_explorer' => :environment do |t, args|
|
task 'data_explorer' => :environment do |t, args|
|
||||||
args.extras.each do |arg|
|
args.extras.each do |arg|
|
||||||
id = arg.to_i
|
id = arg.to_i
|
||||||
|
query = DataExplorer::Query.find_by(id: id)
|
||||||
if DataExplorer.pstore_get("q:#{id}").nil?
|
if query
|
||||||
puts "\nError finding query with id #{id}"
|
puts "\nFound query with id #{id}"
|
||||||
|
query.update!(hidden: true)
|
||||||
|
puts "Query no.#{id} is now hidden"
|
||||||
else
|
else
|
||||||
q = DataExplorer::Query.find(id)
|
puts "\nError finding query with id #{id}"
|
||||||
if q
|
|
||||||
puts "\nFound query with id #{id}"
|
|
||||||
end
|
|
||||||
|
|
||||||
q.hidden = true
|
|
||||||
q.save
|
|
||||||
puts "Query no.#{id} is now hidden" if q.hidden
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
puts ""
|
puts ""
|
||||||
@ -49,18 +41,13 @@ task('data_explorer:unhide_query').clear
|
|||||||
task 'data_explorer:unhide_query' => :environment do |t, args|
|
task 'data_explorer:unhide_query' => :environment do |t, args|
|
||||||
args.extras.each do |arg|
|
args.extras.each do |arg|
|
||||||
id = arg.to_i
|
id = arg.to_i
|
||||||
|
query = DataExplorer::Query.find_by(id: id)
|
||||||
if DataExplorer.pstore_get("q:#{id}").nil?
|
if query
|
||||||
puts "\nError finding query with id #{id}"
|
puts "\nFound query with id #{id}"
|
||||||
|
query.update!(hidden: false)
|
||||||
|
puts "Query no.#{id} is now visible"
|
||||||
else
|
else
|
||||||
q = DataExplorer::Query.find(id)
|
puts "\nError finding query with id #{id}"
|
||||||
if q
|
|
||||||
puts "\nFound query with id #{id}"
|
|
||||||
end
|
|
||||||
|
|
||||||
q.hidden = false
|
|
||||||
q.save
|
|
||||||
puts "Query no.#{id} is now visible" unless q.hidden
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
puts ""
|
puts ""
|
||||||
@ -73,22 +60,19 @@ task('data_explorer:hard_delete').clear
|
|||||||
task 'data_explorer:hard_delete' => :environment do |t, args|
|
task 'data_explorer:hard_delete' => :environment do |t, args|
|
||||||
args.extras.each do |arg|
|
args.extras.each do |arg|
|
||||||
id = arg.to_i
|
id = arg.to_i
|
||||||
|
query = DataExplorer::Query.find_by(id: id)
|
||||||
|
if query
|
||||||
|
puts "\nFound query with id #{id}"
|
||||||
|
|
||||||
if DataExplorer.pstore_get("q:#{id}").nil?
|
if query.hidden
|
||||||
puts "\nError finding query with id #{id}"
|
query.destroy!
|
||||||
else
|
|
||||||
q = DataExplorer::Query.find(id)
|
|
||||||
if q
|
|
||||||
puts "\nFound query with id #{id}"
|
|
||||||
end
|
|
||||||
|
|
||||||
if q.hidden
|
|
||||||
DataExplorer.pstore_delete "q:#{id}"
|
|
||||||
puts "Query no.#{id} has been deleted"
|
puts "Query no.#{id} has been deleted"
|
||||||
else
|
else
|
||||||
puts "Query no.#{id} must be hidden in order to hard delete"
|
puts "Query no.#{id} must be hidden in order to hard delete"
|
||||||
puts "To hide the query, run: " + "rake data_explorer[#{id}]"
|
puts "To hide the query, run: " + "rake data_explorer[#{id}]"
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
puts "\nError finding query with id #{id}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
puts ""
|
puts ""
|
||||||
|
458
plugin.rb
458
plugin.rb
@ -31,21 +31,14 @@ module ::DataExplorer
|
|||||||
def self.plugin_name
|
def self.plugin_name
|
||||||
'discourse-data-explorer'.freeze
|
'discourse-data-explorer'.freeze
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.pstore_get(key)
|
|
||||||
PluginStore.get(DataExplorer.plugin_name, key)
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.pstore_set(key, value)
|
|
||||||
PluginStore.set(DataExplorer.plugin_name, key, value)
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.pstore_delete(key)
|
|
||||||
PluginStore.remove(DataExplorer.plugin_name, key)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
after_initialize do
|
after_initialize do
|
||||||
|
load File.expand_path('../app/models/data_explorer/query.rb', __FILE__)
|
||||||
|
load File.expand_path('../app/controllers/data_explorer/query_controller.rb', __FILE__)
|
||||||
|
load File.expand_path('../app/serializers/data_explorer/query_serializer.rb', __FILE__)
|
||||||
|
load File.expand_path('../app/serializers/data_explorer/small_badge_serializer.rb', __FILE__)
|
||||||
|
load File.expand_path('../app/serializers/data_explorer/small_post_with_excerpt_serializer.rb', __FILE__)
|
||||||
|
|
||||||
add_to_class(:guardian, :user_is_a_member_of_group?) do |group|
|
add_to_class(:guardian, :user_is_a_member_of_group?) do |group|
|
||||||
return false if !current_user
|
return false if !current_user
|
||||||
@ -53,11 +46,18 @@ after_initialize do
|
|||||||
return current_user.group_ids.include?(group.id)
|
return current_user.group_ids.include?(group.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
add_to_class(:guardian, :user_can_access_query?) do |group, query|
|
add_to_class(:guardian, :user_can_access_query?) do |query|
|
||||||
return false if !current_user
|
return false if !current_user
|
||||||
return true if current_user.admin?
|
return true if current_user.admin?
|
||||||
return user_is_a_member_of_group?(group) &&
|
query.groups.blank? || query.groups.any? do |group|
|
||||||
query.group_ids.include?(group.id.to_s)
|
user_is_a_member_of_group?(group)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
add_to_class(:guardian, :group_and_user_can_access_query?) do |group, query|
|
||||||
|
return false if !current_user
|
||||||
|
return true if current_user.admin?
|
||||||
|
return user_is_a_member_of_group?(group) && query.groups.exists?(id: group.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
module ::DataExplorer
|
module ::DataExplorer
|
||||||
@ -69,24 +69,6 @@ after_initialize do
|
|||||||
class ValidationError < StandardError
|
class ValidationError < StandardError
|
||||||
end
|
end
|
||||||
|
|
||||||
class SmallBadgeSerializer < ApplicationSerializer
|
|
||||||
attributes :id, :name, :badge_type, :description, :icon
|
|
||||||
end
|
|
||||||
|
|
||||||
class SmallPostWithExcerptSerializer < ApplicationSerializer
|
|
||||||
attributes :id, :topic_id, :post_number, :excerpt
|
|
||||||
attributes :username, :avatar_template
|
|
||||||
def excerpt
|
|
||||||
Post.excerpt(object.cooked, 70)
|
|
||||||
end
|
|
||||||
def username
|
|
||||||
object.user && object.user.username
|
|
||||||
end
|
|
||||||
def avatar_template
|
|
||||||
object.user && object.user.avatar_template
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Run a data explorer query on the currently connected database.
|
# Run a data explorer query on the currently connected database.
|
||||||
#
|
#
|
||||||
# @param [DataExplorer::Query] query the Query object to run
|
# @param [DataExplorer::Query] query the Query object to run
|
||||||
@ -633,143 +615,6 @@ SQL
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Reimplement a couple ActiveRecord methods, but use PluginStore for storage instead
|
|
||||||
require_dependency File.expand_path('../lib/queries.rb', __FILE__)
|
|
||||||
class DataExplorer::Query
|
|
||||||
attr_accessor :id, :name, :description, :sql, :created_by, :created_at, :group_ids, :last_run_at, :hidden
|
|
||||||
|
|
||||||
def initialize
|
|
||||||
@name = 'Unnamed Query'
|
|
||||||
@description = ''
|
|
||||||
@sql = 'SELECT 1'
|
|
||||||
@group_ids = []
|
|
||||||
@hidden = false
|
|
||||||
end
|
|
||||||
|
|
||||||
def slug
|
|
||||||
Slug.for(name).presence || "query-#{id}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def params
|
|
||||||
@params ||= DataExplorer::Parameter.create_from_sql(sql)
|
|
||||||
end
|
|
||||||
|
|
||||||
def check_params!
|
|
||||||
DataExplorer::Parameter.create_from_sql(sql, strict: true)
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def cast_params(input_params)
|
|
||||||
result = {}.with_indifferent_access
|
|
||||||
self.params.each do |pobj|
|
|
||||||
result[pobj.identifier] = pobj.cast_to_ruby input_params[pobj.identifier]
|
|
||||||
end
|
|
||||||
result
|
|
||||||
end
|
|
||||||
|
|
||||||
def can_be_run_by(group)
|
|
||||||
@group_ids.include?(group.id.to_s)
|
|
||||||
end
|
|
||||||
|
|
||||||
# saving/loading functions
|
|
||||||
# May want to extract this into a library or something for plugins to use?
|
|
||||||
def self.alloc_id
|
|
||||||
DistributedMutex.synchronize('data-explorer_query-id') do
|
|
||||||
max_id = DataExplorer.pstore_get("q:_id")
|
|
||||||
max_id = 1 unless max_id
|
|
||||||
DataExplorer.pstore_set("q:_id", max_id + 1)
|
|
||||||
max_id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.from_hash(h)
|
|
||||||
query = DataExplorer::Query.new
|
|
||||||
[:name, :description, :sql, :created_by, :created_at, :last_run_at].each do |sym|
|
|
||||||
query.send("#{sym}=", h[sym].strip) if h[sym]
|
|
||||||
end
|
|
||||||
group_ids = (h[:group_ids] == "" || !h[:group_ids]) ? [] : h[:group_ids]
|
|
||||||
query.group_ids = group_ids
|
|
||||||
query.id = h[:id].to_i if h[:id]
|
|
||||||
query.hidden = h[:hidden]
|
|
||||||
query
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_hash
|
|
||||||
{
|
|
||||||
id: @id,
|
|
||||||
name: @name,
|
|
||||||
description: @description,
|
|
||||||
sql: @sql,
|
|
||||||
created_by: @created_by,
|
|
||||||
created_at: @created_at,
|
|
||||||
group_ids: @group_ids,
|
|
||||||
last_run_at: @last_run_at,
|
|
||||||
hidden: @hidden
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.find(id, opts = {})
|
|
||||||
if DataExplorer.pstore_get("q:#{id}").nil? && id < 0
|
|
||||||
hash = Queries.default[id.to_s]
|
|
||||||
hash[:id] = id
|
|
||||||
from_hash hash
|
|
||||||
else
|
|
||||||
unless hash = DataExplorer.pstore_get("q:#{id}")
|
|
||||||
return DataExplorer::Query.new if opts[:ignore_deleted]
|
|
||||||
raise Discourse::NotFound
|
|
||||||
end
|
|
||||||
from_hash hash
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def save
|
|
||||||
check_params!
|
|
||||||
return save_default_query if @id && @id < 0
|
|
||||||
|
|
||||||
@id = @id || self.class.alloc_id
|
|
||||||
DataExplorer.pstore_set "q:#{id}", to_hash
|
|
||||||
end
|
|
||||||
|
|
||||||
def save_default_query
|
|
||||||
check_params!
|
|
||||||
# Read from queries.rb again to pick up any changes and save them
|
|
||||||
query = Queries.default[id.to_s]
|
|
||||||
@id = query["id"]
|
|
||||||
@sql = query["sql"]
|
|
||||||
@group_ids = @group_ids || []
|
|
||||||
@name = query["name"]
|
|
||||||
@description = query["description"]
|
|
||||||
|
|
||||||
DataExplorer.pstore_set "q:#{id}", to_hash
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
|
||||||
# Instead of deleting the query from the store, we can set
|
|
||||||
# it to be hidden and not send it to the frontend
|
|
||||||
@hidden = true
|
|
||||||
DataExplorer.pstore_set "q:#{id}", to_hash
|
|
||||||
end
|
|
||||||
|
|
||||||
def read_attribute_for_serialization(attr)
|
|
||||||
self.send(attr)
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.all
|
|
||||||
PluginStoreRow.where(plugin_name: DataExplorer.plugin_name)
|
|
||||||
.where("key LIKE 'q:%'")
|
|
||||||
.where("key != 'q:_id'")
|
|
||||||
.map do |psr|
|
|
||||||
DataExplorer::Query.from_hash PluginStore.cast_value(psr.type_name, psr.value)
|
|
||||||
end.sort_by { |query| query.name }
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.destroy_all
|
|
||||||
PluginStoreRow.where(plugin_name: DataExplorer.plugin_name)
|
|
||||||
.where("key LIKE 'q:%'")
|
|
||||||
.destroy_all
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class DataExplorer::Parameter
|
class DataExplorer::Parameter
|
||||||
attr_accessor :identifier, :type, :default, :nullable
|
attr_accessor :identifier, :type, :default, :nullable
|
||||||
|
|
||||||
@ -1012,279 +857,6 @@ SQL
|
|||||||
|
|
||||||
require_dependency 'application_controller'
|
require_dependency 'application_controller'
|
||||||
require_dependency File.expand_path('../lib/queries.rb', __FILE__)
|
require_dependency File.expand_path('../lib/queries.rb', __FILE__)
|
||||||
class DataExplorer::QueryController < ::ApplicationController
|
|
||||||
requires_plugin DataExplorer.plugin_name
|
|
||||||
|
|
||||||
before_action :check_enabled
|
|
||||||
before_action :set_group, only: [:group_reports_index, :group_reports_show, :group_reports_run]
|
|
||||||
before_action :set_query, only: [:group_reports_show, :group_reports_run]
|
|
||||||
|
|
||||||
attr_reader :group, :query
|
|
||||||
|
|
||||||
def check_enabled
|
|
||||||
raise Discourse::NotFound unless SiteSetting.data_explorer_enabled?
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_group
|
|
||||||
@group = Group.find_by(name: params["group_name"])
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_query
|
|
||||||
@query = DataExplorer::Query.find(params[:id].to_i)
|
|
||||||
end
|
|
||||||
|
|
||||||
def index
|
|
||||||
# guardian.ensure_can_use_data_explorer!
|
|
||||||
queries = []
|
|
||||||
DataExplorer::Query.all.each do |query|
|
|
||||||
queries.push(query) unless query.hidden
|
|
||||||
end
|
|
||||||
|
|
||||||
Queries.default.each do |params|
|
|
||||||
query = DataExplorer::Query.new
|
|
||||||
query.id = params.second["id"]
|
|
||||||
query.sql = params.second["sql"]
|
|
||||||
query.name = params.second["name"]
|
|
||||||
query.description = params.second["description"]
|
|
||||||
query.created_by = Discourse::SYSTEM_USER_ID.to_s
|
|
||||||
|
|
||||||
# don't render this query if query with the same id already exists in pstore
|
|
||||||
queries.push(query) unless DataExplorer.pstore_get("q:#{query.id}").present?
|
|
||||||
end
|
|
||||||
|
|
||||||
render_serialized queries, DataExplorer::QuerySerializer, root: 'queries'
|
|
||||||
end
|
|
||||||
|
|
||||||
skip_before_action :check_xhr, only: [:show]
|
|
||||||
def show
|
|
||||||
check_xhr unless params[:export]
|
|
||||||
|
|
||||||
query = DataExplorer::Query.find(params[:id].to_i)
|
|
||||||
|
|
||||||
if params[:export]
|
|
||||||
response.headers['Content-Disposition'] = "attachment; filename=#{query.slug}.dcquery.json"
|
|
||||||
response.sending_file = true
|
|
||||||
end
|
|
||||||
|
|
||||||
# guardian.ensure_can_see! query
|
|
||||||
render_serialized query, DataExplorer::QuerySerializer, root: 'query'
|
|
||||||
end
|
|
||||||
|
|
||||||
def groups
|
|
||||||
render_serialized(Group.all, BasicGroupSerializer)
|
|
||||||
end
|
|
||||||
|
|
||||||
def group_reports_index
|
|
||||||
return raise Discourse::NotFound unless guardian.user_is_a_member_of_group?(group)
|
|
||||||
|
|
||||||
respond_to do |format|
|
|
||||||
format.html { render 'groups/show' }
|
|
||||||
format.json do
|
|
||||||
queries = DataExplorer::Query.all.select do |query|
|
|
||||||
!query.hidden && query.group_ids&.include?(group.id.to_s)
|
|
||||||
end
|
|
||||||
render_serialized(queries, DataExplorer::QuerySerializer, root: 'queries')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def group_reports_show
|
|
||||||
return raise Discourse::NotFound if !guardian.user_can_access_query?(group, query) || query.hidden
|
|
||||||
|
|
||||||
respond_to do |format|
|
|
||||||
format.html { render 'groups/show' }
|
|
||||||
format.json do
|
|
||||||
render_serialized query, DataExplorer::QuerySerializer, root: 'query'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
skip_before_action :check_xhr, only: [:group_reports_run]
|
|
||||||
def group_reports_run
|
|
||||||
return raise Discourse::NotFound if !guardian.user_can_access_query?(group, query) || query.hidden
|
|
||||||
|
|
||||||
run
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
|
||||||
# guardian.ensure_can_create_explorer_query!
|
|
||||||
|
|
||||||
query = DataExplorer::Query.from_hash params.require(:query)
|
|
||||||
query.created_at = Time.now
|
|
||||||
query.created_by = current_user.id.to_s
|
|
||||||
query.last_run_at = Time.now
|
|
||||||
query.id = nil # json import will assign an id, which is wrong
|
|
||||||
query.save
|
|
||||||
|
|
||||||
render_serialized query, DataExplorer::QuerySerializer, root: 'query'
|
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
|
||||||
query = DataExplorer::Query.find(params[:id].to_i, ignore_deleted: true)
|
|
||||||
hash = params.require(:query)
|
|
||||||
hash[:group_ids] ||= []
|
|
||||||
|
|
||||||
# Undeleting
|
|
||||||
unless query.id
|
|
||||||
if hash[:id]
|
|
||||||
query.id = hash[:id].to_i
|
|
||||||
else
|
|
||||||
raise Discourse::NotFound
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
[:name, :sql, :description, :created_by, :created_at, :group_ids, :last_run_at, :hidden].each do |sym|
|
|
||||||
query.send("#{sym}=", hash[sym]) if hash[sym]
|
|
||||||
end
|
|
||||||
|
|
||||||
query.check_params!
|
|
||||||
query.hidden = false
|
|
||||||
query.save
|
|
||||||
|
|
||||||
render_serialized query, DataExplorer::QuerySerializer, root: 'query'
|
|
||||||
rescue DataExplorer::ValidationError => e
|
|
||||||
render_json_error e.message
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
|
||||||
query = DataExplorer::Query.find(params[:id].to_i)
|
|
||||||
query.destroy
|
|
||||||
|
|
||||||
render json: { success: true, errors: [] }
|
|
||||||
end
|
|
||||||
|
|
||||||
def schema
|
|
||||||
schema_version = DB.query_single("SELECT max(version) AS tag FROM schema_migrations").first
|
|
||||||
if stale?(public: true, etag: schema_version, template: false)
|
|
||||||
render json: DataExplorer.schema
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
skip_before_action :check_xhr, only: [:run]
|
|
||||||
# Return value:
|
|
||||||
# success - true/false. if false, inspect the errors value.
|
|
||||||
# errors - array of strings.
|
|
||||||
# params - hash. Echo of the query parameters as executed.
|
|
||||||
# duration - float. Time to execute the query, in milliseconds, to 1 decimal place.
|
|
||||||
# columns - array of strings. Titles of the returned columns, in order.
|
|
||||||
# explain - string. (Optional - pass explain=true in the request) Postgres query plan, UNIX newlines.
|
|
||||||
# rows - array of array of strings. Results of the query. In the same order as 'columns'.
|
|
||||||
def run
|
|
||||||
check_xhr unless params[:download]
|
|
||||||
|
|
||||||
query = DataExplorer::Query.find(params[:id].to_i)
|
|
||||||
query.last_run_at = Time.now
|
|
||||||
|
|
||||||
if params[:id].to_i < 0
|
|
||||||
query.created_by = Discourse::SYSTEM_USER_ID.to_s
|
|
||||||
query.save_default_query
|
|
||||||
else
|
|
||||||
query.save
|
|
||||||
end
|
|
||||||
|
|
||||||
if params[:download]
|
|
||||||
response.sending_file = true
|
|
||||||
end
|
|
||||||
|
|
||||||
params[:params] = params[:_params] if params[:_params] # testing workaround
|
|
||||||
query_params = {}
|
|
||||||
query_params = MultiJson.load(params[:params]) if params[:params]
|
|
||||||
|
|
||||||
opts = { current_user: current_user.username }
|
|
||||||
opts[:explain] = true if params[:explain] == "true"
|
|
||||||
|
|
||||||
opts[:limit] =
|
|
||||||
if params[:format] == "csv"
|
|
||||||
if params[:limit].present?
|
|
||||||
limit = params[:limit].to_i
|
|
||||||
limit = DataExplorer::QUERY_RESULT_MAX_LIMIT if limit > DataExplorer::QUERY_RESULT_MAX_LIMIT
|
|
||||||
limit
|
|
||||||
else
|
|
||||||
DataExplorer::QUERY_RESULT_MAX_LIMIT
|
|
||||||
end
|
|
||||||
elsif params[:limit].present?
|
|
||||||
params[:limit] == "ALL" ? "ALL" : params[:limit].to_i
|
|
||||||
end
|
|
||||||
|
|
||||||
result = DataExplorer.run_query(query, query_params, opts)
|
|
||||||
|
|
||||||
if result[:error]
|
|
||||||
err = result[:error]
|
|
||||||
|
|
||||||
# Pretty printing logic
|
|
||||||
err_class = err.class
|
|
||||||
err_msg = err.message
|
|
||||||
if err.is_a? ActiveRecord::StatementInvalid
|
|
||||||
err_class = err.original_exception.class
|
|
||||||
err_msg.gsub!("#{err_class}:", '')
|
|
||||||
else
|
|
||||||
err_msg = "#{err_class}: #{err_msg}"
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: {
|
|
||||||
success: false,
|
|
||||||
errors: [err_msg]
|
|
||||||
}, status: 422
|
|
||||||
else
|
|
||||||
pg_result = result[:pg_result]
|
|
||||||
cols = pg_result.fields
|
|
||||||
respond_to do |format|
|
|
||||||
format.json do
|
|
||||||
if params[:download]
|
|
||||||
response.headers['Content-Disposition'] =
|
|
||||||
"attachment; filename=#{query.slug}@#{Slug.for(Discourse.current_hostname, 'discourse')}-#{Date.today}.dcqresult.json"
|
|
||||||
end
|
|
||||||
json = {
|
|
||||||
success: true,
|
|
||||||
errors: [],
|
|
||||||
duration: (result[:duration_secs].to_f * 1000).round(1),
|
|
||||||
result_count: pg_result.values.length || 0,
|
|
||||||
params: query_params,
|
|
||||||
columns: cols,
|
|
||||||
default_limit: DataExplorer::QUERY_RESULT_DEFAULT_LIMIT
|
|
||||||
}
|
|
||||||
json[:explain] = result[:explain] if opts[:explain]
|
|
||||||
|
|
||||||
if !params[:download]
|
|
||||||
relations, colrender = DataExplorer.add_extra_data(pg_result)
|
|
||||||
json[:relations] = relations
|
|
||||||
json[:colrender] = colrender
|
|
||||||
end
|
|
||||||
|
|
||||||
json[:rows] = pg_result.values
|
|
||||||
|
|
||||||
render json: json
|
|
||||||
end
|
|
||||||
format.csv do
|
|
||||||
response.headers['Content-Disposition'] =
|
|
||||||
"attachment; filename=#{query.slug}@#{Slug.for(Discourse.current_hostname, 'discourse')}-#{Date.today}.dcqresult.csv"
|
|
||||||
|
|
||||||
require 'csv'
|
|
||||||
text = CSV.generate do |csv|
|
|
||||||
csv << cols
|
|
||||||
pg_result.values.each do |row|
|
|
||||||
csv << row
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
render plain: text
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class DataExplorer::QuerySerializer < ActiveModel::Serializer
|
|
||||||
attributes :id, :sql, :name, :description, :param_info, :created_by, :created_at, :username, :group_ids, :last_run_at, :hidden
|
|
||||||
|
|
||||||
def param_info
|
|
||||||
object.params.map(&:to_hash) rescue nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def username
|
|
||||||
User.find(created_by).username rescue nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
DataExplorer::Engine.routes.draw do
|
DataExplorer::Engine.routes.draw do
|
||||||
root to: "query#index"
|
root to: "query#index"
|
||||||
|
@ -12,15 +12,11 @@ describe DataExplorer::QueryController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def make_query(sql, opts = {}, group_ids = [])
|
def make_query(sql, opts = {}, group_ids = [])
|
||||||
q = DataExplorer::Query.new
|
query = DataExplorer::Query.create!(name: opts[:name] || "Query number", description: "A description for query number", sql: sql, hidden: opts[:hidden] || false)
|
||||||
q.id = Fabrication::Sequencer.sequence("query-id", 1)
|
group_ids.each do |group_id|
|
||||||
q.name = opts[:name] || "Query number #{q.id}"
|
query.query_groups.create!(group_id: group_id)
|
||||||
q.description = "A description for query number #{q.id}"
|
end
|
||||||
q.group_ids = group_ids
|
query
|
||||||
q.sql = sql
|
|
||||||
q.hidden = opts[:hidden] || false
|
|
||||||
q.save
|
|
||||||
q
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "Admin" do
|
describe "Admin" do
|
||||||
|
@ -7,13 +7,14 @@ describe Guardian do
|
|||||||
before { SiteSetting.data_explorer_enabled = true }
|
before { SiteSetting.data_explorer_enabled = true }
|
||||||
|
|
||||||
def make_query(group_ids = [])
|
def make_query(group_ids = [])
|
||||||
q = DataExplorer::Query.new
|
query = DataExplorer::Query.create!(name: "Query number #{Fabrication::Sequencer.sequence("query-id", 1)}", sql: "SELECT 1")
|
||||||
q.id = Fabrication::Sequencer.sequence("query-id", 1)
|
group_ids.each do |group_id|
|
||||||
q.name = "Query number #{q.id}"
|
query.query_groups.create!(group_id: group_id)
|
||||||
q.sql = "SELECT 1"
|
end
|
||||||
q.group_ids = group_ids
|
group_ids.each do |group_id|
|
||||||
q.save
|
query.query_groups.create!(group_id: group_id)
|
||||||
q
|
end
|
||||||
|
query
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:user) { build(:user) }
|
let(:user) { build(:user) }
|
||||||
@ -37,30 +38,30 @@ describe Guardian do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#user_can_access_query?" do
|
describe "#group_and_user_can_access_query?" do
|
||||||
let(:group) { Fabricate(:group) }
|
let(:group) { Fabricate(:group) }
|
||||||
|
|
||||||
it "is true if the user is an admin" do
|
it "is true if the user is an admin" do
|
||||||
expect(Guardian.new(admin).user_can_access_query?(group, make_query)).to eq(true)
|
expect(Guardian.new(admin).group_and_user_can_access_query?(group, make_query)).to eq(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "is true if the user is a member of the group, and query contains the group id" do
|
it "is true if the user is a member of the group, and query contains the group id" do
|
||||||
query = make_query(["#{group.id}"])
|
query = make_query(["#{group.id}"])
|
||||||
group.add(user)
|
group.add(user)
|
||||||
|
|
||||||
expect(Guardian.new(user).user_can_access_query?(group, query)).to eq(true)
|
expect(Guardian.new(user).group_and_user_can_access_query?(group, query)).to eq(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "is false if the query does not contain the group id" do
|
it "is false if the query does not contain the group id" do
|
||||||
group.add(user)
|
group.add(user)
|
||||||
|
|
||||||
expect(Guardian.new(user).user_can_access_query?(group, make_query)).to eq(false)
|
expect(Guardian.new(user).group_and_user_can_access_query?(group, make_query)).to eq(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "is false if the user is not member of the group" do
|
it "is false if the user is not member of the group" do
|
||||||
query = make_query(["#{group.id}"])
|
query = make_query(["#{group.id}"])
|
||||||
|
|
||||||
expect(Guardian.new(user).user_can_access_query?(group, query)).to eq(false)
|
expect(Guardian.new(user).group_and_user_can_access_query?(group, query)).to eq(false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -13,25 +13,15 @@ describe 'Data Explorer rake tasks' do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def make_query(sql, opts = {}, group_ids = [])
|
def make_query(sql, opts = {}, group_ids = [])
|
||||||
q = DataExplorer::Query.new
|
query = DataExplorer::Query.create!(id: opts[:id], name: opts[:name] || "Query number", description: "A description for query number", sql: sql, hidden: opts[:hidden] || false)
|
||||||
q.id = opts[:id] || Fabrication::Sequencer.sequence("query-id", 1)
|
group_ids.each do |group_id|
|
||||||
q.name = opts[:name] || "Query number #{q.id}"
|
query.query_groups.create!(group_id: group_id)
|
||||||
q.description = "A description for query number #{q.id}"
|
end
|
||||||
q.group_ids = group_ids
|
query
|
||||||
q.sql = sql
|
|
||||||
q.hidden = opts[:hidden] || false
|
|
||||||
q.save
|
|
||||||
q
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def hidden_queries
|
def hidden_queries
|
||||||
hidden_queries = []
|
DataExplorer::Query.where(hidden: true)
|
||||||
|
|
||||||
DataExplorer::Query.all.each do |query|
|
|
||||||
hidden_queries.push(query) if query.hidden
|
|
||||||
end
|
|
||||||
|
|
||||||
hidden_queries
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'data_explorer' do
|
describe 'data_explorer' do
|
||||||
|
Loading…
x
Reference in New Issue
Block a user