DEV: Introduce syntax_tree for ruby formatting (#208)

This commit is contained in:
David Taylor 2022-12-29 12:31:29 +00:00 committed by GitHub
parent ac6b0467a1
commit 148d6c32a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1064 additions and 897 deletions

View File

@ -55,3 +55,12 @@ jobs:
- name: Rubocop
if: ${{ !cancelled() }}
run: bundle exec rubocop .
- name: Syntax Tree
if: ${{ !cancelled() }}
run: |
if test -f .streerc; then
bundle exec stree check Gemfile $(git ls-files '*.rb') $(git ls-files '*.rake')
else
echo "Stree config not detected for this repository. Skipping."
fi

View File

@ -80,7 +80,7 @@ jobs:
- name: Get yarn cache directory
id: yarn-cache-dir
run: echo "::set-output name=dir::$(yarn cache dir)"
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- name: Yarn cache
uses: actions/cache@v3
@ -130,7 +130,7 @@ jobs:
shell: bash
run: |
if [ 0 -lt $(find plugins/${{ github.event.repository.name }}/spec -type f -name "*.rb" 2> /dev/null | wc -l) ]; then
echo "::set-output name=files_exist::true"
echo "files_exist=true" >> $GITHUB_OUTPUT
fi
- name: Plugin RSpec
@ -142,7 +142,7 @@ jobs:
shell: bash
run: |
if [ 0 -lt $(find plugins/${{ github.event.repository.name }}/test/javascripts -type f \( -name "*.js" -or -name "*.es6" \) 2> /dev/null | wc -l) ]; then
echo "::set-output name=files_exist::true"
echo "files_exist=true" >> $GITHUB_OUTPUT
fi
- name: Plugin QUnit

View File

@ -1,2 +1,2 @@
inherit_gem:
rubocop-discourse: default.yml
rubocop-discourse: stree-compat.yml

2
.streerc Normal file
View File

@ -0,0 +1,2 @@
--print-width=100
--plugins=plugin/trailing_comma

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true
source 'https://rubygems.org'
source "https://rubygems.org"
group :development do
gem 'rubocop-discourse'
gem "rubocop-discourse"
gem "syntax_tree"
end

View File

@ -6,6 +6,7 @@ GEM
parallel (1.22.1)
parser (3.1.2.1)
ast (~> 2.4.1)
prettier_print (1.2.0)
rainbow (3.1.1)
regexp_parser (2.6.0)
rexml (3.2.5)
@ -27,6 +28,8 @@ GEM
rubocop-rspec (2.13.2)
rubocop (~> 1.33)
ruby-progressbar (1.11.0)
syntax_tree (5.1.0)
prettier_print (>= 1.2.0)
unicode-display_width (2.3.0)
PLATFORMS
@ -39,6 +42,7 @@ PLATFORMS
DEPENDENCIES
rubocop-discourse
syntax_tree
BUNDLED WITH
2.3.10

View File

@ -3,19 +3,17 @@
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)
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]
before_action :ensure_admin
skip_before_action :check_xhr, only: %i(show group_reports_run run)
skip_before_action :ensure_admin, only: %i(
group_reports_index
group_reports_show
group_reports_run
)
skip_before_action :check_xhr, only: %i[show group_reports_run run]
skip_before_action :ensure_admin,
only: %i[group_reports_index group_reports_show group_reports_run]
def index
queries = DataExplorer::Query.where(hidden: false).order(:last_run_at, :name).includes(:groups).to_a
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|
@ -30,19 +28,19 @@ class DataExplorer::QueryController < ::ApplicationController
queries << query
end
render_serialized queries, DataExplorer::QuerySerializer, root: 'queries'
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.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'
render_serialized @query, DataExplorer::QuerySerializer, root: "query"
end
def groups
@ -55,13 +53,15 @@ class DataExplorer::QueryController < ::ApplicationController
respond_to do |format|
format.json do
queries = DataExplorer::Query.for_group(@group)
render_serialized(queries, DataExplorer::QuerySerializer, root: 'queries')
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
if !guardian.group_and_user_can_access_query?(@group, @query) || @query.hidden
return raise Discourse::NotFound
end
respond_to do |format|
format.json do
@ -69,26 +69,32 @@ class DataExplorer::QueryController < ::ApplicationController
render json: {
query: serialize_data(@query, DataExplorer::QuerySerializer, root: nil),
query_group: serialize_data(query_group, DataExplorer::QueryGroupSerializer, root: nil),
query_group:
serialize_data(query_group, DataExplorer::QueryGroupSerializer, root: nil),
}
end
end
end
def group_reports_run
return raise Discourse::NotFound if !guardian.group_and_user_can_access_query?(@group, @query) || @query.hidden
if !guardian.group_and_user_can_access_query?(@group, @query) || @query.hidden
return raise Discourse::NotFound
end
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))
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'
group_ids&.each { |group_id| query.query_groups.find_or_create_by!(group_id: group_id) }
render_serialized query, DataExplorer::QuerySerializer, root: "query"
end
def update
@ -97,12 +103,10 @@ class DataExplorer::QueryController < ::ApplicationController
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
group_ids&.each { |group_id| @query.query_groups.find_or_create_by!(group_id: group_id) }
end
render_serialized @query, DataExplorer::QuerySerializer, root: 'query'
render_serialized @query, DataExplorer::QuerySerializer, root: "query"
rescue DataExplorer::ValidationError => e
render_json_error e.message
end
@ -116,9 +120,7 @@ class DataExplorer::QueryController < ::ApplicationController
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
render json: DataExplorer.schema if stale?(public: true, etag: schema_version, template: false)
end
# Return value:
@ -135,9 +137,7 @@ class DataExplorer::QueryController < ::ApplicationController
query = DataExplorer::Query.find(params[:id].to_i)
query.update!(last_run_at: Time.now)
if params[:download]
response.sending_file = true
end
response.sending_file = true if params[:download]
query_params = {}
query_params = MultiJson.load(params[:params]) if params[:params]
@ -145,8 +145,7 @@ class DataExplorer::QueryController < ::ApplicationController
opts = { current_user: current_user.username }
opts[:explain] = true if params[:explain] == "true"
opts[:limit] =
if params[:format] == "csv"
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
@ -168,23 +167,21 @@ class DataExplorer::QueryController < ::ApplicationController
err_msg = err.message
if err.is_a? ActiveRecord::StatementInvalid
err_class = err.original_exception.class
err_msg.gsub!("#{err_class}:", '')
err_msg.gsub!("#{err_class}:", "")
else
err_msg = "#{err_class}: #{err_msg}"
end
render json: {
success: false,
errors: [err_msg]
}, status: 422
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"
response.headers[
"Content-Disposition"
] = "attachment; filename=#{query.slug}@#{Slug.for(Discourse.current_hostname, "discourse")}-#{Date.today}.dcqresult.json"
end
json = {
success: true,
@ -193,7 +190,7 @@ class DataExplorer::QueryController < ::ApplicationController
result_count: pg_result.values.length || 0,
params: query_params,
columns: cols,
default_limit: SiteSetting.data_explorer_query_result_limit
default_limit: SiteSetting.data_explorer_query_result_limit,
}
json[:explain] = result[:explain] if opts[:explain]
@ -208,15 +205,15 @@ class DataExplorer::QueryController < ::ApplicationController
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"
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|
require "csv"
text =
CSV.generate do |csv|
csv << cols
pg_result.values.each do |row|
csv << row
end
pg_result.values.each { |row| csv << row }
end
render plain: text

View File

@ -7,9 +7,13 @@ module Jobs
def execute(args)
return unless SiteSetting.data_explorer_enabled
DataExplorer::Query.where("id > 0")
DataExplorer::Query
.where("id > 0")
.where(hidden: true)
.where("(last_run_at IS NULL OR last_run_at < :days_ago) AND updated_at < :days_ago", days_ago: 7.days.ago)
.where(
"(last_run_at IS NULL OR last_run_at < :days_ago) AND updated_at < :days_ago",
days_ago: 7.days.ago,
)
.delete_all
end
end

View File

