diff --git a/app/controllers/articles_controller.rb b/app/controllers/articles_controller.rb index 91900f162014f..526b93d872788 100644 --- a/app/controllers/articles_controller.rb +++ b/app/controllers/articles_controller.rb @@ -2,7 +2,8 @@ class ArticlesController < ApplicationController include ApplicationHelper # NOTE: It seems quite odd to not authenticate the user for the :new action. - before_action :authenticate_user!, except: %i[feed new] + # before_action :authenticate_user!, except: %i[feed new] + before_action :authenticate_user!, except: %i[feed new search] before_action :set_article, only: %i[edit manage update destroy stats admin_unpublish admin_featured_toggle] # NOTE: Consider pushing this check into the associated Policy. We could choose to raise a # different error which we could then rescue as part of our exception handling. @@ -65,6 +66,20 @@ def feed } end + def search + skip_authorization + + query = params[:query].to_s.strip + sort_option = params[:sort] || "relevance" # Default to relevance if no sort option is provided + + @articles = Article.search_with_sort(query: query, sort_option: sort_option) + + respond_to do |format| + format.html { render :index } + format.json { render json: @articles } + end + end + # @note The /new path is a unique creature. We want to ensure that folks coming to the /new with # a prefill of information are first prompted to sign-in, and then given a form that # prepopulates with that pre-fill information. This is a feature that StackOverflow and diff --git a/app/models/article.rb b/app/models/article.rb index e78fc6804a70e..4c1b62f1baa27 100644 --- a/app/models/article.rb +++ b/app/models/article.rb @@ -326,6 +326,7 @@ def self.unique_url_error } }, ignoring: :accents + ranked_by: ":tsearch + (1.0 / (NOW() - published_at))" # [@jgaskins] We use an index on `published`, but since it's a boolean value # the Postgres query planner often skips it due to lack of diversity of the @@ -391,29 +392,48 @@ def self.unique_url_error :body_markdown, :email_digest_eligible, :processed_html, :co_author_ids, :score, :type_of) } + scope :search_and_sort, lambda { |query, sort_option| + articles = search_articles(query).published + case sort_option + when "newest" + articles.order(published_at: :desc) + when "oldest" + articles.order(published_at: :asc) + when "relevance" + articles + else + articles.order(created_at: :desc) + end + } + scope :sorting, lambda { |value| - value ||= "creation-desc" - kind, dir = value.split("-") - - dir = "desc" unless %w[asc desc].include?(dir) - - case kind - when "creation" - order(created_at: dir) - when "views" - order(page_views_count: dir) - when "reactions" - order(public_reactions_count: dir) - when "comments" - order(comments_count: dir) - when "published" - # NOTE: For recently published, we further filter to only published posts - order(published_at: dir).published - else - order(created_at: dir) - end + value ||= "creation-desc" + kind, dir = value.split("-") + + dir = "desc" unless %w[asc desc].include?(dir) + + case kind + when "creation" + order(created_at: dir) + when "views" + order(page_views_count: dir) + when "reactions" + order(public_reactions_count: dir) + when "comments" + order(comments_count: dir) + when "published" + order(published_at: dir).published + when "search_newest" + order(published_at: :desc).search_articles + else + order(created_at: dir) + end } + def self.search_with_sort(query:, sort_option:) + search_and_sort(query, sort_option).limit(DEFAULT_FEED_PAGINATION_WINDOW_SIZE) + end + # @note This includes the `featured` scope, which may or may not be # something we expose going forward. However, it was # something used in two of the three queries we had that diff --git a/app/models/articles/feeds.rb b/app/models/articles/feeds.rb index bd0e3afaf0c4f..565c92058ca01 100644 --- a/app/models/articles/feeds.rb +++ b/app/models/articles/feeds.rb @@ -65,6 +65,18 @@ def self.oldest_published_at_to_consider_for(user:, days_since_published: DEFAUL def self.feed_for(controller:, user:, number_of_articles:, page:, tag:, type_of: "discover") variant = AbExperiment.get_feed_variant_for(controller: controller, user: user) + # Ensure relevancy and recency sorting for "Newest" + if type_of == "search_sort_newest" + return VariantQuery.build_for( + variant: :relevancy_score_and_publication_date, + user: user, + number_of_articles: number_of_articles, + page: page, + tag: tag, + type_of: type_of + ) + end + VariantQuery.build_for( variant: variant, user: user, diff --git a/app/views/admin/users/index/_applied_filters.html.erb b/app/views/admin/users/index/_applied_filters.html.erb index 5cceb7085cc3c..c78dd0d1e4b0c 100644 --- a/app/views/admin/users/index/_applied_filters.html.erb +++ b/app/views/admin/users/index/_applied_filters.html.erb @@ -22,6 +22,19 @@ <%= crayons_icon_tag("x.svg", class: "c-pill__action-icon", aria_hidden: true) %> <% end %> + + <% if params[:search].present? %> + + <% end %> + + <% params[:organizations]&.each do |org_id| %> <% org = @organizations.find_by(id: org_id) %> <% end %> - <% if params[:roles] || params[:organizations] || params[:joining_start].present? || params[:joining_end].present? || params[:statuses] %> - + <% if params[:roles] || params[:organizations] || params[:joining_start].present? || params[:joining_end].present? || params[:statuses] || params[:search].present? %> + <% end %> diff --git a/app/views/admin/users/index/_controls.html.erb b/app/views/admin/users/index/_controls.html.erb index 957607c348f9c..de9b60405490e 100644 --- a/app/views/admin/users/index/_controls.html.erb +++ b/app/views/admin/users/index/_controls.html.erb @@ -13,6 +13,7 @@
<%= render "admin/users/controls/expand_search_button" %>
+ <%= f.text_field :search, placeholder: t("views.admin.users.search.placeholder"), class: "crayons-textfield crayons-textfield--sm ml-2" if params[:search].present? %> <%= render "admin/users/controls/export" %> diff --git a/app/views/admin/users/index/_filters_modal.html.erb b/app/views/admin/users/index/_filters_modal.html.erb index 4ba6234f79f68..41e17d98cced5 100644 --- a/app/views/admin/users/index/_filters_modal.html.erb +++ b/app/views/admin/users/index/_filters_modal.html.erb @@ -6,6 +6,28 @@

<%= t("views.admin.users.filters.desc") %>

+ +
+ + + <%= t("views.admin.users.filters.summary.keyword") %> + "> + + + + <%= crayons_icon_tag("chevron-down", aria_hidden: true, class: "summary-icon") %> + +
+ <%= t("views.admin.users.filters.legend.keyword") %> +
+ <%= f.text_field :search, placeholder: t("views.admin.users.filters.keyword.placeholder"), class: "crayons-textfield", value: params[:search] %> +
+ +
+