From 148d6c32a31a918c91eef118ec20584fab2c9e49 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Thu, 29 Dec 2022 12:31:29 +0000 Subject: [PATCH] DEV: Introduce syntax_tree for ruby formatting (#208) --- .github/workflows/plugin-linting.yml | 9 + .github/workflows/plugin-tests.yml | 6 +- .rubocop.yml | 2 +- .streerc | 2 + Gemfile | 5 +- Gemfile.lock | 4 + .../data_explorer/query_controller.rb | 119 ++- app/jobs/scheduled/delete_hidden_queries.rb | 8 +- app/models/data_explorer/query.rb | 14 +- app/models/data_explorer/query_group.rb | 2 +- .../data_explorer/query_group_serializer.rb | 15 +- .../data_explorer/query_serializer.rb | 12 +- ...0810053843_create_data_explorer_queries.rb | 32 +- db/migrate/20200902225712_fix_query_ids.rb | 2 +- lib/data_explorer_query_group_bookmarkable.rb | 23 +- lib/queries.rb | 194 +++-- lib/tasks/data_explorer.rake | 16 +- lib/tasks/fix_query_ids.rake | 12 +- plugin.rb | 814 +++++++++--------- spec/data_explorer_spec.rb | 8 +- spec/guardian_spec.rb | 12 +- .../integration/custom_api_key_scopes_spec.rb | 92 +- .../scheduled/delete_hidden_queries_spec.rb | 60 +- ..._explorer_query_group_bookmarkable_spec.rb | 100 ++- spec/requests/group_spec.rb | 2 +- spec/requests/query_controller_spec.rb | 225 ++--- spec/tasks/data_explorer_spec.rb | 127 +-- spec/tasks/fix_query_ids_spec.rb | 44 +- 28 files changed, 1064 insertions(+), 897 deletions(-) create mode 100644 .streerc diff --git a/.github/workflows/plugin-linting.yml b/.github/workflows/plugin-linting.yml index c807794..6d2bb97 100644 --- a/.github/workflows/plugin-linting.yml +++ b/.github/workflows/plugin-linting.yml @@ -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 diff --git a/.github/workflows/plugin-tests.yml b/.github/workflows/plugin-tests.yml index 9d390bc..f30a5be 100644 --- a/.github/workflows/plugin-tests.yml +++ b/.github/workflows/plugin-tests.yml @@ -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 diff --git a/.rubocop.yml b/.rubocop.yml index d46296c..fb14dfa 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,2 +1,2 @@ inherit_gem: - rubocop-discourse: default.yml + rubocop-discourse: stree-compat.yml diff --git a/.streerc b/.streerc new file mode 100644 index 0000000..0bc4379 --- /dev/null +++ b/.streerc @@ -0,0 +1,2 @@ +--print-width=100 +--plugins=plugin/trailing_comma diff --git a/Gemfile b/Gemfile index 7da32ec..31d8bf7 100644 --- a/Gemfile +++ b/Gemfile @@ -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 diff --git a/Gemfile.lock b/Gemfile.lock index 2434c95..3b79e02 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 diff --git a/app/controllers/data_explorer/query_controller.rb b/app/controllers/data_explorer/query_controller.rb index 8a870d4..566f02d 100644 --- a/app/controllers/data_explorer/query_controller.rb +++ b/app/controllers/data_explorer/query_controller.rb @@ -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,40 +53,48 @@ 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 query_group = DataExplorer::QueryGroup.find_by(query_id: @query.id, group_id: @group.id) render json: { - query: serialize_data(@query, DataExplorer::QuerySerializer, root: nil), - query_group: serialize_data(query_group, DataExplorer::QueryGroupSerializer, root: nil), - } - + query: serialize_data(@query, DataExplorer::QuerySerializer, 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,18 +145,17 @@ class DataExplorer::QueryController < ::ApplicationController 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 + 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) @@ -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,16 +205,16 @@ 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| - csv << cols - pg_result.values.each do |row| - csv << row + require "csv" + text = + CSV.generate do |csv| + csv << cols + pg_result.values.each { |row| csv << row } end - end render plain: text end diff --git a/app/jobs/scheduled/delete_hidden_queries.rb b/app/jobs/scheduled/delete_hidden_queries.rb index 3a1c8d1..9669835 100644 --- a/app/jobs/scheduled/delete_hidden_queries.rb +++ b/app/jobs/scheduled/delete_hidden_queries.rb @@ -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 diff --git a/app/models/data_explorer/query.rb b/app/models/data_explorer/query.rb index f072463..7491fa8 100644 --- a/app/models/data_explorer/query.rb +++ b/app/models/data_explorer/query.rb @@ -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) diff --git a/app/models/data_explorer/query_group.rb b/app/models/data_explorer/query_group.rb index d7b7e93..4efc338 100644 --- a/app/models/data_explorer/query_group.rb +++ b/app/models/data_explorer/query_group.rb @@ -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 diff --git a/app/serializers/data_explorer/query_group_serializer.rb b/app/serializers/data_explorer/query_group_serializer.rb index 21c9540..da15ca3 100644 --- a/app/serializers/data_explorer/query_group_serializer.rb +++ b/app/serializers/data_explorer/query_group_serializer.rb @@ -1,11 +1,13 @@ # frozen_string_literal: true class DataExplorer::QueryGroupSerializer < ActiveModel::Serializer - attributes :id, :group_id, :query_id, :bookmark, - - def query_group_bookmark - @query_group_bookmark ||= Bookmark.find_by(user: scope.user, bookmarkable: object) - end + attributes :id, + :group_id, + :query_id, + :bookmark, + def query_group_bookmark + @query_group_bookmark ||= Bookmark.find_by(user: scope.user, bookmarkable: object) + end def include_bookmark? query_group_bookmark.present? @@ -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 diff --git a/app/serializers/data_explorer/query_serializer.rb b/app/serializers/data_explorer/query_serializer.rb index 634c8f8..2117557 100644 --- a/app/serializers/data_explorer/query_serializer.rb +++ b/app/serializers/data_explorer/query_serializer.rb @@ -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) diff --git a/db/migrate/20200810053843_create_data_explorer_queries.rb b/db/migrate/20200810053843_create_data_explorer_queries.rb index f7a3e0f..a966387 100644 --- a/db/migrate/20200810053843_create_data_explorer_queries.rb +++ b/db/migrate/20200810053843_create_data_explorer_queries.rb @@ -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,20 +56,32 @@ 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| - 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 + 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| - next if group_id.blank? || query_id.blank? - DB.exec <<~SQL + 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) VALUES(#{query_id}, #{group_id}) SQL + end end - end DB.exec <<~SQL SELECT diff --git a/db/migrate/20200902225712_fix_query_ids.rb b/db/migrate/20200902225712_fix_query_ids.rb index 4dcc4c3..e566c96 100644 --- a/db/migrate/20200902225712_fix_query_ids.rb +++ b/db/migrate/20200902225712_fix_query_ids.rb @@ -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 diff --git a/lib/data_explorer_query_group_bookmarkable.rb b/lib/data_explorer_query_group_bookmarkable.rb index 40693f4..d36780d 100644 --- a/lib/data_explorer_query_group_bookmarkable.rb +++ b/lib/data_explorer_query_group_bookmarkable.rb @@ -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 diff --git a/lib/queries.rb b/lib/queries.rb index 2b32979..cf57f07 100644 --- a/lib/queries.rb +++ b/lib/queries.rb @@ -11,96 +11,104 @@ class Queries # you must run the query with id=-1 on the site again to update these changes in the site db queries = { - "most-common-likers": { - "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" - }, - "edited-post-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." - }, - "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." - }, - "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." - }, - "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." - }, - "user-participation": { - "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." - }, - "inactive-users": { - "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." - }, - "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." - }, - "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" - }, - "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." - }, - "total-assigned-topics-report": { - "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." - }, - "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." - } + "most-common-likers": { + 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", + }, + "edited-post-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.", + }, + "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.", + }, + "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.", + }, + "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.", + }, + "user-participation": { + 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.", + }, + "inactive-users": { + 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.", + }, + "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.", + }, + "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", + }, + "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.", + }, + "total-assigned-topics-report": { + 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.", + }, + "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.", + }, }.with_indifferent_access queries["most-common-likers"]["sql"] = <<~SQL @@ -546,8 +554,8 @@ class Queries ORDER BY p.year DESC, qt DESC SQL - # convert query ids from "mostcommonlikers" to "-1", "mostmessages" to "-2" etc. - queries.transform_keys!.with_index { |key, idx| "-#{idx + 1}" } - queries + # convert query ids from "mostcommonlikers" to "-1", "mostmessages" to "-2" etc. + queries.transform_keys!.with_index { |key, idx| "-#{idx + 1}" } + queries end end diff --git a/lib/tasks/data_explorer.rake b/lib/tasks/data_explorer.rake index 407f30b..3dfdb49 100644 --- a/lib/tasks/data_explorer.rake +++ b/lib/tasks/data_explorer.rake @@ -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) diff --git a/lib/tasks/fix_query_ids.rake b/lib/tasks/fix_query_ids.rake index 1366204..e2a2775 100644 --- a/lib/tasks/fix_query_ids.rake +++ b/lib/tasks/fix_query_ids.rake @@ -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) diff --git a/plugin.rb b/plugin.rb index 61ea23d..3cf7326 100644 --- a/plugin.rb +++ b/plugin.rb @@ -9,8 +9,8 @@ enabled_site_setting :data_explorer_enabled -require File.expand_path('../lib/discourse_data_explorer/engine.rb', __FILE__) -register_asset 'stylesheets/explorer.scss' +require File.expand_path("../lib/discourse_data_explorer/engine.rb", __FILE__) +register_asset "stylesheets/explorer.scss" if respond_to?(:register_svg_icon) register_svg_icon "caret-down" @@ -23,15 +23,15 @@ if respond_to?(:register_svg_icon) end # route: /admin/plugins/explorer -add_admin_route 'explorer.title', 'explorer' +add_admin_route "explorer.title", "explorer" module ::DataExplorer # This should always match the max value for the data_explorer_query_result_limit # site setting. - QUERY_RESULT_MAX_LIMIT = 10000 + QUERY_RESULT_MAX_LIMIT = 10_000 def self.plugin_name - 'discourse-data-explorer'.freeze + "discourse-data-explorer".freeze end end @@ -45,9 +45,7 @@ after_initialize do add_to_class(:guardian, :user_can_access_query?) do |query| return false if !current_user return true if current_user.admin? - query.groups.blank? || query.groups.any? do |group| - user_is_a_member_of_group?(group) - end + query.groups.blank? || query.groups.any? { |group| user_is_a_member_of_group?(group) } end add_to_class(:guardian, :group_and_user_can_access_query?) do |group, query| @@ -89,7 +87,7 @@ after_initialize do # Safety checks # see test 'doesn't allow you to modify the database #2' if query.sql =~ /;/ - err = DataExplorer::ValidationError.new(I18n.t('js.errors.explorer.no_semicolons')) + err = DataExplorer::ValidationError.new(I18n.t("js.errors.explorer.no_semicolons")) return { error: err, duration_nanos: 0 } end @@ -126,15 +124,21 @@ SQL # Using MiniSql::InlineParamEncoder directly instead of DB.param_encoder because current implementation of # DB.param_encoder is meant for SQL fragments and not an entire SQL string. - sql = MiniSql::InlineParamEncoder.new(ActiveRecord::Base.connection.raw_connection).encode(sql, query_args) + sql = + MiniSql::InlineParamEncoder.new(ActiveRecord::Base.connection.raw_connection).encode( + sql, + query_args, + ) result = ActiveRecord::Base.connection.raw_connection.async_exec(sql) result.check # make sure it's done time_end = Time.now if opts[:explain] - explain = DB.query_hash("EXPLAIN #{query.sql}", query_args) - .map { |row| row["QUERY PLAN"] }.join "\n" + explain = + DB + .query_hash("EXPLAIN #{query.sql}", query_args) + .map { |row| row["QUERY PLAN"] }.join "\n" end # All done. Issue a rollback anyways, just in case @@ -151,30 +155,56 @@ SQL pg_result: result, duration_secs: time_end - time_start, explain: explain, - params_full: query_args + params_full: query_args, } end def self.extra_data_pluck_fields @extra_data_pluck_fields ||= { - user: { class: User, fields: [:id, :username, :uploaded_avatar_id], serializer: BasicUserSerializer }, - badge: { class: Badge, fields: [:id, :name, :badge_type_id, :description, :icon], include: [:badge_type], serializer: SmallBadgeSerializer }, - post: { class: Post, fields: [:id, :topic_id, :post_number, :cooked, :user_id], include: [:user], serializer: SmallPostWithExcerptSerializer }, - topic: { class: Topic, fields: [:id, :title, :slug, :posts_count], serializer: BasicTopicSerializer }, - group: { class: Group, ignore: true }, - category: { class: Category, ignore: true }, - reltime: { ignore: true }, - html: { ignore: true }, + user: { + class: User, + fields: %i[id username uploaded_avatar_id], + serializer: BasicUserSerializer, + }, + badge: { + class: Badge, + fields: %i[id name badge_type_id description icon], + include: [:badge_type], + serializer: SmallBadgeSerializer, + }, + post: { + class: Post, + fields: %i[id topic_id post_number cooked user_id], + include: [:user], + serializer: SmallPostWithExcerptSerializer, + }, + topic: { + class: Topic, + fields: %i[id title slug posts_count], + serializer: BasicTopicSerializer, + }, + group: { + class: Group, + ignore: true, + }, + category: { + class: Category, + ignore: true, + }, + reltime: { + ignore: true, + }, + html: { + ignore: true, + }, } end def self.column_regexes @column_regexes ||= - extra_data_pluck_fields.map do |key, val| - if val[:class] - /(#{val[:class].to_s.downcase})_id$/ - end - end.compact + extra_data_pluck_fields + .map { |key, val| /(#{val[:class].to_s.downcase})_id$/ if val[:class] } + .compact end def self.add_extra_data(pg_result) @@ -202,9 +232,7 @@ SQL support_info = extra_data_pluck_fields[cls] next unless support_info - column_nums.each do |col_n| - col_map[col_n] = cls - end + column_nums.each { |col_n| col_map[col_n] = cls } if support_info[:ignore] ret[cls] = [] @@ -212,28 +240,30 @@ SQL end ids = Set.new - column_nums.each do |col_n| - ids.merge(pg_result.column_values(col_n)) - end + column_nums.each { |col_n| ids.merge(pg_result.column_values(col_n)) } ids.delete nil ids.map! &:to_i object_class = support_info[:class] all_objs = object_class all_objs = all_objs.with_deleted if all_objs.respond_to? :with_deleted - all_objs = all_objs - .select(support_info[:fields]) - .where(id: ids.to_a.sort) - .includes(support_info[:include]) - .order(:id) + all_objs = + all_objs + .select(support_info[:fields]) + .where(id: ids.to_a.sort) + .includes(support_info[:include]) + .order(:id) - ret[cls] = ActiveModel::ArraySerializer.new(all_objs, each_serializer: support_info[:serializer]) + ret[cls] = ActiveModel::ArraySerializer.new( + all_objs, + each_serializer: support_info[:serializer], + ) end [ret, col_map] end def self.sensitive_column_names - %w( + %w[ #_IP_Addresses topic_views.ip_address users.ip_address @@ -241,7 +271,6 @@ SQL incoming_links.ip_address topic_link_clicks.ip_address user_histories.ip_address - #_Emails email_tokens.email users.email @@ -250,17 +279,14 @@ SQL email_logs.to_address posts.raw_email badge_posts.raw_email - #_Secret_Tokens email_tokens.token email_logs.reply_key api_keys.key site_settings.value - users.auth_token users.password_hash users.salt - #_Authentication_Info user_open_ids.email oauth2_user_infos.uid @@ -273,14 +299,15 @@ SQL single_sign_on_records.external_id google_user_infos.google_user_id google_user_infos.email - ) + ] end def self.schema # No need to expire this, because the server processes get restarted on upgrade # refer user to http://www.postgresql.org/docs/9.3/static/datatype.html - @schema ||= begin - results = DB.query_hash <<~SQL + @schema ||= + begin + results = DB.query_hash <<~SQL select c.column_name column_name, c.data_type data_type, @@ -296,148 +323,151 @@ SQL ORDER BY c.table_name, c.ordinal_position SQL - by_table = {} - # Massage the results into a nicer form - results.each do |hash| - full_col_name = "#{hash['table_name']}.#{hash['column_name']}" + by_table = {} + # Massage the results into a nicer form + results.each do |hash| + full_col_name = "#{hash["table_name"]}.#{hash["column_name"]}" - if hash['is_nullable'] == "YES" - hash['is_nullable'] = true - else - hash.delete('is_nullable') - end - clen = hash.delete 'character_maximum_length' - dt = hash['data_type'] - if hash['column_name'] == 'id' - hash['data_type'] = 'serial' - hash['primary'] = true - elsif dt == 'character varying' - hash['data_type'] = "varchar(#{clen.to_i})" - elsif dt == 'timestamp without time zone' - hash['data_type'] = 'timestamp' - elsif dt == 'double precision' - hash['data_type'] = 'double' - end - default = hash['column_default'] - if default.nil? || default =~ /^nextval\(/ - hash.delete 'column_default' - elsif default =~ /^'(.*)'::(character varying|text)/ - hash['column_default'] = $1 - end - hash.delete('column_desc') unless hash['column_desc'] + if hash["is_nullable"] == "YES" + hash["is_nullable"] = true + else + hash.delete("is_nullable") + end + clen = hash.delete "character_maximum_length" + dt = hash["data_type"] + if hash["column_name"] == "id" + hash["data_type"] = "serial" + hash["primary"] = true + elsif dt == "character varying" + hash["data_type"] = "varchar(#{clen.to_i})" + elsif dt == "timestamp without time zone" + hash["data_type"] = "timestamp" + elsif dt == "double precision" + hash["data_type"] = "double" + end + default = hash["column_default"] + if default.nil? || default =~ /^nextval\(/ + hash.delete "column_default" + elsif default =~ /^'(.*)'::(character varying|text)/ + hash["column_default"] = $1 + end + hash.delete("column_desc") unless hash["column_desc"] - if sensitive_column_names.include? full_col_name - hash['sensitive'] = true - end - if enum_info.include? full_col_name - hash['enum'] = enum_info[full_col_name] - end - if denormalized_columns.include? full_col_name - hash['denormal'] = denormalized_columns[full_col_name] - end - fkey = fkey_info(hash['table_name'], hash['column_name']) - if fkey - hash['fkey_info'] = fkey + hash["sensitive"] = true if sensitive_column_names.include? full_col_name + hash["enum"] = enum_info[full_col_name] if enum_info.include? full_col_name + if denormalized_columns.include? full_col_name + hash["denormal"] = denormalized_columns[full_col_name] + end + fkey = fkey_info(hash["table_name"], hash["column_name"]) + hash["fkey_info"] = fkey if fkey + + table_name = hash.delete("table_name") + by_table[table_name] ||= [] + by_table[table_name] << hash end - table_name = hash.delete('table_name') - by_table[table_name] ||= [] - by_table[table_name] << hash + # this works for now, but no big loss if the tables aren't quite sorted + favored_order = %w[ + posts + topics + users + categories + badges + groups + notifications + post_actions + site_settings + ] + sorted_by_table = {} + favored_order.each { |tbl| sorted_by_table[tbl] = by_table[tbl] } + by_table.keys.sort.each do |tbl| + next if favored_order.include? tbl + sorted_by_table[tbl] = by_table[tbl] + end + sorted_by_table end - - # this works for now, but no big loss if the tables aren't quite sorted - favored_order = %w(posts topics users categories badges groups notifications post_actions site_settings) - sorted_by_table = {} - favored_order.each do |tbl| - sorted_by_table[tbl] = by_table[tbl] - end - by_table.keys.sort.each do |tbl| - next if favored_order.include? tbl - sorted_by_table[tbl] = by_table[tbl] - end - sorted_by_table - end end def self.enums return @enums if @enums @enums = { - 'application_requests.req_type': ApplicationRequest.req_types, - 'badges.badge_type_id': Enum.new(:gold, :silver, :bronze, start: 1), - 'bookmarks.auto_delete_preference': Bookmark.auto_delete_preferences, - 'category_groups.permission_type': CategoryGroup.permission_types, - 'category_users.notification_level': CategoryUser.notification_levels, - 'directory_items.period_type': DirectoryItem.period_types, - 'email_change_requests.change_state': EmailChangeRequest.states, - 'groups.id': Group::AUTO_GROUPS, - 'groups.mentionable_level': Group::ALIAS_LEVELS, - 'groups.messageable_level': Group::ALIAS_LEVELS, - 'groups.members_visibility_level': Group.visibility_levels, - 'groups.visibility_level': Group.visibility_levels, - 'groups.default_notification_level': GroupUser.notification_levels, - 'group_histories.action': GroupHistory.actions, - 'group_users.notification_level': GroupUser.notification_levels, - 'imap_sync_logs.level': ImapSyncLog.levels, - 'invites.emailed_status': Invite.emailed_status_types, - 'notifications.notification_type': Notification.types, - 'polls.results': Poll.results, - 'polls.status': Poll.statuses, - 'polls.type': Poll.types, - 'polls.visibility': Poll.visibilities, - 'post_action_types.id': PostActionType.types, - 'post_actions.post_action_type_id': PostActionType.types, - 'posts.cook_method': Post.cook_methods, - 'posts.hidden_reason_id': Post.hidden_reasons, - 'posts.post_type': Post.types, - 'reviewables.status': Reviewable.statuses, - 'reviewable_histories.reviewable_history_type': ReviewableHistory.types, - 'reviewable_scores.status': ReviewableScore.statuses, - 'screened_emails.action_type': ScreenedEmail.actions, - 'screened_ip_addresses.action_type': ScreenedIpAddress.actions, - 'screened_urls.action_type': ScreenedUrl.actions, - 'search_logs.search_result_type': SearchLog.search_result_types, - 'search_logs.search_type': SearchLog.search_types, - 'site_settings.data_type': SiteSetting.types, - 'skipped_email_logs.reason_type': SkippedEmailLog.reason_types, - 'tag_group_permissions.permission_type': TagGroupPermission.permission_types, - 'theme_fields.type_id': ThemeField.types, - 'theme_settings.data_type': ThemeSetting.types, - 'topic_timers.status_type': TopicTimer.types, - 'topic_users.notification_level': TopicUser.notification_levels, - 'topic_users.notifications_reason_id': TopicUser.notification_reasons, - 'uploads.verification_status': Upload.verification_statuses, - 'user_actions.action_type': UserAction.types, - 'user_histories.action': UserHistory.actions, - 'user_options.email_previous_replies': UserOption.previous_replies_type, - 'user_options.like_notification_frequency': UserOption.like_notification_frequency_type, - 'user_options.text_size_key': UserOption.text_sizes, - 'user_options.title_count_mode_key': UserOption.title_count_modes, - 'user_options.email_level': UserOption.email_level_types, - 'user_options.email_messages_level': UserOption.email_level_types, - 'user_second_factors.method': UserSecondFactor.methods, - 'user_security_keys.factor_type': UserSecurityKey.factor_types, - 'users.trust_level': TrustLevel.levels, - 'watched_words.action': WatchedWord.actions, - 'web_hooks.content_type': WebHook.content_types, - 'web_hooks.last_delivery_status': WebHook.last_delivery_statuses, + "application_requests.req_type": ApplicationRequest.req_types, + "badges.badge_type_id": Enum.new(:gold, :silver, :bronze, start: 1), + "bookmarks.auto_delete_preference": Bookmark.auto_delete_preferences, + "category_groups.permission_type": CategoryGroup.permission_types, + "category_users.notification_level": CategoryUser.notification_levels, + "directory_items.period_type": DirectoryItem.period_types, + "email_change_requests.change_state": EmailChangeRequest.states, + "groups.id": Group::AUTO_GROUPS, + "groups.mentionable_level": Group::ALIAS_LEVELS, + "groups.messageable_level": Group::ALIAS_LEVELS, + "groups.members_visibility_level": Group.visibility_levels, + "groups.visibility_level": Group.visibility_levels, + "groups.default_notification_level": GroupUser.notification_levels, + "group_histories.action": GroupHistory.actions, + "group_users.notification_level": GroupUser.notification_levels, + "imap_sync_logs.level": ImapSyncLog.levels, + "invites.emailed_status": Invite.emailed_status_types, + "notifications.notification_type": Notification.types, + "polls.results": Poll.results, + "polls.status": Poll.statuses, + "polls.type": Poll.types, + "polls.visibility": Poll.visibilities, + "post_action_types.id": PostActionType.types, + "post_actions.post_action_type_id": PostActionType.types, + "posts.cook_method": Post.cook_methods, + "posts.hidden_reason_id": Post.hidden_reasons, + "posts.post_type": Post.types, + "reviewables.status": Reviewable.statuses, + "reviewable_histories.reviewable_history_type": ReviewableHistory.types, + "reviewable_scores.status": ReviewableScore.statuses, + "screened_emails.action_type": ScreenedEmail.actions, + "screened_ip_addresses.action_type": ScreenedIpAddress.actions, + "screened_urls.action_type": ScreenedUrl.actions, + "search_logs.search_result_type": SearchLog.search_result_types, + "search_logs.search_type": SearchLog.search_types, + "site_settings.data_type": SiteSetting.types, + "skipped_email_logs.reason_type": SkippedEmailLog.reason_types, + "tag_group_permissions.permission_type": TagGroupPermission.permission_types, + "theme_fields.type_id": ThemeField.types, + "theme_settings.data_type": ThemeSetting.types, + "topic_timers.status_type": TopicTimer.types, + "topic_users.notification_level": TopicUser.notification_levels, + "topic_users.notifications_reason_id": TopicUser.notification_reasons, + "uploads.verification_status": Upload.verification_statuses, + "user_actions.action_type": UserAction.types, + "user_histories.action": UserHistory.actions, + "user_options.email_previous_replies": UserOption.previous_replies_type, + "user_options.like_notification_frequency": UserOption.like_notification_frequency_type, + "user_options.text_size_key": UserOption.text_sizes, + "user_options.title_count_mode_key": UserOption.title_count_modes, + "user_options.email_level": UserOption.email_level_types, + "user_options.email_messages_level": UserOption.email_level_types, + "user_second_factors.method": UserSecondFactor.methods, + "user_security_keys.factor_type": UserSecurityKey.factor_types, + "users.trust_level": TrustLevel.levels, + "watched_words.action": WatchedWord.actions, + "web_hooks.content_type": WebHook.content_types, + "web_hooks.last_delivery_status": WebHook.last_delivery_statuses, }.with_indifferent_access # QueuedPost is removed in recent Discourse releases - @enums['queued_posts.state'] = QueuedPost.states if defined?(QueuedPost) + @enums["queued_posts.state"] = QueuedPost.states if defined?(QueuedPost) @enums end def self.enum_info - @enum_info ||= begin - enum_info = {} - enums.map do |key, enum| - # https://stackoverflow.com/questions/10874356/reverse-a-hash-in-ruby - enum_info[key] = Hash[enum.to_a.map(&:reverse)] + @enum_info ||= + begin + enum_info = {} + enums.map do |key, enum| + # https://stackoverflow.com/questions/10874356/reverse-a-hash-in-ruby + enum_info[key] = Hash[enum.to_a.map(&:reverse)] + end + enum_info end - enum_info - end end def self.fkey_info(table, column) @@ -456,50 +486,36 @@ SQL def self.foreign_keys @fkey_columns ||= { - 'posts.last_editor_id': :users, - 'posts.version': :'post_revisions.number', - - 'topics.featured_user1_id': :users, - 'topics.featured_user2_id': :users, - 'topics.featured_user3_id': :users, - 'topics.featured_user4_id': :users, - 'topics.featured_user5_id': :users, - - 'users.seen_notification_id': :notifications, - 'users.uploaded_avatar_id': :uploads, - 'users.primary_group_id': :groups, - - 'categories.latest_post_id': :posts, - 'categories.latest_topic_id': :topics, - 'categories.parent_category_id': :categories, - - 'badges.badge_grouping_id': :badge_groupings, - - 'post_actions.related_post_id': :posts, - - 'color_scheme_colors.color_scheme_id': :color_schemes, - 'color_schemes.versioned_id': :color_schemes, - - 'incoming_links.incoming_referer_id': :incoming_referers, - 'incoming_referers.incoming_domain_id': :incoming_domains, - - 'post_replies.reply_id': :posts, - - 'quoted_posts.quoted_post_id': :posts, - - 'topic_link_clicks.topic_link_id': :topic_links, - 'topic_link_clicks.link_topic_id': :topics, - 'topic_link_clicks.link_post_id': :posts, - - 'user_actions.target_topic_id': :topics, - 'user_actions.target_post_id': :posts, - - 'user_avatars.custom_upload_id': :uploads, - 'user_avatars.gravatar_upload_id': :uploads, - - 'user_badges.notification_id': :notifications, - - 'user_profiles.card_image_badge_id': :badges, + "posts.last_editor_id": :users, + "posts.version": :"post_revisions.number", + "topics.featured_user1_id": :users, + "topics.featured_user2_id": :users, + "topics.featured_user3_id": :users, + "topics.featured_user4_id": :users, + "topics.featured_user5_id": :users, + "users.seen_notification_id": :notifications, + "users.uploaded_avatar_id": :uploads, + "users.primary_group_id": :groups, + "categories.latest_post_id": :posts, + "categories.latest_topic_id": :topics, + "categories.parent_category_id": :categories, + "badges.badge_grouping_id": :badge_groupings, + "post_actions.related_post_id": :posts, + "color_scheme_colors.color_scheme_id": :color_schemes, + "color_schemes.versioned_id": :color_schemes, + "incoming_links.incoming_referer_id": :incoming_referers, + "incoming_referers.incoming_domain_id": :incoming_domains, + "post_replies.reply_id": :posts, + "quoted_posts.quoted_post_id": :posts, + "topic_link_clicks.topic_link_id": :topic_links, + "topic_link_clicks.link_topic_id": :topics, + "topic_link_clicks.link_post_id": :posts, + "user_actions.target_topic_id": :topics, + "user_actions.target_post_id": :posts, + "user_avatars.custom_upload_id": :uploads, + "user_avatars.gravatar_upload_id": :uploads, + "user_badges.notification_id": :notifications, + "user_profiles.card_image_badge_id": :badges, }.with_indifferent_access end @@ -508,129 +524,114 @@ SQL user_id: :users, # :*_by_id => :users, # :*_user_id => :users, - category_id: :categories, group_id: :groups, post_id: :posts, post_action_id: :post_actions, topic_id: :topics, upload_id: :uploads, - }.with_indifferent_access end def self.denormalized_columns { - 'posts.reply_count': :post_replies, - 'posts.quote_count': :quoted_posts, - 'posts.incoming_link_count': :topic_links, - 'posts.word_count': :posts, - 'posts.avg_time': :post_timings, - 'posts.reads': :post_timings, - 'posts.like_score': :post_actions, - - 'posts.like_count': :post_actions, - 'posts.bookmark_count': :post_actions, - 'posts.vote_count': :post_actions, - 'posts.off_topic_count': :post_actions, - 'posts.notify_moderators_count': :post_actions, - 'posts.spam_count': :post_actions, - 'posts.illegal_count': :post_actions, - 'posts.inappropriate_count': :post_actions, - 'posts.notify_user_count': :post_actions, - - 'topics.views': :topic_views, - 'topics.posts_count': :posts, - 'topics.reply_count': :posts, - 'topics.incoming_link_count': :topic_links, - 'topics.moderator_posts_count': :posts, - 'topics.participant_count': :posts, - 'topics.word_count': :posts, - 'topics.last_posted_at': :posts, - 'topics.last_post_user_idt': :posts, - 'topics.avg_time': :post_timings, - 'topics.highest_post_number': :posts, - 'topics.image_url': :posts, - 'topics.excerpt': :posts, - - 'topics.like_count': :post_actions, - 'topics.bookmark_count': :post_actions, - 'topics.vote_count': :post_actions, - 'topics.off_topic_count': :post_actions, - 'topics.notify_moderators_count': :post_actions, - 'topics.spam_count': :post_actions, - 'topics.illegal_count': :post_actions, - 'topics.inappropriate_count': :post_actions, - 'topics.notify_user_count': :post_actions, - - 'categories.topic_count': :topics, - 'categories.post_count': :posts, - 'categories.latest_post_id': :posts, - 'categories.latest_topic_id': :topics, - 'categories.description': :posts, - 'categories.read_restricted': :category_groups, - 'categories.topics_year': :topics, - 'categories.topics_month': :topics, - 'categories.topics_week': :topics, - 'categories.topics_day': :topics, - 'categories.posts_year': :posts, - 'categories.posts_month': :posts, - 'categories.posts_week': :posts, - 'categories.posts_day': :posts, - - 'badges.grant_count': :user_badges, - 'groups.user_count': :group_users, - - 'directory_items.likes_received': :post_actions, - 'directory_items.likes_given': :post_actions, - 'directory_items.topics_entered': :user_stats, - 'directory_items.days_visited': :user_stats, - 'directory_items.posts_read': :user_stats, - 'directory_items.topic_count': :topics, - 'directory_items.post_count': :posts, - - 'post_search_data.search_data': :posts, - - 'top_topics.yearly_posts_count': :posts, - 'top_topics.monthly_posts_count': :posts, - 'top_topics.weekly_posts_count': :posts, - 'top_topics.daily_posts_count': :posts, - 'top_topics.yearly_views_count': :topic_views, - 'top_topics.monthly_views_count': :topic_views, - 'top_topics.weekly_views_count': :topic_views, - 'top_topics.daily_views_count': :topic_views, - 'top_topics.yearly_likes_count': :post_actions, - 'top_topics.monthly_likes_count': :post_actions, - 'top_topics.weekly_likes_count': :post_actions, - 'top_topics.daily_likes_count': :post_actions, - 'top_topics.yearly_op_likes_count': :post_actions, - 'top_topics.monthly_op_likes_count': :post_actions, - 'top_topics.weekly_op_likes_count': :post_actions, - 'top_topics.daily_op_likes_count': :post_actions, - 'top_topics.all_score': :posts, - 'top_topics.yearly_score': :posts, - 'top_topics.monthly_score': :posts, - 'top_topics.weekly_score': :posts, - 'top_topics.daily_score': :posts, - - 'topic_links.clicks': :topic_link_clicks, - 'topic_search_data.search_data': :topics, - - 'topic_users.liked': :post_actions, - 'topic_users.bookmarked': :post_actions, - - 'user_stats.posts_read_count': :post_timings, - 'user_stats.topic_reply_count': :posts, - 'user_stats.first_post_created_at': :posts, - 'user_stats.post_count': :posts, - 'user_stats.topic_count': :topics, - 'user_stats.likes_given': :post_actions, - 'user_stats.likes_received': :post_actions, - - 'user_search_data.search_data': :user_profiles, - - 'users.last_posted_at': :posts, - 'users.previous_visit_at': :user_visits, + "posts.reply_count": :post_replies, + "posts.quote_count": :quoted_posts, + "posts.incoming_link_count": :topic_links, + "posts.word_count": :posts, + "posts.avg_time": :post_timings, + "posts.reads": :post_timings, + "posts.like_score": :post_actions, + "posts.like_count": :post_actions, + "posts.bookmark_count": :post_actions, + "posts.vote_count": :post_actions, + "posts.off_topic_count": :post_actions, + "posts.notify_moderators_count": :post_actions, + "posts.spam_count": :post_actions, + "posts.illegal_count": :post_actions, + "posts.inappropriate_count": :post_actions, + "posts.notify_user_count": :post_actions, + "topics.views": :topic_views, + "topics.posts_count": :posts, + "topics.reply_count": :posts, + "topics.incoming_link_count": :topic_links, + "topics.moderator_posts_count": :posts, + "topics.participant_count": :posts, + "topics.word_count": :posts, + "topics.last_posted_at": :posts, + "topics.last_post_user_idt": :posts, + "topics.avg_time": :post_timings, + "topics.highest_post_number": :posts, + "topics.image_url": :posts, + "topics.excerpt": :posts, + "topics.like_count": :post_actions, + "topics.bookmark_count": :post_actions, + "topics.vote_count": :post_actions, + "topics.off_topic_count": :post_actions, + "topics.notify_moderators_count": :post_actions, + "topics.spam_count": :post_actions, + "topics.illegal_count": :post_actions, + "topics.inappropriate_count": :post_actions, + "topics.notify_user_count": :post_actions, + "categories.topic_count": :topics, + "categories.post_count": :posts, + "categories.latest_post_id": :posts, + "categories.latest_topic_id": :topics, + "categories.description": :posts, + "categories.read_restricted": :category_groups, + "categories.topics_year": :topics, + "categories.topics_month": :topics, + "categories.topics_week": :topics, + "categories.topics_day": :topics, + "categories.posts_year": :posts, + "categories.posts_month": :posts, + "categories.posts_week": :posts, + "categories.posts_day": :posts, + "badges.grant_count": :user_badges, + "groups.user_count": :group_users, + "directory_items.likes_received": :post_actions, + "directory_items.likes_given": :post_actions, + "directory_items.topics_entered": :user_stats, + "directory_items.days_visited": :user_stats, + "directory_items.posts_read": :user_stats, + "directory_items.topic_count": :topics, + "directory_items.post_count": :posts, + "post_search_data.search_data": :posts, + "top_topics.yearly_posts_count": :posts, + "top_topics.monthly_posts_count": :posts, + "top_topics.weekly_posts_count": :posts, + "top_topics.daily_posts_count": :posts, + "top_topics.yearly_views_count": :topic_views, + "top_topics.monthly_views_count": :topic_views, + "top_topics.weekly_views_count": :topic_views, + "top_topics.daily_views_count": :topic_views, + "top_topics.yearly_likes_count": :post_actions, + "top_topics.monthly_likes_count": :post_actions, + "top_topics.weekly_likes_count": :post_actions, + "top_topics.daily_likes_count": :post_actions, + "top_topics.yearly_op_likes_count": :post_actions, + "top_topics.monthly_op_likes_count": :post_actions, + "top_topics.weekly_op_likes_count": :post_actions, + "top_topics.daily_op_likes_count": :post_actions, + "top_topics.all_score": :posts, + "top_topics.yearly_score": :posts, + "top_topics.monthly_score": :posts, + "top_topics.weekly_score": :posts, + "top_topics.daily_score": :posts, + "topic_links.clicks": :topic_link_clicks, + "topic_search_data.search_data": :topics, + "topic_users.liked": :post_actions, + "topic_users.bookmarked": :post_actions, + "user_stats.posts_read_count": :post_timings, + "user_stats.topic_reply_count": :posts, + "user_stats.first_post_created_at": :posts, + "user_stats.post_count": :posts, + "user_stats.topic_count": :topics, + "user_stats.likes_given": :post_actions, + "user_stats.likes_received": :post_actions, + "user_search_data.search_data": :user_profiles, + "users.last_posted_at": :posts, + "users.previous_visit_at": :user_visits, }.with_indifferent_access end end @@ -639,14 +640,24 @@ SQL attr_accessor :identifier, :type, :default, :nullable def initialize(identifier, type, default, nullable) - raise DataExplorer::ValidationError.new('Parameter declaration error - identifier is missing') unless identifier - raise DataExplorer::ValidationError.new('Parameter declaration error - type is missing') unless type + unless identifier + raise DataExplorer::ValidationError.new( + "Parameter declaration error - identifier is missing", + ) + end + unless type + raise DataExplorer::ValidationError.new("Parameter declaration error - type is missing") + end # process aliases type = type.to_sym if DataExplorer::Parameter.type_aliases[type] type = DataExplorer::Parameter.type_aliases[type] end - raise DataExplorer::ValidationError.new("Parameter declaration error - unknown type #{type}") unless DataExplorer::Parameter.types[type] + unless DataExplorer::Parameter.types[type] + raise DataExplorer::ValidationError.new( + "Parameter declaration error - unknown type #{type}", + ) + end @identifier = identifier @type = type @@ -655,36 +666,44 @@ SQL begin cast_to_ruby default unless default.blank? rescue DataExplorer::ValidationError - raise DataExplorer::ValidationError.new("Parameter declaration error - the default value is not a valid #{type}") + raise DataExplorer::ValidationError.new( + "Parameter declaration error - the default value is not a valid #{type}", + ) end end def to_hash - { - identifier: @identifier, - type: @type, - default: @default, - nullable: @nullable, - } + { identifier: @identifier, type: @type, default: @default, nullable: @nullable } end def self.types - @types ||= Enum.new( - # Normal types - :int, :bigint, :boolean, :string, :date, :time, :datetime, :double, - # Selection help - :user_id, :post_id, :topic_id, :category_id, :group_id, :badge_id, - # Arrays - :int_list, :string_list, :user_list - ) + @types ||= + Enum.new( + # Normal types + :int, + :bigint, + :boolean, + :string, + :date, + :time, + :datetime, + :double, + # Selection help + :user_id, + :post_id, + :topic_id, + :category_id, + :group_id, + :badge_id, + # Arrays + :int_list, + :string_list, + :user_list, + ) end def self.type_aliases - @type_aliases ||= { - integer: :int, - text: :string, - timestamp: :datetime, - } + @type_aliases ||= { integer: :int, text: :string, timestamp: :datetime } end def cast_to_ruby(string) @@ -697,9 +716,7 @@ SQL raise DataExplorer::ValidationError.new("Missing parameter #{identifier} of type #{type}") end end - if string.downcase == '#null' - return nil - end + return nil if string.downcase == "#null" def invalid_format(string, msg = nil) if msg @@ -713,11 +730,11 @@ SQL case @type when :int - invalid_format string, 'Not an integer' unless string =~ /^-?\d+$/ + invalid_format string, "Not an integer" unless string =~ /^-?\d+$/ value = string.to_i - invalid_format string, 'Too large' unless Integer === value + invalid_format string, "Too large" unless Integer === value when :bigint - invalid_format string, 'Not an integer' unless string =~ /^-?\d+$/ + invalid_format string, "Not an integer" unless string =~ /^-?\d+$/ value = string.to_i when :boolean value = !!(string =~ /t|true|y|yes|1/i) @@ -760,24 +777,29 @@ SQL invalid_format string end when :category_id - if string =~ /(.*)\/(.*)/ + if string =~ %r{(.*)/(.*)} parent_name = $1 child_name = $2 parent = Category.query_parent_category(parent_name) invalid_format string, "Could not find category named #{parent_name}" unless parent object = Category.query_category(child_name, parent) - invalid_format string, "Could not find subcategory of #{parent_name} named #{child_name}" unless object + unless object + invalid_format string, + "Could not find subcategory of #{parent_name} named #{child_name}" + end else - object = Category.where(id: string.to_i).first || Category.where(slug: string).first || Category.where(name: string).first + object = + Category.where(id: string.to_i).first || Category.where(slug: string).first || + Category.where(name: string).first invalid_format string, "Could not find category named #{string}" unless object end value = object.id when :user_id, :post_id, :topic_id, :group_id, :badge_id - if string.gsub(/[ _]/, '') =~ /^-?\d+$/ + if string.gsub(/[ _]/, "") =~ /^-?\d+$/ clazz_name = (/^(.*)_id$/.match(type.to_s)[1].classify.to_sym) begin - object = Object.const_get(clazz_name).with_deleted.find(string.gsub(/[ _]/, '').to_i) + object = Object.const_get(clazz_name).with_deleted.find(string.gsub(/[ _]/, "").to_i) value = object.id rescue ActiveRecord::RecordNotFound invalid_format string, "The specified #{clazz_name} was not found" @@ -790,13 +812,15 @@ SQL invalid_format string, "The user named #{string} was not found" end elsif type == :post_id - if string =~ /(\d+)\/(\d+)(\?u=.*)?$/ + if string =~ %r{(\d+)/(\d+)(\?u=.*)?$} object = Post.with_deleted.find_by(topic_id: $1, post_number: $2) - invalid_format string, "The post at topic:#{$1} post_number:#{$2} was not found" unless object + unless object + invalid_format string, "The post at topic:#{$1} post_number:#{$2} was not found" + end value = object.id end elsif type == :topic_id - if string =~ /\/t\/[^\/]+\/(\d+)/ + if string =~ %r{/t/[^/]+/(\d+)} begin object = Topic.with_deleted.find($1) value = object.id @@ -812,16 +836,16 @@ SQL invalid_format string end when :int_list - value = string.split(',').map { |s| s.downcase == '#null' ? nil : s.to_i } + value = string.split(",").map { |s| s.downcase == "#null" ? nil : s.to_i } invalid_format string, "can't be empty" if value.length == 0 when :string_list - value = string.split(',').map { |s| s.downcase == '#null' ? nil : s } + value = string.split(",").map { |s| s.downcase == "#null" ? nil : s } invalid_format string, "can't be empty" if value.length == 0 when :user_list - value = string.split(',').map { |s| User.find_by_username_or_email(s) } + value = string.split(",").map { |s| User.find_by_username_or_email(s) } invalid_format string, "can't be empty" if value.length == 0 else - raise TypeError.new('unknown parameter type??? should not get here') + raise TypeError.new("unknown parameter type??? should not get here") end value @@ -842,19 +866,15 @@ SQL default = $3 nullable = false if type =~ /^(null)?(.*?)(null)?$/i - if $1 || $3 - nullable = true - end + nullable = true if $1 || $3 type = $2 end type = type.strip begin ret_params << DataExplorer::Parameter.new(ident, type, default, nullable) - rescue - if opts[:strict] - raise - end + rescue StandardError + raise if opts[:strict] end false @@ -863,11 +883,8 @@ SQL else true end - else - if line =~ /^\s*--\s*\[params\]\s*$/ - in_params = true - end + in_params = true if line =~ /^\s*--\s*\[params\]\s*$/ false end end @@ -875,41 +892,42 @@ SQL end end - load File.expand_path('../lib/data_explorer_query_group_bookmarkable.rb', __FILE__) - load File.expand_path('../app/serializers/user_data_explorer_query_group_bookmark_serializer.rb', __FILE__) + load File.expand_path("../lib/data_explorer_query_group_bookmarkable.rb", __FILE__) + load File.expand_path( + "../app/serializers/user_data_explorer_query_group_bookmark_serializer.rb", + __FILE__, + ) # Making DataExplorer::QueryGroup Bookmarkable. Bookmark.register_bookmarkable(DataExplorerQueryGroupBookmarkable) - require_dependency 'application_controller' - require_dependency File.expand_path('../lib/queries.rb', __FILE__) + require_dependency "application_controller" + require_dependency File.expand_path("../lib/queries.rb", __FILE__) DataExplorer::Engine.routes.draw do root to: "query#index" - get 'queries' => "query#index" + get "queries" => "query#index" scope "/", defaults: { format: :json } do - get 'schema' => "query#schema" - get 'groups' => "query#groups" - post 'queries' => "query#create" - get 'queries/:id' => "query#show" - put 'queries/:id' => "query#update" - delete 'queries/:id' => "query#destroy" - post 'queries/:id/run' => "query#run", constraints: { format: /(json|csv)/ } + get "schema" => "query#schema" + get "groups" => "query#groups" + post "queries" => "query#create" + get "queries/:id" => "query#show" + put "queries/:id" => "query#update" + delete "queries/:id" => "query#destroy" + post "queries/:id/run" => "query#run", :constraints => { format: /(json|csv)/ } end end Discourse::Application.routes.append do - get '/g/:group_name/reports' => 'data_explorer/query#group_reports_index' - get '/g/:group_name/reports/:id' => 'data_explorer/query#group_reports_show' - post '/g/:group_name/reports/:id/run' => 'data_explorer/query#group_reports_run' + get "/g/:group_name/reports" => "data_explorer/query#group_reports_index" + get "/g/:group_name/reports/:id" => "data_explorer/query#group_reports_show" + post "/g/:group_name/reports/:id/run" => "data_explorer/query#group_reports_run" - mount ::DataExplorer::Engine, at: '/admin/plugins/explorer' + mount ::DataExplorer::Engine, at: "/admin/plugins/explorer" end - add_api_key_scope(:data_explorer, { - run_queries: { - actions: %w[data_explorer/query#run], - params: %i[id] - } - }) + add_api_key_scope( + :data_explorer, + { run_queries: { actions: %w[data_explorer/query#run], params: %i[id] } }, + ) end diff --git a/spec/data_explorer_spec.rb b/spec/data_explorer_spec.rb index 997eca2..d649d8a 100644 --- a/spec/data_explorer_spec.rb +++ b/spec/data_explorer_spec.rb @@ -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 diff --git a/spec/guardian_spec.rb b/spec/guardian_spec.rb index 5eb4659..33c8ab4 100644 --- a/spec/guardian_spec.rb +++ b/spec/guardian_spec.rb @@ -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 diff --git a/spec/integration/custom_api_key_scopes_spec.rb b/spec/integration/custom_api_key_scopes_spec.rb index 6cbc8a6..3a0cdb0 100644 --- a/spec/integration/custom_api_key_scopes_spec.rb +++ b/spec/integration/custom_api_key_scopes_spec.rb @@ -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,73 +21,83 @@ 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: { - "Api-Key" => all_queries_api_key.key, - "Api-Username" => admin.username - } + it "cannot hit any other endpoints" do + get "/latest.json", + headers: { + "Api-Key" => all_queries_api_key.key, + "Api-Username" => admin.username, + } expect(response.status).to eq(403) - get "/latest.json", headers: { - "Api-Key" => single_query_api_key.key, - "Api-Username" => admin.username - } + get "/latest.json", + headers: { + "Api-Key" => single_query_api_key.key, + "Api-Username" => admin.username, + } expect(response.status).to eq(403) - get "/u/#{admin.username}.json", headers: { - "Api-Key" => all_queries_api_key.key, - "Api-Username" => admin.username - } + get "/u/#{admin.username}.json", + headers: { + "Api-Key" => all_queries_api_key.key, + "Api-Username" => admin.username, + } expect(response.status).to eq(403) - get "/u/#{admin.username}.json", headers: { - "Api-Key" => single_query_api_key.key, - "Api-Username" => admin.username - } + get "/u/#{admin.username}.json", + headers: { + "Api-Key" => single_query_api_key.key, + "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: { - "Api-Key" => single_query_api_key.key, - "Api-Username" => admin.username - } + post "/admin/plugins/explorer/queries/#{query1.id}/run.json", + headers: { + "Api-Key" => single_query_api_key.key, + "Api-Username" => admin.username, + } }.to change { query1.reload.last_run_at } expect(response.status).to eq(200) expect(response.parsed_body["success"]).to eq(true) expect(response.parsed_body["columns"]).to eq(["query1_res"]) expect { - post "/admin/plugins/explorer/queries/#{query2.id}/run.json", headers: { - "Api-Key" => single_query_api_key.key, - "Api-Username" => admin.username - } + post "/admin/plugins/explorer/queries/#{query2.id}/run.json", + headers: { + "Api-Key" => single_query_api_key.key, + "Api-Username" => admin.username, + } }.not_to change { query2.reload.last_run_at } expect(response.status).to eq(403) end 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: { - "Api-Key" => all_queries_api_key.key, - "Api-Username" => admin.username - } + post "/admin/plugins/explorer/queries/#{query1.id}/run.json", + headers: { + "Api-Key" => all_queries_api_key.key, + "Api-Username" => admin.username, + } }.to change { query1.reload.last_run_at } expect(response.status).to eq(200) expect(response.parsed_body["success"]).to eq(true) expect(response.parsed_body["columns"]).to eq(["query1_res"]) expect { - post "/admin/plugins/explorer/queries/#{query2.id}/run.json", headers: { - "Api-Key" => all_queries_api_key.key, - "Api-Username" => admin.username - } + post "/admin/plugins/explorer/queries/#{query2.id}/run.json", + headers: { + "Api-Key" => all_queries_api_key.key, + "Api-Username" => admin.username, + } }.to change { query2.reload.last_run_at } expect(response.status).to eq(200) expect(response.parsed_body["success"]).to eq(true) diff --git a/spec/jobs/scheduled/delete_hidden_queries_spec.rb b/spec/jobs/scheduled/delete_hidden_queries_spec.rb index 005ecb3..4d2c20e 100644 --- a/spec/jobs/scheduled/delete_hidden_queries_spec.rb +++ b/spec/jobs/scheduled/delete_hidden_queries_spec.rb @@ -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) diff --git a/spec/lib/data_explorer_query_group_bookmarkable_spec.rb b/spec/lib/data_explorer_query_group_bookmarkable_spec.rb index 27388c0..70c516a 100644 --- a/spec/lib/data_explorer_query_group_bookmarkable_spec.rb +++ b/spec/lib/data_explorer_query_group_bookmarkable_spec.rb @@ -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", - description: "This is the description of my 1st query.", - sql: "Not really important", - user: admin_user) } - fab!(:query2) { Fabricate(:query, name: "My Second Query", - description: "This is my 2nd query's description.", - sql: "Not really important", - user: admin_user) } + 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, + ) + 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, + ) + 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, - bookmarkable: query_group2, - name: "something else i have to do") } + 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", + ) + end # User also bookmarked Query 2 from Group 1. - let!(:bookmark3) { Fabricate(:bookmark, user: user, - bookmarkable: query_group3, - name: "this is the other query I needed.") } + let!(:bookmark3) do + Fabricate( + :bookmark, + user: user, + bookmarkable: query_group3, + 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 diff --git a/spec/requests/group_spec.rb b/spec/requests/group_spec.rb index 17278d1..94163e9 100644 --- a/spec/requests/group_spec.rb +++ b/spec/requests/group_spec.rb @@ -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) } diff --git a/spec/requests/query_controller_spec.rb b/spec/requests/query_controller_spec.rb index e89ba83..0ae3d8a 100644 --- a/spec/requests/query_controller_spec.rb +++ b/spec/requests/query_controller_spec.rb @@ -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,29 +89,32 @@ 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: { - "query" => { - "name" => query.name, - "description" => query.description, - "sql" => query.sql, - "user_id" => query.user_id, - "created_at" => query.created_at, - "group_ids" => [group2.id], - "last_run_at" => query.last_run_at - }, - "id" => query.id } + put "/admin/plugins/explorer/queries/#{query.id}.json", + params: { + "query" => { + "name" => query.name, + "description" => query.description, + "sql" => query.sql, + "user_id" => query.user_id, + "created_at" => query.created_at, + "group_ids" => [group2.id], + "last_run_at" => query.last_run_at, + }, + "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: { - "query" => { - "name" => "", - }, - "id" => query.id } + put "/admin/plugins/explorer/queries/#{query.id}", + params: { + "query" => { + "name" => "", + }, + "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) diff --git a/spec/tasks/data_explorer_spec.rb b/spec/tasks/data_explorer_spec.rb index 9d11178..b13165f 100644 --- a/spec/tasks/data_explorer_spec.rb +++ b/spec/tasks/data_explorer_spec.rb @@ -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) diff --git a/spec/tasks/fix_query_ids_spec.rb b/spec/tasks/fix_query_ids_spec.rb index 1b2f38f..dbd5f18 100644 --- a/spec/tasks/fix_query_ids_spec.rb +++ b/spec/tasks/fix_query_ids_spec.rb @@ -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