@ -2,18 +2,20 @@
module DataExplorer
class Query < ActiveRecord::Base
self.table_name = 'data_explorer_queries'
self.table_name = "data_explorer_queries"
has_many :query_groups
has_many :groups, through: :query_groups
belongs_to :user
validates :name, presence: true
scope :for_group, ->(group) do
where(hidden: false)
.joins("INNER JOIN data_explorer_query_groups
scope :for_group,
->(group) {
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}")
end
AND data_explorer_query_groups.group_id = #{group.id}",
)
}
def params
@params ||= DataExplorer::Parameter.create_from_sql(sql)

View File

@ -2,7 +2,7 @@
module DataExplorer
class QueryGroup < ActiveRecord::Base
self.table_name = 'data_explorer_query_groups'
self.table_name = "data_explorer_query_groups"
belongs_to :query
belongs_to :group

View File

@ -1,8 +1,10 @@
# frozen_string_literal: true
class DataExplorer::QueryGroupSerializer < ActiveModel::Serializer
attributes :id, :group_id, :query_id, :bookmark,
attributes :id,
:group_id,
:query_id,
:bookmark,
def query_group_bookmark
@query_group_bookmark ||= Bookmark.find_by(user: scope.user, bookmarkable: object)
end
@ -18,8 +20,7 @@ class DataExplorer::QueryGroupSerializer < ActiveModel::Serializer
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
bookmarkable_type: query_group_bookmark.bookmarkable_type,
}
end
end

View File

@ -1,7 +1,17 @@
# frozen_string_literal: true
class DataExplorer::QuerySerializer < ActiveModel::Serializer
attributes :id, :sql, :name, :description, :param_info, :created_at, :username, :group_ids, :last_run_at, :hidden, :user_id
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)

View File

@ -18,7 +18,7 @@ class CreateDataExplorerQueries < ActiveRecord::Migration[6.0]
t.index :query_id
t.index :group_id
end
add_index(:data_explorer_query_groups, [:query_id, :group_id], unique: true)
add_index(:data_explorer_query_groups, %i[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)
@ -56,13 +56,25 @@ class CreateDataExplorerQueries < ActiveRecord::Migration[6.0]
WHERE plugin_name = 'discourse-data-explorer' AND type_name = 'JSON'
SQL
DB.query("SELECT * FROM plugin_store_rows WHERE plugin_name = 'discourse-data-explorer' AND type_name = 'JSON'").each do |row|
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
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|
json["group_ids"].each do |group_id|
next if group_id.blank? || query_id.blank?
DB.exec <<~SQL
INSERT INTO data_explorer_query_groups(query_id, group_id)

View File

@ -2,7 +2,7 @@
class FixQueryIds < ActiveRecord::Migration[6.0]
def up
Rake::Task['data_explorer:fix_query_ids'].invoke
Rake::Task["data_explorer:fix_query_ids"].invoke
end
def down

View File

@ -10,7 +10,7 @@ class DataExplorerQueryGroupBookmarkable < BaseBookmarkable
end
def self.preload_associations
[:data_explorer_queries, :groups]
%i[data_explorer_queries groups]
end
def self.list_query(user, guardian)
@ -20,17 +20,22 @@ class DataExplorerQueryGroupBookmarkable < BaseBookmarkable
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 =
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")
bookmarkable_search.call(bookmarks, "data_explorer_queries.name ILIKE :q")
end
def self.reminder_handler(bookmark)
@ -38,8 +43,9 @@ class DataExplorerQueryGroupBookmarkable < BaseBookmarkable
bookmark,
data: {
title: bookmark.bookmarkable.query.name,
bookmarkable_url: "/g/#{bookmark.bookmarkable.group.name}/reports/#{bookmark.bookmarkable.query.id}"
}
bookmarkable_url:
"/g/#{bookmark.bookmarkable.group.name}/reports/#{bookmark.bookmarkable.query.id}",
},
)
end
@ -51,5 +57,4 @@ class DataExplorerQueryGroupBookmarkable < BaseBookmarkable
return false if !bookmark.bookmarkable.group
guardian.user_is_a_member_of_group?(bookmark.bookmarkable.group)
end
end

View File

@ -12,95 +12,103 @@ class Queries
queries = {
"most-common-likers": {
"id": -1,
"name": "Most Common Likers",
"description": "Which users like particular other users the most?"
id: -1,
name: "Most Common Likers",
description: "Which users like particular other users the most?",
},
"most-messages": {
"id": -2,
"name": "Who has been sending the most messages in the last week?",
"description": "tracking down suspicious PM activity"
id: -2,
name: "Who has been sending the most messages in the last week?",
description: "tracking down suspicious PM activity",
},
"edited-post-spam": {
"id": -3,
"name": "Last 500 posts that were edited by TL0/TL1 users",
"description": "fighting human-driven copy-paste spam"
id: -3,
name: "Last 500 posts that were edited by TL0/TL1 users",
description: "fighting human-driven copy-paste spam",
},
"new-topics": {
"id": -4,
"name": "New Topics by Category",
"description": "Lists all new topics ordered by category and creation_date. The query accepts a months_ago parameter. It defaults to 0 to give you the stats for the current month."
id: -4,
name: "New Topics by Category",
description:
"Lists all new topics ordered by category and creation_date. The query accepts a months_ago parameter. It defaults to 0 to give you the stats for the current month.",
},
"active-topics": {
"id": -5,
"name": "Top 100 Active Topics",
"description": "based on the number of replies, it accepts a months_ago parameter, defaults to 1 to give results for the last calendar month."
id: -5,
name: "Top 100 Active Topics",
description:
"based on the number of replies, it accepts a months_ago parameter, defaults to 1 to give results for the last calendar month.",
},
"top-likers": {
"id": -6,
"name": "Top 100 Likers",
"description": "returns the top 100 likers for a given monthly period ordered by like_count. It accepts a months_ago parameter, defaults to 1 to give results for the last calendar month."
id: -6,
name: "Top 100 Likers",
description:
"returns the top 100 likers for a given monthly period ordered by like_count. It accepts a months_ago parameter, defaults to 1 to give results for the last calendar month.",
},
"quality-users": {
"id": -7,
"name": "Top 50 Quality Users",
"description": "based on post score calculated using reply count, likes, incoming links, bookmarks, time spent and read count."
id: -7,
name: "Top 50 Quality Users",
description:
"based on post score calculated using reply count, likes, incoming links, bookmarks, time spent and read count.",
},
"user-participation": {
"id": -8,
"name": "User Participation Statistics",
"description": "Detailed statistics for the most active users."
id: -8,
name: "User Participation Statistics",
description: "Detailed statistics for the most active users.",
},
"largest-uploads": {
"id": -9,
"name": "Top 50 Largest Uploads",
"description": "sorted by file size."
id: -9,
name: "Top 50 Largest Uploads",
description: "sorted by file size.",
},
"inactive-users": {
"id": -10,
"name": "Inactive Users with no posts",
"description": "analyze pre-Discourse signups."
id: -10,
name: "Inactive Users with no posts",
description: "analyze pre-Discourse signups.",
},
"active-lurkers": {
"id": -11,
"name": "Most Active Lurkers",
"description": "active users without posts and excessive read times, it accepts a post_read_count parameter that sets the threshold for posts read."
id: -11,
name: "Most Active Lurkers",
description:
"active users without posts and excessive read times, it accepts a post_read_count parameter that sets the threshold for posts read.",
},
"topic-user-notification-level": {
"id": -12,
"name": "List of topics a user is watching/tracking/muted",
"description": "The query requires a notification_level parameter. Use 0 for muted, 1 for regular, 2 for tracked and 3 for watched topics."
id: -12,
name: "List of topics a user is watching/tracking/muted",
description:
"The query requires a notification_level parameter. Use 0 for muted, 1 for regular, 2 for tracked and 3 for watched topics.",
},
"assigned-topics-report": {
"id": -13,
"name": "List of assigned topics by user",
"description": "This report requires the assign plugin, it will find all assigned topics"
id: -13,
name: "List of assigned topics by user",
description: "This report requires the assign plugin, it will find all assigned topics",
},
"group-members-reply-count": {
"id": -14,
"name": "Group Members Reply Count",
"description": "Number of replies by members of a group over a given time period. Requires 'group_name', 'start_date', and 'end_date' parameters. Dates need to be in the form 'yyyy-mm-dd'. Accepts an 'include_pms' parameter."
id: -14,
name: "Group Members Reply Count",
description:
"Number of replies by members of a group over a given time period. Requires 'group_name', 'start_date', and 'end_date' parameters. Dates need to be in the form 'yyyy-mm-dd'. Accepts an 'include_pms' parameter.",
},
"total-assigned-topics-report": {
"id": -15,
"name": "Total topics assigned per user",
"description": "Count of assigned topis per user linking to assign list"
id: -15,
name: "Total topics assigned per user",
description: "Count of assigned topis per user linking to assign list",
},
"poll-results": {
"id": -16,
"name": "Poll results report",
"description": "Details of a poll result, including details about each vote and voter, useful for analyzing results in external software."
id: -16,
name: "Poll results report",
description:
"Details of a poll result, including details about each vote and voter, useful for analyzing results in external software.",
},
"top-tags-per-year": {
"id": -17,
"name": "Top tags per year",
"description": "List the top tags per year."
id: -17,
name: "Top tags per year",
description: "List the top tags per year.",
},
number_of_replies_by_category: {
id: -18,
name: "Number of replies by category",
description: "List the number of replies by category.",
},
"number_of_replies_by_category": {
"id": -18,
"name": "Number of replies by category",
"description": "List the number of replies by category."
}
}.with_indifferent_access
queries["most-common-likers"]["sql"] = <<~SQL

