diff --git a/.rubocop.yml b/.rubocop.yml index 1cbf9c9..f2b31bd 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -56,4 +56,8 @@ Layout/CaseIndentation: IndentOneStep: true Style/NumericPredicate: + Enabled: false + +# I've always been an "unnecessary this->" guy myself. +Style/RedundantSelf: Enabled: false \ No newline at end of file diff --git a/Gemfile b/Gemfile index c9358ec..e829a60 100644 --- a/Gemfile +++ b/Gemfile @@ -11,16 +11,16 @@ gem 'sprockets-rails' gem 'pg', '~> 1.1' gem 'redis' +# Views +gem 'kaminari' # Must be included before ElasticSearch +gem 'redcarpet' +gem 'slim-rails' + # Search gem 'elasticsearch-model' gem 'fancy_searchable', github: 'Twibooru/fancy_searchable', ref: '40687c9' gem 'model-msearch' -# Views -gem 'kaminari' -gem 'redcarpet' -gem 'slim-rails' - # Programs gem 'puma', '~> 5.0' gem 'sidekiq' diff --git a/Gemfile.lock b/Gemfile.lock index d1c5b78..b1af439 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -82,7 +82,6 @@ GEM rake (>= 10.4, < 14.0) ast (2.4.2) base64 (0.2.0) - bcrypt (3.1.20) bindex (0.8.1) builder (3.2.4) bullet (7.1.6) @@ -301,7 +300,6 @@ PLATFORMS DEPENDENCIES annotate - bcrypt bullet capybara debug diff --git a/app/controllers/authors_controller.rb b/app/controllers/authors_controller.rb index 4e96a7a..6190424 100644 --- a/app/controllers/authors_controller.rb +++ b/app/controllers/authors_controller.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true class AuthorsController < ApplicationController def show - @author = Author.find(params[:id]) + author = Author.select(:name).find(params[:id]) + scope = SearchScope.new(nil, { author: author.name }) + + redirect_to "/search?scope=#{scope.scope_key}" end end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index d86de57..9247a12 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -11,11 +11,20 @@ class SearchController < ApplicationController end def search - unless setup_scope - return + # unless params[:scope] + # return redirect_to root_path + # end + + @scope = SearchScope.new(params) + + # The scope is valid if was successfully used to load the existing search params + unless @scope.scope_loaded + return redirect_to "/search?scope=#{@scope.scope_key}" end - using_random = @search_params['luck'].present? + @search_params = @scope.search_params + + using_random = @search_params[:luck].present? # This was mainly written this way to match the way FiMFetch's query interface looks, without using JS. # I should do a Derpibooru-esque textual search system sometime. @@ -23,8 +32,8 @@ class SearchController < ApplicationController per_page: @per_page, page: @page_num ) do |s| - s.add_query(match: { title: { query: @search_params['q'], operator: :and } }) if @search_params['q'].present? - s.add_query(match: { author: { query: @search_params['author'], operator: :and } }) if @search_params['author'].present? + s.add_query(match: { title: { query: @search_params[:q], operator: :and } }) if @search_params[:q].present? + s.add_query(match: { author: { query: @search_params[:author], operator: :and } }) if @search_params[:author].present? # tags -> match any of the included tags, exclude any of the excluded tags tag_musts, tag_must_nots = parse_tag_queries @@ -42,13 +51,13 @@ class SearchController < ApplicationController # ratings -> match stories with any of them s.add_filter(bool: { - should: @search_params['ratings'].keys.map { |k| { term: { content_rating: k } } } - }) if @search_params['ratings'].present? + should: @search_params[:ratings].keys.map { |k| { term: { content_rating: k } } } + }) if @search_params[:ratings].present? # completeness -> match stories with any of them s.add_filter(bool: { - should: @search_params['state'].keys.map { |k| { term: { completion_status: k } } } - }) if @search_params['state'].present? + should: @search_params[:state].keys.map { |k| { term: { completion_status: k } } } + }) if @search_params[:state].present? # sort direction if using_random @@ -75,7 +84,7 @@ class SearchController < ApplicationController # returns: [included tags, excluded tags] def parse_tag_queries - tag_searches = "#{@search_params['tags']},#{@search_params['characters']}".split(',').reject(&:blank?) + tag_searches = "#{@search_params[:tas]},#{@search_params[:characters]}".split(',').reject(&:blank?) [ tag_searches.reject { |t| t[0] == '-' }, @@ -85,8 +94,8 @@ class SearchController < ApplicationController end def parse_sort - sf = ALLOWED_SORT_FIELDS.detect { |f| @search_params['sf'] == f.to_s } || :date_updated - sd = ALLOWED_SORT_DIRS.detect { |d| @search_params['sd'] == d.to_s } || :desc + sf = ALLOWED_SORT_FIELDS.detect { |f| @search_params[:sf] == f.to_s } || :date_updated + sd = ALLOWED_SORT_DIRS.detect { |d| @search_params[:sd] == d.to_s } || :desc # we need to sort on the keyword versions of text fields, to avoid using fielddata. sf = case sf @@ -102,40 +111,4 @@ class SearchController < ApplicationController {sf => sd} end - - # FIXME: This is some of the worst Ruby code I have ever written. - def setup_scope - @scope_key = Random.hex(16) - scope_valid = false - - # scope passed, try to look it up in redis and use the search params from it - if params[:scope].present? - result = $redis.get("search_scope/#{params[:scope]}") - if result.present? - @search_params = JSON.parse(result) - @scope_key = params[:scope] - scope_valid = true - else - redirect_to '/' - return false - end - else - @search_params = params - end - - # you can't JSON.dump a Parameters - if @search_params.is_a? ActionController::Parameters - @search_params = @search_params.permit!.to_h - end - $redis.setex("search_scope/#{@scope_key}", 3600, JSON.dump(@search_params)) - - # if the scope was invalid (no key passed, or invalid key passed), redirect to the results page - # with the new key we just generated for the params we had. - unless scope_valid - redirect_to "/search?scope=#{@scope_key}" - return false - end - - true - end end diff --git a/app/lib/search_scope.rb b/app/lib/search_scope.rb new file mode 100644 index 0000000..514757c --- /dev/null +++ b/app/lib/search_scope.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +class SearchScope + attr_reader :search_params, :scope_key, :scope_loaded + + def initialize(params, search_params = nil) + @scope_key = SecureRandom.hex 16 + @scope_loaded = false + + if search_params + @search_params = search_params + self.persist + elsif params + self.load_from_params(params) + end + end + + private + + def load_from_params(params) + if params[:scope].present? + result = $redis.get("search_scope/#{params[:scope]}") + if result.present? + @search_params = JSON.parse(result, symbolize_names: true) + @scope_key = params[:scope] + @scope_loaded = true + self.persist # refresh the expiry + end + else + @search_params = params.permit!.to_h.symbolize_keys + self.persist + end + end + + def persist + # Nice long expiry so nobody's search disappears if they don't touch it for awhile. + $redis.setex("search_scope/#{@scope_key}", 1.day.in_seconds, JSON.dump(@search_params)) + end +end diff --git a/app/views/search/_advanced.html.slim b/app/views/search/_advanced.html.slim index ce46947..a0643ef 100644 --- a/app/views/search/_advanced.html.slim +++ b/app/views/search/_advanced.html.slim @@ -9,28 +9,28 @@ div.search-adv b Rating br = label_tag "ratings_everyone" - = check_box_tag "ratings[everyone]", 1, @search_params.dig('ratings', 'everyone').present? + = check_box_tag "ratings[everyone]", 1, @search_params.dig(:ratings, 'everyone').present? | Everyone = label_tag "ratings_teen" - = check_box_tag "ratings[teen]", 1, @search_params.dig('ratings', 'teen').present? + = check_box_tag "ratings[teen]", 1, @search_params.dig(:ratings, 'teen').present? | Teen = label_tag "ratings_mature" - = check_box_tag "ratings[mature]", 1, @search_params.dig('ratings', 'mature').present? + = check_box_tag "ratings[mature]", 1, @search_params.dig(:ratings, 'mature').present? | Mature .opts b Story State br = label_tag "state_complete" - = check_box_tag "state[complete]", 1, @search_params.dig('state', 'complete').present? + = check_box_tag "state[complete]", 1, @search_params.dig(:state, 'complete').present? | Complete = label_tag "state_incomplete" - = check_box_tag "state[incomplete]", 1, @search_params.dig('state', 'incomplete').present? + = check_box_tag "state[incomplete]", 1, @search_params.dig(:state, 'incomplete').present? | Incomplete = label_tag "state_hiatus" - = check_box_tag "state[hiatus]", 1, @search_params.dig('state', 'hiatus').present? + = check_box_tag "state[hiatus]", 1, @search_params.dig(:state, 'hiatus').present? | On Hiatus = label_tag "state_cancelled" - = check_box_tag "state[cancelled]", 1, @search_params.dig('state', 'cancelled').present? + = check_box_tag "state[cancelled]", 1, @search_params.dig(:state, 'cancelled').present? | Cancelled /.opts b Story Age @@ -73,7 +73,7 @@ div.search-adv .opts b Author: br - = text_field_tag :author, @search_params['author'] + = text_field_tag :author, @search_params[:author] /.cols .opts b FiMFiction Rating: @@ -104,7 +104,7 @@ div.search-adv br .js-tag-editor .selected-tags - = text_field_tag :characters, @search_params['characters'] + = text_field_tag :characters, @search_params[:characters] = select_tag :fancy_tags, options_for_select(@character_tags) .cols .opts @@ -112,15 +112,15 @@ div.search-adv br .js-tag-editor .selected-tags - = text_field_tag :tags, @search_params['tags'] + = text_field_tag :tags, @search_params[:tags] = select_tag :fancy_characters, options_for_select(@other_tags) .cols .opts b Sort By br => select_tag :sf, options_for_select({'Query Relevance' => 'rel', 'Title' => 'title', 'Author' => 'author', \ - 'Publish Date' => 'date_published', 'Updated Date' => 'date_updated', 'Word Count' => 'num_words'}, @search_params['sf']) - = select_tag :sd, options_for_select({'High to Low' => 'desc', 'Low to High' => 'asc'}, @search_params['sd']) + 'Publish Date' => 'date_published', 'Updated Date' => 'date_updated', 'Word Count' => 'num_words'}, @search_params[:sf]) + = select_tag :sd, options_for_select({'High to Low' => 'desc', 'Low to High' => 'asc'}, @search_params[:sd]) - if show_button .buttons = submit_tag 'Go Fetch!', name: 'search' \ No newline at end of file diff --git a/app/views/search/search.html.slim b/app/views/search/search.html.slim index b8b47b2..11a04f5 100644 --- a/app/views/search/search.html.slim +++ b/app/views/search/search.html.slim @@ -5,7 +5,7 @@ a.logo href="/" = image_tag '/img/logo_small.png' .searchbox - = search_field_tag 'q', @search_params['q'], placeholder: 'Search story titles...' + = search_field_tag :q, @search_params[:q], placeholder: 'Search story titles...' = render partial: 'advanced', locals: { show_button: true } - @records.each do |rec| @@ -57,6 +57,6 @@ ' Last Update span= rec.date_updated .searchnav - = paginate(@records, params: { scope: @scope_key }) + = paginate(@records, params: { scope: @scope.scope_key }) br span.searchtime Found #{@search.total_count} results. \ No newline at end of file