View File

@ -2,8 +2,8 @@
# rake data_explorer:list_hidden_queries
desc "Shows a list of hidden queries"
task('data_explorer:list_hidden_queries').clear
task 'data_explorer:list_hidden_queries' => :environment do |t|
task("data_explorer:list_hidden_queries").clear
task "data_explorer:list_hidden_queries" => :environment do |t|
puts "\nHidden Queries\n\n"
hidden_queries = DataExplorer::Query.where(hidden: false)
@ -18,8 +18,8 @@ end
# rake data_explorer[-1]
# rake data_explorer[1,-2,3,-4,5]
desc "Hides one or multiple queries by ID"
task('data_explorer').clear
task 'data_explorer' => :environment do |t, args|
task("data_explorer").clear
task "data_explorer" => :environment do |t, args|
args.extras.each do |arg|
id = arg.to_i
query = DataExplorer::Query.find_by(id: id)
@ -37,8 +37,8 @@ end
# rake data_explorer:unhide_query[-1]
# rake data_explorer:unhide_query[1,-2,3,-4,5]
desc "Unhides one or multiple queries by ID"
task('data_explorer:unhide_query').clear
task 'data_explorer:unhide_query' => :environment do |t, args|
task("data_explorer:unhide_query").clear
task "data_explorer:unhide_query" => :environment do |t, args|
args.extras.each do |arg|
id = arg.to_i
query = DataExplorer::Query.find_by(id: id)
@ -56,8 +56,8 @@ end
# rake data_explorer:hard_delete[-1]
# rake data_explorer:hard_delete[1,-2,3,-4,5]
desc "Hard deletes one or multiple queries by ID"
task('data_explorer:hard_delete').clear
task 'data_explorer:hard_delete' => :environment do |t, args|
task("data_explorer:hard_delete").clear
task "data_explorer:hard_delete" => :environment do |t, args|
args.extras.each do |arg|
id = arg.to_i
query = DataExplorer::Query.find_by(id: id)

View File

@ -1,9 +1,9 @@
# frozen_string_literal: true
desc 'Fix query IDs to match the old ones used in the plugin store (q:id)'
desc "Fix query IDs to match the old ones used in the plugin store (q:id)"
task('data_explorer:fix_query_ids').clear
task 'data_explorer:fix_query_ids' => :environment do
task("data_explorer:fix_query_ids").clear
task "data_explorer:fix_query_ids" => :environment do
ActiveRecord::Base.transaction do
# Only queries with unique title can be fixed
movements = DB.query <<~SQL
@ -20,7 +20,8 @@ task 'data_explorer:fix_query_ids' => :environment do
if movements.present?
# If there are new queries, they still may have conflict
# We just want to move their ids to safe space and we will not move them back
additional_conflicts = DB.query(<<~SQL, from: movements.map { |m| m.from }, to: movements.map { |m| m.to })
additional_conflicts =
DB.query(<<~SQL, from: movements.map { |m| m.from }, to: movements.map { |m| m.to })
SELECT id FROM data_explorer_queries
WHERE id IN (:to)
AND id NOT IN (:from)
@ -84,7 +85,8 @@ task 'data_explorer:fix_query_ids' => :environment do
SQL
# insert additional_conflicts to temporary tables
new_id = DB.query("select greatest(max(id), 1) from tmp_data_explorer_queries").first.greatest + 1
new_id =
DB.query("select greatest(max(id), 1) from tmp_data_explorer_queries").first.greatest + 1
additional_conflicts.each do |conflict_id|
DB.exec <<-SQL
INSERT INTO tmp_data_explorer_queries(id, name, description, sql, user_id, last_run_at, hidden, created_at, updated_at)

740
plugin.rb

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,10 @@
# frozen_string_literal: true
describe DataExplorer do
describe '.run_query' do
describe ".run_query" do
fab!(:topic) { Fabricate(:topic) }
it 'should run a query that includes PG template patterns' do
it "should run a query that includes PG template patterns" do
sql = <<~SQL
WITH query AS (
SELECT TO_CHAR(created_at, 'yyyy:mm:dd') AS date FROM topics
@ -19,7 +19,7 @@ describe DataExplorer do
expect(result[:pg_result][0]["date"]).to eq(topic.created_at.strftime("%Y:%m:%d"))
end
it 'should run a query containing a question mark in the comment' do
it "should run a query containing a question mark in the comment" do
sql = <<~SQL
WITH query AS (
SELECT id FROM topics -- some SQL ? comment ?
@ -34,7 +34,7 @@ describe DataExplorer do
expect(result[:pg_result][0]["id"]).to eq(topic.id)
end
it 'can run a query with params interpolation' do
it "can run a query with params interpolation" do
topic2 = Fabricate(:topic)
sql = <<~SQL

View File

@ -1,16 +1,18 @@
# frozen_string_literal: true
require 'rails_helper'
require "rails_helper"
describe Guardian do
before { SiteSetting.data_explorer_enabled = true }
def make_query(group_ids = [])
query = DataExplorer::Query.create!(name: "Query number #{Fabrication::Sequencer.sequence("query-id", 1)}", sql: "SELECT 1")
query =
DataExplorer::Query.create!(
name: "Query number #{Fabrication::Sequencer.sequence("query-id", 1)}",
sql: "SELECT 1",
)
group_ids.each do |group_id|
query.query_groups.create!(group_id: group_id)
end
group_ids.each { |group_id| query.query_groups.create!(group_id: group_id) }
query
end

View File

@ -1,11 +1,9 @@
# frozen_string_literal: true
require 'rails_helper'
require "rails_helper"
describe 'API keys scoped to query#run' do
before do
SiteSetting.data_explorer_enabled = true
end
describe "API keys scoped to query#run" do
before { SiteSetting.data_explorer_enabled = true }
fab!(:query1) { DataExplorer::Query.create!(name: "Query 1", sql: "SELECT 1 AS query1_res") }
fab!(:query2) { DataExplorer::Query.create!(name: "Query 2", sql: "SELECT 1 AS query2_res") }
@ -13,11 +11,7 @@ describe 'API keys scoped to query#run' do
let(:all_queries_api_key) do
key = ApiKey.create!
ApiKeyScope.create!(
resource: "data_explorer",
action: "run_queries",
api_key_id: key.id
)
ApiKeyScope.create!(resource: "data_explorer", action: "run_queries", api_key_id: key.id)
key
end
@ -27,42 +21,49 @@ describe 'API keys scoped to query#run' do
resource: "data_explorer",
action: "run_queries",
api_key_id: key.id,
allowed_parameters: { "id" => [query1.id.to_s] }
allowed_parameters: {
"id" => [query1.id.to_s],
},
)
key
end
it 'cannot hit any other endpoints' do
get "/latest.json", headers: {
it "cannot hit any other endpoints" do
get "/latest.json",
headers: {
"Api-Key" => all_queries_api_key.key,
"Api-Username" => admin.username
"Api-Username" => admin.username,
}
expect(response.status).to eq(403)
get "/latest.json", headers: {
get "/latest.json",
headers: {
"Api-Key" => single_query_api_key.key,
"Api-Username" => admin.username
"Api-Username" => admin.username,
}
expect(response.status).to eq(403)
get "/u/#{admin.username}.json", headers: {
get "/u/#{admin.username}.json",
headers: {
"Api-Key" => all_queries_api_key.key,
"Api-Username" => admin.username
"Api-Username" => admin.username,
}
expect(response.status).to eq(403)
get "/u/#{admin.username}.json", headers: {
get "/u/#{admin.username}.json",
headers: {
"Api-Key" => single_query_api_key.key,
"Api-Username" => admin.username
"Api-Username" => admin.username,
}
expect(response.status).to eq(403)
end
it "can only run the queries they're allowed to run" do
expect {
post "/admin/plugins/explorer/queries/#{query1.id}/run.json", headers: {
post "/admin/plugins/explorer/queries/#{query1.id}/run.json",
headers: {
"Api-Key" => single_query_api_key.key,
"Api-Username" => admin.username
"Api-Username" => admin.username,
}
}.to change { query1.reload.last_run_at }
expect(response.status).to eq(200)
@ -70,9 +71,10 @@ describe 'API keys scoped to query#run' do
expect(response.parsed_body["columns"]).to eq(["query1_res"])
expect {
post "/admin/plugins/explorer/queries/#{query2.id}/run.json", headers: {
post "/admin/plugins/explorer/queries/#{query2.id}/run.json",
headers: {
"Api-Key" => single_query_api_key.key,
"Api-Username" => admin.username
"Api-Username" => admin.username,
}
}.not_to change { query2.reload.last_run_at }
expect(response.status).to eq(403)
@ -80,9 +82,10 @@ describe 'API keys scoped to query#run' do
it "can run all queries if they're not restricted to any queries" do
expect {
post "/admin/plugins/explorer/queries/#{query1.id}/run.json", headers: {
post "/admin/plugins/explorer/queries/#{query1.id}/run.json",
headers: {
"Api-Key" => all_queries_api_key.key,
"Api-Username" => admin.username
"Api-Username" => admin.username,
}
}.to change { query1.reload.last_run_at }
expect(response.status).to eq(200)
@ -90,9 +93,10 @@ describe 'API keys scoped to query#run' do
expect(response.parsed_body["columns"]).to eq(["query1_res"])
expect {
post "/admin/plugins/explorer/queries/#{query2.id}/run.json", headers: {
post "/admin/plugins/explorer/queries/#{query2.id}/run.json",
headers: {
"Api-Key" => all_queries_api_key.key,
"Api-Username" => admin.username
"Api-Username" => admin.username,
}
}.to change { query2.reload.last_run_at }
expect(response.status).to eq(200)

View File

@ -9,12 +9,60 @@ describe Jobs::DeleteHiddenQueries do
end
it "will correctly destroy old hidden queries" do
DataExplorer::Query.create!(id: 1, name: "A", description: "A description for A", sql: "SELECT 1 as value", hidden: false, last_run_at: 2.days.ago, updated_at: 2.days.ago)
DataExplorer::Query.create!(id: 2, name: "B", description: "A description for B", sql: "SELECT 1 as value", hidden: true, last_run_at: 8.days.ago, updated_at: 8.days.ago)
DataExplorer::Query.create!(id: 3, name: "C", description: "A description for C", sql: "SELECT 1 as value", hidden: true, last_run_at: 4.days.ago, updated_at: 4.days.ago)
DataExplorer::Query.create!(id: 4, name: "D", description: "A description for D", sql: "SELECT 1 as value", hidden: true, last_run_at: nil, updated_at: 10.days.ago)
DataExplorer::Query.create!(id: 5, name: "E", description: "A description for E", sql: "SELECT 1 as value", hidden: true, last_run_at: 5.days.ago, updated_at: 10.days.ago)
DataExplorer::Query.create!(id: 6, name: "F", description: "A description for F", sql: "SELECT 1 as value", hidden: true, last_run_at: 10.days.ago, updated_at: 5.days.ago)
DataExplorer::Query.create!(
id: 1,
name: "A",
description: "A description for A",
sql: "SELECT 1 as value",
hidden: false,
last_run_at: 2.days.ago,
updated_at: 2.days.ago,
)
DataExplorer::Query.create!(
id: 2,
name: "B",
description: "A description for B",
sql: "SELECT 1 as value",
hidden: true,
last_run_at: 8.days.ago,
updated_at: 8.days.ago,
)
DataExplorer::Query.create!(
id: 3,
name: "C",
description: "A description for C",
sql: "SELECT 1 as value",
hidden: true,
last_run_at: 4.days.ago,
updated_at: 4.days.ago,
)
DataExplorer::Query.create!(
id: 4,
name: "D",
description: "A description for D",
sql: "SELECT 1 as value",
hidden: true,
last_run_at: nil,
updated_at: 10.days.ago,
)
DataExplorer::Query.create!(
id: 5,
name: "E",
description: "A description for E",
sql: "SELECT 1 as value",
hidden: true,
last_run_at: 5.days.ago,
updated_at: 10.days.ago,
)
DataExplorer::Query.create!(
id: 6,
name: "F",
description: "A description for F",
sql: "SELECT 1 as value",
hidden: true,
last_run_at: 10.days.ago,
updated_at: 5.days.ago,
)
subject.execute(nil)
expect(DataExplorer::Query.all.length).to eq(4)

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require "rails_helper"
describe DataExplorerQueryGroupBookmarkable do
fab!(:admin_user) { Fabricate(:admin) }
@ -10,14 +10,24 @@ describe DataExplorerQueryGroupBookmarkable do
fab!(:group1) { Fabricate(:group) }
fab!(:group2) { Fabricate(:group) }
fab!(:group3) { Fabricate(:group) }
fab!(:query1) { Fabricate(:query, name: "My First Query",
fab!(:query1) do
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",
user: admin_user,
)
end
fab!(:query2) do
Fabricate(
:query,
name: "My Second Query",
description: "This is my 2nd query's description.",
sql: "Not really important",
user: admin_user) }
user: admin_user,
)
end
before do
SiteSetting.data_explorer_enabled = true
@ -41,28 +51,40 @@ describe DataExplorerQueryGroupBookmarkable do
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,
let!(:bookmark1) do
Fabricate(:bookmark, user: user, bookmarkable: query_group1, name: "something i gotta do")
end
let!(:bookmark2) do
Fabricate(
:bookmark,
user: user,
bookmarkable: query_group2,
name: "something else i have to do") }
name: "something else i have to do",
)
end
# User also bookmarked Query 2 from Group 1.
let!(:bookmark3) { Fabricate(:bookmark, user: user,
let!(:bookmark3) do
Fabricate(
:bookmark,
user: user,
bookmarkable: query_group3,
name: "this is the other query I needed.") }
name: "this is the other query I needed.",
)
end
# 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") }
let!(:bookmark4) do
Fabricate(:bookmark, user: user, bookmarkable: query_group4, name: "something i gotta do also")
end
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])
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
@ -75,24 +97,32 @@ describe DataExplorerQueryGroupBookmarkable do
# 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
before { SearchIndexer.enable }
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])
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])
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
@ -106,17 +136,20 @@ describe DataExplorerQueryGroupBookmarkable do
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)
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,
bookmarkable_url: "/g/#{bookmark1.bookmarkable.group.name}/reports/#{bookmark1.bookmarkable.query.id}",
bookmarkable_url:
"/g/#{bookmark1.bookmarkable.group.name}/reports/#{bookmark1.bookmarkable.query.id}",
display_username: bookmark1.user.username,
bookmark_name: bookmark1.name,
bookmark_id: bookmark1.id
}.to_json
bookmark_id: bookmark1.id,
}.to_json,
)
end
end
@ -144,6 +177,5 @@ describe DataExplorerQueryGroupBookmarkable do
expect(subject.can_see?(guardian, bookmark4)).to eq(true)
end
end
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require "rails_helper"
describe "Data explorer group serializer additions" do
fab!(:group_user) { Fabricate(:user) }

View File

@ -1,37 +1,35 @@
# frozen_string_literal: true
require 'rails_helper'
require "rails_helper"
describe DataExplorer::QueryController do
def response_json
response.parsed_body
end
before do
SiteSetting.data_explorer_enabled = true
end
before { SiteSetting.data_explorer_enabled = true }
def make_query(sql, opts = {}, group_ids = [])
query = DataExplorer::Query.create!(name: opts[:name] || "Query number", description: "A description for query number", sql: sql, hidden: opts[:hidden] || false)
group_ids.each do |group_id|
query.query_groups.create!(group_id: group_id)
end
query =
DataExplorer::Query.create!(
name: opts[:name] || "Query number",
description: "A description for query number",
sql: sql,
hidden: opts[:hidden] || false,
)
group_ids.each { |group_id| query.query_groups.create!(group_id: group_id) }
query
end
describe "Admin" do
fab!(:admin) { Fabricate(:admin) }
before do
sign_in(admin)
end
before { sign_in(admin) }
describe "when disabled" do
before do
SiteSetting.data_explorer_enabled = false
end
before { SiteSetting.data_explorer_enabled = false }
it 'denies every request' do
it "denies every request" do
get "/admin/plugins/explorer/queries.json"
expect(response.status).to eq(404)
@ -41,9 +39,7 @@ describe DataExplorer::QueryController do
get "/admin/plugins/explorer/queries/3.json"
expect(response.status).to eq(404)
post "/admin/plugins/explorer/queries.json", params: {
id: 3
}
post "/admin/plugins/explorer/queries.json", params: { id: 3 }
expect(response.status).to eq(404)
post "/admin/plugins/explorer/queries/3/run.json"
@ -62,28 +58,28 @@ describe DataExplorer::QueryController do
DataExplorer::Query.destroy_all
get "/admin/plugins/explorer/queries.json"
expect(response.status).to eq(200)
expect(response_json['queries'].count).to eq(Queries.default.count)
expect(response_json["queries"].count).to eq(Queries.default.count)
end
it "shows all available queries in alphabetical order" do
DataExplorer::Query.destroy_all
make_query('SELECT 1 as value', name: 'B')
make_query('SELECT 1 as value', name: 'A')
make_query("SELECT 1 as value", name: "B")
make_query("SELECT 1 as value", name: "A")
get "/admin/plugins/explorer/queries.json"
expect(response.status).to eq(200)
expect(response_json['queries'].length).to eq(Queries.default.count + 2)
expect(response_json['queries'][0]['name']).to eq('A')
expect(response_json['queries'][1]['name']).to eq('B')
expect(response_json["queries"].length).to eq(Queries.default.count + 2)
expect(response_json["queries"][0]["name"]).to eq("A")
expect(response_json["queries"][1]["name"]).to eq("B")
end
it "doesn't show hidden/deleted queries" do
DataExplorer::Query.destroy_all
make_query('SELECT 1 as value', name: 'A', hidden: false)
make_query('SELECT 1 as value', name: 'B', hidden: true)
make_query('SELECT 1 as value', name: 'C', hidden: true)
make_query("SELECT 1 as value", name: "A", hidden: false)
make_query("SELECT 1 as value", name: "B", hidden: true)
make_query("SELECT 1 as value", name: "C", hidden: true)
get "/admin/plugins/explorer/queries.json"
expect(response.status).to eq(200)
expect(response_json['queries'].length).to eq(Queries.default.count + 1)
expect(response_json["queries"].length).to eq(Queries.default.count + 1)
end
end
@ -93,7 +89,8 @@ describe DataExplorer::QueryController do
it "allows group to access system query" do
query = DataExplorer::Query.find(-4)
put "/admin/plugins/explorer/queries/#{query.id}.json", params: {
put "/admin/plugins/explorer/queries/#{query.id}.json",
params: {
"query" => {
"name" => query.name,
"description" => query.description,
@ -101,21 +98,23 @@ describe DataExplorer::QueryController do
"user_id" => query.user_id,
"created_at" => query.created_at,
"group_ids" => [group2.id],
"last_run_at" => query.last_run_at
"last_run_at" => query.last_run_at,
},
"id" => query.id }
"id" => query.id,
}
expect(response.status).to eq(200)
end
it "returns a proper json error for invalid updates" do
query = DataExplorer::Query.find(-4)
put "/admin/plugins/explorer/queries/#{query.id}", params: {
put "/admin/plugins/explorer/queries/#{query.id}",
params: {
"query" => {
"name" => "",
},
"id" => query.id }
"id" => query.id,
}
expect(response.status).to eq(422)
expect(response.parsed_body["errors"]).to eq(["Name can't be blank"])
@ -129,13 +128,13 @@ describe DataExplorer::QueryController do
end
it "can run queries" do
query = make_query('SELECT 23 as my_value')
query = make_query("SELECT 23 as my_value")
run_query query.id
expect(response.status).to eq(200)
expect(response_json['success']).to eq(true)
expect(response_json['errors']).to eq([])
expect(response_json['columns']).to eq(['my_value'])
expect(response_json['rows']).to eq([[23]])
expect(response_json["success"]).to eq(true)
expect(response_json["errors"]).to eq([])
expect(response_json["columns"]).to eq(["my_value"])
expect(response_json["rows"]).to eq([[23]])
end
it "can process parameters" do
@ -147,24 +146,24 @@ describe DataExplorer::QueryController do
run_query query.id, foo: 23
expect(response.status).to eq(200)
expect(response_json['errors']).to eq([])
expect(response_json['success']).to eq(true)
expect(response_json['columns']).to eq(['my_value'])
expect(response_json['rows']).to eq([[23]])
expect(response_json["errors"]).to eq([])
expect(response_json["success"]).to eq(true)
expect(response_json["columns"]).to eq(["my_value"])
expect(response_json["rows"]).to eq([[23]])
run_query query.id
expect(response.status).to eq(200)
expect(response_json['errors']).to eq([])
expect(response_json['success']).to eq(true)
expect(response_json['columns']).to eq(['my_value'])
expect(response_json['rows']).to eq([[34]])
expect(response_json["errors"]).to eq([])
expect(response_json["success"]).to eq(true)
expect(response_json["columns"]).to eq(["my_value"])
expect(response_json["rows"]).to eq([[34]])
# 2.3 is not an integer
run_query query.id, foo: '2.3'
run_query query.id, foo: "2.3"
expect(response.status).to eq(422)
expect(response_json['errors']).to_not eq([])
expect(response_json['success']).to eq(false)
expect(response_json['errors'].first).to match(/ValidationError/)
expect(response_json["errors"]).to_not eq([])
expect(response_json["success"]).to eq(false)
expect(response_json["errors"].first).to match(/ValidationError/)
end
it "doesn't allow you to modify the database #1" do
@ -184,9 +183,9 @@ describe DataExplorer::QueryController do
# This test should fail on the below check.
expect(p.cooked).to_not match(/winner/)
expect(response.status).to eq(422)
expect(response_json['errors']).to_not eq([])
expect(response_json['success']).to eq(false)
expect(response_json['errors'].first).to match(/read-only transaction/)
expect(response_json["errors"]).to_not eq([])
expect(response_json["success"]).to eq(false)
expect(response_json["errors"].first).to match(/read-only transaction/)
end
it "doesn't allow you to modify the database #2" do
@ -221,9 +220,9 @@ describe DataExplorer::QueryController do
# Afterwards, this test should fail on the below check.
expect(p.cooked).to_not match(/winner/)
expect(response.status).to eq(422)
expect(response_json['errors']).to_not eq([])
expect(response_json['success']).to eq(false)
expect(response_json['errors'].first).to match(/semicolon/)
expect(response_json["errors"]).to_not eq([])
expect(response_json["success"]).to eq(false)
expect(response_json["errors"].first).to match(/semicolon/)
end
it "doesn't allow you to lock rows" do
@ -233,9 +232,9 @@ describe DataExplorer::QueryController do
run_query query.id
expect(response.status).to eq(422)
expect(response_json['errors']).to_not eq([])
expect(response_json['success']).to eq(false)
expect(response_json['errors'].first).to match(/read-only transaction/)
expect(response_json["errors"]).to_not eq([])
expect(response_json["success"]).to eq(false)
expect(response_json["errors"].first).to match(/read-only transaction/)
end
it "doesn't allow you to create a table" do
@ -245,9 +244,9 @@ describe DataExplorer::QueryController do
run_query query.id
expect(response.status).to eq(422)
expect(response_json['errors']).to_not eq([])
expect(response_json['success']).to eq(false)
expect(response_json['errors'].first).to match(/read-only transaction|syntax error/)
expect(response_json["errors"]).to_not eq([])
expect(response_json["success"]).to eq(false)
expect(response_json["errors"].first).to match(/read-only transaction|syntax error/)
end
it "doesn't allow you to break the transaction" do
@ -257,9 +256,9 @@ describe DataExplorer::QueryController do
run_query query.id
expect(response.status).to eq(422)
expect(response_json['errors']).to_not eq([])
expect(response_json['success']).to eq(false)
expect(response_json['errors'].first).to match(/syntax error/)
expect(response_json["errors"]).to_not eq([])
expect(response_json["success"]).to eq(false)
expect(response_json["errors"].first).to match(/syntax error/)
query.sql = <<~SQL
)
@ -267,9 +266,9 @@ describe DataExplorer::QueryController do
run_query query.id
expect(response.status).to eq(422)
expect(response_json['errors']).to_not eq([])
expect(response_json['success']).to eq(false)
expect(response_json['errors'].first).to match(/syntax error/)
expect(response_json["errors"]).to_not eq([])
expect(response_json["success"]).to eq(false)
expect(response_json["errors"].first).to match(/syntax error/)
query.sql = <<~SQL
RELEASE SAVEPOINT active_record_1
@ -277,13 +276,13 @@ describe DataExplorer::QueryController do
run_query query.id
expect(response.status).to eq(422)
expect(response_json['errors']).to_not eq([])
expect(response_json['success']).to eq(false)
expect(response_json['errors'].first).to match(/syntax error/)
expect(response_json["errors"]).to_not eq([])
expect(response_json["success"]).to eq(false)
expect(response_json["errors"].first).to match(/syntax error/)
end
it "can export data in CSV format" do
query = make_query('SELECT 23 as my_value')
query = make_query("SELECT 23 as my_value")
post "/admin/plugins/explorer/queries/#{query.id}/run.json", params: { download: 1 }
expect(response.status).to eq(200)
end
@ -302,13 +301,13 @@ describe DataExplorer::QueryController do
SQL
run_query query.id
expect(response_json['rows'].count).to eq(2)
expect(response_json["rows"].count).to eq(2)
post "/admin/plugins/explorer/queries/#{query.id}/run.json", params: { limit: 1 }
expect(response_json['rows'].count).to eq(1)
expect(response_json["rows"].count).to eq(1)
post "/admin/plugins/explorer/queries/#{query.id}/run.json", params: { limit: "ALL" }
expect(response_json['rows'].count).to eq(3)
expect(response_json["rows"].count).to eq(3)
end
it "should limit the results in CSV download" do
@ -324,11 +323,19 @@ describe DataExplorer::QueryController do
post "/admin/plugins/explorer/queries/#{query.id}/run.csv", params: { download: 1 }
expect(response.body.split("\n").count).to eq(3)
post "/admin/plugins/explorer/queries/#{query.id}/run.csv", params: { download: 1, limit: 1 }
post "/admin/plugins/explorer/queries/#{query.id}/run.csv",
params: {
download: 1,
limit: 1,
}
expect(response.body.split("\n").count).to eq(2)
# The value `ALL` is not supported in csv exports.
post "/admin/plugins/explorer/queries/#{query.id}/run.csv", params: { download: 1, limit: "ALL" }
post "/admin/plugins/explorer/queries/#{query.id}/run.csv",
params: {
download: 1,
limit: "ALL",
}
expect(response.body.split("\n").count).to eq(1)
ensure
DataExplorer.send(:remove_const, "QUERY_RESULT_MAX_LIMIT")
@ -343,16 +350,12 @@ describe DataExplorer::QueryController do
fab!(:user) { Fabricate(:user) }
fab!(:group) { Fabricate(:group, users: [user]) }
before do
sign_in(user)
end
before { sign_in(user) }
describe "when disabled" do
before do
SiteSetting.data_explorer_enabled = false
end
before { SiteSetting.data_explorer_enabled = false }
it 'denies every request' do
it "denies every request" do
get "/g/1/reports.json"
expect(response.status).to eq(404)
@ -365,7 +368,7 @@ describe DataExplorer::QueryController do
end
it "cannot access admin endpoints" do
query = make_query('SELECT 1 as value')
query = make_query("SELECT 1 as value")
post "/admin/plugins/explorer/queries/#{query.id}/run.json"
expect(response.status).to eq(403)
end
@ -373,12 +376,12 @@ describe DataExplorer::QueryController do
describe "#group_reports_index" do
it "only returns queries that the group has access to" do
group.add(user)
make_query('SELECT 1 as value', { name: 'A' }, ["#{group.id}"])
make_query("SELECT 1 as value", { name: "A" }, ["#{group.id}"])
get "/g/#{group.name}/reports.json"
expect(response.status).to eq(200)
expect(response_json['queries'].length).to eq(1)
expect(response_json['queries'][0]['name']).to eq('A')
expect(response_json["queries"].length).to eq(1)
expect(response_json["queries"][0]["name"]).to eq("A")
end
it "returns a 404 when the user should not have access to the query " do
@ -398,19 +401,19 @@ describe DataExplorer::QueryController do
it "does not return hidden queries" do
group.add(user)
make_query('SELECT 1 as value', { name: 'A', hidden: true }, ["#{group.id}"])
make_query('SELECT 1 as value', { name: 'B' }, ["#{group.id}"])
make_query("SELECT 1 as value", { name: "A", hidden: true }, ["#{group.id}"])
make_query("SELECT 1 as value", { name: "B" }, ["#{group.id}"])
get "/g/#{group.name}/reports.json"
expect(response.status).to eq(200)
expect(response_json['queries'].length).to eq(1)
expect(response_json['queries'][0]['name']).to eq('B')
expect(response_json["queries"].length).to eq(1)
expect(response_json["queries"][0]["name"]).to eq("B")
end
end
describe "#group_reports_run" do
it "runs the query" do
query = make_query('SELECT 1828 as value', { name: 'B' }, ["#{group.id}"])
query = make_query("SELECT 1828 as value", { name: "B" }, ["#{group.id}"])
post "/g/#{group.name}/reports/#{query.id}/run.json"
expect(response.status).to eq(200)
@ -421,7 +424,7 @@ describe DataExplorer::QueryController do
it "returns a 404 when the user should not have access to the query " do
group.add(user)
query = make_query('SELECT 1 as value', {}, [])
query = make_query("SELECT 1 as value", {}, [])
post "/g/#{group.name}/reports/#{query.id}/run.json"
expect(response.status).to eq(404)
@ -429,7 +432,7 @@ describe DataExplorer::QueryController do
it "return a 200 when the user has access the the query" do
group.add(user)
query = make_query('SELECT 1 as value', {}, [group.id.to_s])
query = make_query("SELECT 1 as value", {}, [group.id.to_s])
post "/g/#{group.name}/reports/#{query.id}/run.json"
expect(response.status).to eq(200)
@ -437,7 +440,7 @@ describe DataExplorer::QueryController do
it "return a 404 when the query is hidden" do
group.add(user)
query = make_query('SELECT 1 as value', { hidden: true }, [group.id.to_s])
query = make_query("SELECT 1 as value", { hidden: true }, [group.id.to_s])
post "/g/#{group.name}/reports/#{query.id}/run.json"
expect(response.status).to eq(404)
@ -446,21 +449,21 @@ describe DataExplorer::QueryController do
describe "#group_reports_show" do
it "returns a 404 when the user should not have access to the query " do
query = make_query('SELECT 1 as value', {}, [])
query = make_query("SELECT 1 as value", {}, [])
get "/g/#{group.name}/reports/#{query.id}.json"
expect(response.status).to eq(404)
end
it "return a 200 when the user has access the the query" do
query = make_query('SELECT 1 as value', {}, [group.id.to_s])
query = make_query("SELECT 1 as value", {}, [group.id.to_s])
get "/g/#{group.name}/reports/#{query.id}.json"
expect(response.status).to eq(200)
end
it "return a 404 when the query is hidden" do
query = make_query('SELECT 1 as value', { hidden: true }, [group.id.to_s])
query = make_query("SELECT 1 as value", { hidden: true }, [group.id.to_s])
get "/g/#{group.name}/reports/#{query.id}.json"
expect(response.status).to eq(404)

View File

@ -1,18 +1,23 @@
# frozen_string_literal: true
require 'rails_helper'
require "rails_helper"
describe 'Data Explorer rake tasks' do
describe "Data Explorer rake tasks" do
before do
Rake::Task.clear
Discourse::Application.load_tasks
end
def make_query(sql, opts = {}, group_ids = [])
query = DataExplorer::Query.create!(id: opts[:id], name: opts[:name] || "Query number", description: "A description for query number", sql: sql, hidden: opts[:hidden] || false)
group_ids.each do |group_id|
query.query_groups.create!(group_id: group_id)
end
query =
DataExplorer::Query.create!(
id: opts[:id],
name: opts[:name] || "Query number",
description: "A description for query number",
sql: sql,
hidden: opts[:hidden] || false,
)
group_ids.each { |group_id| query.query_groups.create!(group_id: group_id) }
query
end
@ -20,13 +25,13 @@ describe 'Data Explorer rake tasks' do
DataExplorer::Query.where(hidden: true).order(:id)
end
describe 'data_explorer' do
it 'hides a single query' do
describe "data_explorer" do
it "hides a single query" do
DataExplorer::Query.destroy_all
make_query('SELECT 1 as value', id: 1, name: 'A')
make_query('SELECT 1 as value', id: 2, name: 'B')
make_query("SELECT 1 as value", id: 1, name: "A")
make_query("SELECT 1 as value", id: 2, name: "B")
# rake data_explorer[1] => hide query with ID 1
silence_stdout { Rake::Task['data_explorer'].invoke(1) }
silence_stdout { Rake::Task["data_explorer"].invoke(1) }
# Soft deletion: PluginStoreRow should not be modified
expect(DataExplorer::Query.all.length).to eq(2)
@ -36,14 +41,14 @@ describe 'Data Explorer rake tasks' do
expect(hidden_queries[0].id).to eq(1)
end
it 'hides multiple queries' do
it "hides multiple queries" do
DataExplorer::Query.destroy_all
make_query('SELECT 1 as value', id: 1, name: 'A')
make_query('SELECT 1 as value', id: 2, name: 'B')
make_query('SELECT 1 as value', id: 3, name: 'C')
make_query('SELECT 1 as value', id: 4, name: 'D')
make_query("SELECT 1 as value", id: 1, name: "A")
make_query("SELECT 1 as value", id: 2, name: "B")
make_query("SELECT 1 as value", id: 3, name: "C")
make_query("SELECT 1 as value", id: 4, name: "D")
# rake data_explorer[1,2,4] => hide queries with IDs 1, 2 and 4
silence_stdout { Rake::Task['data_explorer'].invoke(1, 2, 4) }
silence_stdout { Rake::Task["data_explorer"].invoke(1, 2, 4) }
# Soft deletion: PluginStoreRow should not be modified
expect(DataExplorer::Query.all.length).to eq(4)
@ -55,15 +60,15 @@ describe 'Data Explorer rake tasks' do
expect(hidden_queries[2].id).to eq(4)
end
context 'when query does not exist in PluginStore' do
it 'should not hide the query' do
context "when query does not exist in PluginStore" do
it "should not hide the query" do
DataExplorer::Query.destroy_all
make_query('SELECT 1 as value', id: 1, name: 'A')
make_query('SELECT 1 as value', id: 2, name: 'B')
make_query("SELECT 1 as value", id: 1, name: "A")
make_query("SELECT 1 as value", id: 2, name: "B")
# rake data_explorer[3] => try to hide query with ID 3
silence_stdout { Rake::Task['data_explorer'].invoke(3) }
silence_stdout { Rake::Task["data_explorer"].invoke(3) }
# rake data_explorer[3,4,5] => try to hide queries with IDs 3, 4 and 5
silence_stdout { Rake::Task['data_explorer'].invoke(3, 4, 5) }
silence_stdout { Rake::Task["data_explorer"].invoke(3, 4, 5) }
# Array of hidden queries should be empty
expect(hidden_queries.length).to eq(0)
@ -71,13 +76,13 @@ describe 'Data Explorer rake tasks' do
end
end
describe '#unhide_query' do
it 'unhides a single query' do
describe "#unhide_query" do
it "unhides a single query" do
DataExplorer::Query.destroy_all
make_query('SELECT 1 as value', id: 1, name: 'A', hidden: true)
make_query('SELECT 1 as value', id: 2, name: 'B', hidden: true)
make_query("SELECT 1 as value", id: 1, name: "A", hidden: true)
make_query("SELECT 1 as value", id: 2, name: "B", hidden: true)
# rake data_explorer:unhide_query[1] => unhide query with ID 1
silence_stdout { Rake::Task['data_explorer:unhide_query'].invoke(1) }
silence_stdout { Rake::Task["data_explorer:unhide_query"].invoke(1) }
# Soft deletion: PluginStoreRow should not be modified
expect(DataExplorer::Query.all.length).to eq(2)
@ -87,14 +92,14 @@ describe 'Data Explorer rake tasks' do
expect(hidden_queries[0].id).to eq(2)
end
it 'unhides multiple queries' do
it "unhides multiple queries" do
DataExplorer::Query.destroy_all
make_query('SELECT 1 as value', id: 1, name: 'A', hidden: true)
make_query('SELECT 1 as value', id: 2, name: 'B', hidden: true)
make_query('SELECT 1 as value', id: 3, name: 'C', hidden: true)
make_query('SELECT 1 as value', id: 4, name: 'D', hidden: true)
make_query("SELECT 1 as value", id: 1, name: "A", hidden: true)
make_query("SELECT 1 as value", id: 2, name: "B", hidden: true)
make_query("SELECT 1 as value", id: 3, name: "C", hidden: true)
make_query("SELECT 1 as value", id: 4, name: "D", hidden: true)
# rake data_explorer:unhide_query[1,2,4] => unhide queries with IDs 1, 2 and 4
silence_stdout { Rake::Task['data_explorer:unhide_query'].invoke(1, 2, 4) }
silence_stdout { Rake::Task["data_explorer:unhide_query"].invoke(1, 2, 4) }
# Soft deletion: PluginStoreRow should not be modified
expect(DataExplorer::Query.all.length).to eq(4)
@ -104,15 +109,15 @@ describe 'Data Explorer rake tasks' do
expect(hidden_queries[0].id).to eq(3)
end
context 'when query does not exist in PluginStore' do
it 'should not unhide the query' do
context "when query does not exist in PluginStore" do
it "should not unhide the query" do
DataExplorer::Query.destroy_all
make_query('SELECT 1 as value', id: 1, name: 'A', hidden: true)
make_query('SELECT 1 as value', id: 2, name: 'B', hidden: true)
make_query("SELECT 1 as value", id: 1, name: "A", hidden: true)
make_query("SELECT 1 as value", id: 2, name: "B", hidden: true)
# rake data_explorer:unhide_query[3] => try to unhide query with ID 3
silence_stdout { Rake::Task['data_explorer:unhide_query'].invoke(3) }
silence_stdout { Rake::Task["data_explorer:unhide_query"].invoke(3) }
# rake data_explorer:unhide_query[3,4,5] => try to unhide queries with IDs 3, 4 and 5
silence_stdout { Rake::Task['data_explorer:unhide_query'].invoke(3, 4, 5) }
silence_stdout { Rake::Task["data_explorer:unhide_query"].invoke(3, 4, 5) }
# Array of hidden queries shouldn't change
expect(hidden_queries.length).to eq(2)
@ -120,13 +125,13 @@ describe 'Data Explorer rake tasks' do
end
end
describe '#hard_delete' do
it 'hard deletes a single query' do
describe "#hard_delete" do
it "hard deletes a single query" do
DataExplorer::Query.destroy_all
make_query('SELECT 1 as value', id: 1, name: 'A', hidden: true)
make_query('SELECT 1 as value', id: 2, name: 'B', hidden: true)
make_query("SELECT 1 as value", id: 1, name: "A", hidden: true)
make_query("SELECT 1 as value", id: 2, name: "B", hidden: true)
# rake data_explorer:hard_delete[1] => hard delete query with ID 1
silence_stdout { Rake::Task['data_explorer:hard_delete'].invoke(1) }
silence_stdout { Rake::Task["data_explorer:hard_delete"].invoke(1) }
# Hard deletion: query list should be shorter by 1
expect(DataExplorer::Query.all.length).to eq(1)
@ -136,14 +141,14 @@ describe 'Data Explorer rake tasks' do
expect(hidden_queries[0].id).to eq(2)
end
it 'hard deletes multiple queries' do
it "hard deletes multiple queries" do
DataExplorer::Query.destroy_all
make_query('SELECT 1 as value', id: 1, name: 'A', hidden: true)
make_query('SELECT 1 as value', id: 2, name: 'B', hidden: true)
make_query('SELECT 1 as value', id: 3, name: 'C', hidden: true)
make_query('SELECT 1 as value', id: 4, name: 'D', hidden: true)
make_query("SELECT 1 as value", id: 1, name: "A", hidden: true)
make_query("SELECT 1 as value", id: 2, name: "B", hidden: true)
make_query("SELECT 1 as value", id: 3, name: "C", hidden: true)
make_query("SELECT 1 as value", id: 4, name: "D", hidden: true)
# rake data_explorer:hard_delete[1,2,4] => hard delete queries with IDs 1, 2 and 4
silence_stdout { Rake::Task['data_explorer:hard_delete'].invoke(1, 2, 4) }
silence_stdout { Rake::Task["data_explorer:hard_delete"].invoke(1, 2, 4) }
# Hard deletion: query list should be shorter by 3
expect(DataExplorer::Query.all.length).to eq(1)
@ -153,27 +158,27 @@ describe 'Data Explorer rake tasks' do
expect(hidden_queries[0].id).to eq(3)
end
context 'when query does not exist in PluginStore' do
it 'should not hard delete the query' do
context "when query does not exist in PluginStore" do
it "should not hard delete the query" do
DataExplorer::Query.destroy_all
make_query('SELECT 1 as value', id: 1, name: 'A', hidden: true)
make_query('SELECT 1 as value', id: 2, name: 'B', hidden: true)
make_query("SELECT 1 as value", id: 1, name: "A", hidden: true)
make_query("SELECT 1 as value", id: 2, name: "B", hidden: true)
# rake data_explorer:hard_delete[3] => try to hard delete query with ID 3
silence_stdout { Rake::Task['data_explorer:hard_delete'].invoke(3) }
silence_stdout { Rake::Task["data_explorer:hard_delete"].invoke(3) }
# rake data_explorer:hard_delete[3,4,5] => try to hard delete queries with IDs 3, 4 and 5
silence_stdout { Rake::Task['data_explorer:hard_delete'].invoke(3, 4, 5) }
silence_stdout { Rake::Task["data_explorer:hard_delete"].invoke(3, 4, 5) }
# Array of hidden queries shouldn't change
expect(hidden_queries.length).to eq(2)
end
end
context 'when query is not hidden' do
it 'should not hard delete the query' do
context "when query is not hidden" do
it "should not hard delete the query" do
DataExplorer::Query.destroy_all
make_query('SELECT 1 as value', id: 1, name: 'A')
make_query("SELECT 1 as value", id: 1, name: "A")
# rake data_explorer:hard_delete[1] => try to hard delete query with ID 1
silence_stdout { Rake::Task['data_explorer:hard_delete'].invoke(1) }
silence_stdout { Rake::Task["data_explorer:hard_delete"].invoke(1) }
# List of queries shouldn't change
expect(DataExplorer::Query.all.length).to eq(1)

View File

@ -1,16 +1,16 @@
# frozen_string_literal: true
require 'rails_helper'
require "rails_helper"
describe 'fix query ids rake task' do
describe "fix query ids rake task" do
before do
Rake::Task.clear
Discourse::Application.load_tasks
end
let(:query_name) { 'Awesome query' }
let(:query_name) { "Awesome query" }
it 'fixes the ID of the query if they share the same name' do
it "fixes the ID of the query if they share the same name" do
original_query_id = 4
create_plugin_store_row(query_name, original_query_id)
create_query(query_name)
@ -20,7 +20,7 @@ describe 'fix query ids rake task' do
expect(find(query_name).id).to eq(original_query_id)
end
it 'only fixes queries with unique name' do
it "only fixes queries with unique name" do
original_query_id = 4
create_plugin_store_row(query_name, original_query_id)
create_query(query_name)
@ -31,7 +31,7 @@ describe 'fix query ids rake task' do
expect(find(query_name).id).not_to eq(original_query_id)
end
it 'skips queries that already have the same ID' do
it "skips queries that already have the same ID" do
db_query = create_query(query_name)
last_updated_at = db_query.updated_at
create_plugin_store_row(query_name, db_query.id)
@ -41,9 +41,9 @@ describe 'fix query ids rake task' do
expect(find(query_name).updated_at).to eq_time(last_updated_at)
end
it 'keeps queries the rest of the queries' do
it "keeps queries the rest of the queries" do
original_query_id = 4
different_query_name = 'Another query'
different_query_name = "Another query"
create_plugin_store_row(query_name, original_query_id)
create_query(query_name)
create_query(different_query_name)
@ -53,8 +53,8 @@ describe 'fix query ids rake task' do
expect(find(different_query_name)).not_to be_nil
end
it 'works even if they are additional conflicts' do
different_query_name = 'Another query'
it "works even if they are additional conflicts" do
different_query_name = "Another query"
additional_conflict = create_query(different_query_name)
create_query(query_name)
create_plugin_store_row(query_name, additional_conflict.id)
@ -65,7 +65,7 @@ describe 'fix query ids rake task' do
expect(find(query_name).id).to eq(additional_conflict.id)
end
describe 'query groups' do
describe "query groups" do
let(:group) { Fabricate(:group) }
it "fixes the query group's query_id" do
@ -78,8 +78,8 @@ describe 'fix query ids rake task' do
expect(find_query_group(original_query_id)).not_to be_nil
end
it 'works with additional conflicts' do
different_query_name = 'Another query'
it "works with additional conflicts" do
different_query_name = "Another query"
additional_conflict = create_query(different_query_name, [group.id])
create_query(query_name, [group.id])
create_plugin_store_row(query_name, additional_conflict.id, [group.id])
@ -98,7 +98,7 @@ describe 'fix query ids rake task' do
end
end
it 'changes the serial sequence for future queries' do
it "changes the serial sequence for future queries" do
original_query_id = 4
create_plugin_store_row(query_name, original_query_id)
create_query(query_name)
@ -110,7 +110,7 @@ describe 'fix query ids rake task' do
end
def run_task
Rake::Task['data_explorer:fix_query_ids'].invoke
Rake::Task["data_explorer:fix_query_ids"].invoke
end
def create_plugin_store_row(name, id, group_ids = [])
@ -119,27 +119,25 @@ describe 'fix query ids rake task' do
PluginStore.set(
DataExplorer.plugin_name,
key,
attributes(name).merge(group_ids: group_ids, id: id)
attributes(name).merge(group_ids: group_ids, id: id),
)
end
def create_query(name, group_ids = [])
DataExplorer::Query.create!(attributes(name)).tap do |query|
group_ids.each do |group_id|
query.query_groups.create!(group_id: group_id)
end
end
DataExplorer::Query
.create!(attributes(name))
.tap { |query| group_ids.each { |group_id| query.query_groups.create!(group_id: group_id) } }
end
def attributes(name)
{
id: DataExplorer::Query.count == 0 ? 5 : DataExplorer::Query.maximum(:id) + 1,
name: name,
description: 'A Query',
description: "A Query",
sql: "SELECT 1",
created_at: 3.hours.ago,
last_run_at: 1.hour.ago,
hidden: false
hidden: false,
}
end