diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 39dbfe2e4710f9b56bdb7bc1d9254c25bbe92065..726c0907c3df66c013e1923fd92892b01eb5e750 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -1,7 +1,7 @@ class TagsController < ApplicationController before_action :authenticate_user!, only: [:edit, :update] before_action :set_category, except: [:index] - before_action :set_tag, only: [:show, :edit, :update] + before_action :set_tag, only: [:show, :edit, :update, :children] def index @tag_set = if params[:tag_set].present? @@ -24,16 +24,21 @@ class TagsController < ApplicationController @tags = if params[:q].present? @tag_set.tags.search(params[:q]) else - @tag_set.tags.left_joins(:posts).group(Arel.sql('tags.id')).order(Arel.sql('COUNT(posts.id) DESC')) - .select(Arel.sql('tags.*, COUNT(posts.id) AS post_count')) - end + @tag_set.tags.order(Arel.sql('COUNT(posts.id) DESC')) + end.left_joins(:posts).group(Arel.sql('tags.id')).select(Arel.sql('tags.*, COUNT(posts.id) AS post_count')) + .paginate(per_page: 96, page: params[:page]) end def show sort_params = { activity: { last_activity: :desc }, age: { created_at: :desc }, score: { score: :desc }, native: Arel.sql('att_source IS NULL DESC, last_activity DESC') } sort_param = sort_params[params[:sort]&.to_sym] || { last_activity: :desc } - @posts = @tag.posts.undeleted.where(post_type_id: @category.display_post_types) + tag_ids = if params[:self].present? + [@tag.id] + else + @tag.all_children + [@tag.id] + end + @posts = Post.joins(:tags).where(tags: { id: tag_ids }).undeleted.where(post_type_id: @category.display_post_types) .includes(:post_type, :tags).list_includes.paginate(page: params[:page], per_page: 50) .order(sort_param) end @@ -48,17 +53,19 @@ class TagsController < ApplicationController end end + def children + @tags = if params[:q].present? + @tag.children.search(params[:q]) + else + @tag.children.order(Arel.sql('COUNT(posts.id) DESC')) + end.left_joins(:posts).group(Arel.sql('tags.id')).select(Arel.sql('tags.*, COUNT(posts.id) AS post_count')) + .paginate(per_page: 96, page: params[:page]) + end + private def set_tag @tag = Tag.find params[:tag_id] - required_ids = @category&.required_tag_ids - moderator_ids = @category&.moderator_tag_ids - topic_ids = @category&.topic_tag_ids - required = required_ids&.include?(@tag.id) ? 'is-filled' : '' - topic = topic_ids&.include?(@tag.id) ? 'is-outlined' : '' - moderator = moderator_ids&.include?(@tag.id) ? 'is-red is-outlined' : '' - @classes = "badge is-tag #{required} #{topic} #{moderator}" end def set_category @@ -66,6 +73,6 @@ class TagsController < ApplicationController end def tag_params - params.require(:tag).permit(:excerpt, :wiki_markdown) + params.require(:tag).permit(:excerpt, :wiki_markdown, :parent_id) end end diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb index 24185619b74b7f546f76529149a11dc68d1dc00d..83a063b6040e33d6b5b2545f81c09cdeb18c6b50 100644 --- a/app/helpers/tags_helper.rb +++ b/app/helpers/tags_helper.rb @@ -7,4 +7,14 @@ module TagsHelper topic_ids.include?(t.id) ? 0 : 1, t.id] end end + + def tag_classes(tag, category) + required_ids = category&.required_tag_ids + moderator_ids = category&.moderator_tag_ids + topic_ids = category&.topic_tag_ids + required = required_ids&.include?(tag.id) ? 'is-filled' : '' + topic = topic_ids&.include?(tag.id) ? 'is-outlined' : '' + moderator = moderator_ids&.include?(tag.id) ? 'is-red is-outlined' : '' + "badge is-tag #{required} #{topic} #{moderator}" + end end diff --git a/app/models/tag.rb b/app/models/tag.rb index efe46842d65bc9e1825082bc8e2018f3f4f574fc..43436d17b89745aa1504585d756244c521e1b904 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -2,7 +2,9 @@ class Tag < ApplicationRecord include CommunityRelated has_and_belongs_to_many :posts + has_many :children, class_name: 'Tag', foreign_key: :parent_id belongs_to :tag_set + belongs_to :parent, class_name: 'Tag', optional: true validates :excerpt, length: { maximum: 600 }, allow_blank: true validates :wiki_markdown, length: { maximum: 30000 }, allow_blank: true @@ -11,4 +13,10 @@ class Tag < ApplicationRecord where('name LIKE ?', "%#{sanitize_sql_like(term)}%") .order(sanitize_sql_array(['name LIKE ? DESC, name', "#{sanitize_sql_like(term)}%"])) end + + def all_children + query = File.read(Rails.root.join('db/scripts/tag_children.sql')) + query = query.gsub('$ParentId', id.to_s) + ActiveRecord::Base.connection.execute(query).to_a.map(&:first) + end end diff --git a/app/views/tags/_list.html.erb b/app/views/tags/_list.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..7b4ffc202edee459079d59a0278e4876177abb1c --- /dev/null +++ b/app/views/tags/_list.html.erb @@ -0,0 +1,13 @@ +<div class="grid"> + <% required_ids = @category&.required_tag_ids %> + <% moderator_ids = @category&.moderator_tag_ids %> + <% topic_ids = @category&.topic_tag_ids %> + + <% @tags.each do |tag| %> + <% required = required_ids&.include?(tag.id) ? 'is-filled' : '' %> + <% topic = topic_ids&.include?(tag.id) ? 'is-outlined' : '' %> + <% moderator = moderator_ids&.include?(tag.id) ? 'is-red is-outlined' : '' %> + <% classes = "badge is-tag #{required} #{topic} #{moderator}" %> + <%= render 'tag', category: @category, tag: tag, classes: classes %> + <% end %> +</div> \ No newline at end of file diff --git a/app/views/tags/category.html.erb b/app/views/tags/category.html.erb index 32f8fd292e9ad58b2709560119b51f8c3a74a4e8..00d461de38226f46eb6b0ad5a55f96a73c8a3566 100644 --- a/app/views/tags/category.html.erb +++ b/app/views/tags/category.html.erb @@ -2,16 +2,18 @@ <h1>Tags for <%= @category.name %></h1> -<div class="grid"> - <% required_ids = @category&.required_tag_ids %> - <% moderator_ids = @category&.moderator_tag_ids %> - <% topic_ids = @category&.topic_tag_ids %> +<%= form_tag category_tags_path(@category), method: :get, class: 'form-inline' do %> + <div class="form-group-horizontal"> + <div class="form-group"> + <%= label_tag :q, 'Search', class: 'form-element' %> + <%= text_field_tag :q, params[:q], class: 'form-element' %> + </div> + <div class="actions has-padding-bottom-1"> + <button type="submit" class="button is-filled is-medium"><i class="fas fa-search"></i><span class="sr-only">Search</span></button> + </div> + </div> +<% end %> - <% @tags.each do |tag| %> - <% required = required_ids&.include?(tag.id) ? 'is-filled' : '' %> - <% topic = topic_ids&.include?(tag.id) ? 'is-outlined' : '' %> - <% moderator = moderator_ids&.include?(tag.id) ? 'is-red is-outlined' : '' %> - <% classes = "badge is-tag #{required} #{topic} #{moderator}" %> - <%= render 'tag', category: @category, tag: tag, classes: classes %> - <% end %> -</div> +<%= render 'list' %> + +<%= will_paginate @tags, renderer: BootstrapPagination::Rails %> diff --git a/app/views/tags/children.html.erb b/app/views/tags/children.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..92cc1e14b5339f1d1df57be9c687bcdac33a358f --- /dev/null +++ b/app/views/tags/children.html.erb @@ -0,0 +1,19 @@ +<% content_for :title, "Child tags of #{@tag.name}" %> + +<h1>Child tags of <span class="<%= tag_classes(@tag, @category) %> is-large"><%= @tag.name %></span></h1> + +<%= form_tag tag_children_path(id: @category.id, tag_id: @tag.id), method: :get, class: 'form-inline' do %> + <div class="form-group-horizontal"> + <div class="form-group"> + <%= label_tag :q, 'Search', class: 'form-element' %> + <%= text_field_tag :q, params[:q], class: 'form-element' %> + </div> + <div class="actions has-padding-bottom-1"> + <button type="submit" class="button is-filled is-medium"><i class="fas fa-search"></i><span class="sr-only">Search</span></button> + </div> + </div> +<% end %> + +<%= render 'list' %> + +<%= will_paginate @tags, renderer: BootstrapPagination::Rails %> diff --git a/app/views/tags/edit.html.erb b/app/views/tags/edit.html.erb index 487d673dbf76a22495b9a82ea0c5e9e226ed7dfa..830c7afeff9c223c96191340f3a65533276dcedd 100644 --- a/app/views/tags/edit.html.erb +++ b/app/views/tags/edit.html.erb @@ -19,6 +19,17 @@ <% end %> <%= form_for @tag, url: update_tag_path(id: @category.id, tag_id: @tag.id) do |f| %> + <div class="form-group"> + <%= f.label :parent_id, 'Parent tag', class: 'form-element' %> + <span class="form-caption"> + Optional. Select a parent tag to make this part of a tag hierarchy. + </span> + <%= f.select :parent_id, options_for_select(@tag.parent.present? ? [[@tag.parent.name, @tag.parent_id]] : [], + selected: @tag.parent.present? ? @tag.parent_id : nil), + { include_blank: true }, class: "form-element js-tag-select", + data: { tag_set: @category.tag_set_id, use_ids: true } %> + </div> + <div class="form-group"> <%= f.label :excerpt, 'Usage guidance', class: 'form-element' %> <span class="form-caption"> diff --git a/app/views/tags/show.html.erb b/app/views/tags/show.html.erb index 9f815fda137ee3f61cdc4933c86ebf7ca34b000d..b1b0478f9ed0f162f7230a383d598ca47bd07800 100644 --- a/app/views/tags/show.html.erb +++ b/app/views/tags/show.html.erb @@ -1,12 +1,25 @@ <% content_for :title, "Posts tagged #{@tag.name}" %> -<h1> - Posts tagged <span class="<%= @classes %> is-large"><%= @tag.name %></span> +<h1 class="has-margin-0 has-margin-top-4"> + Posts tagged <span class="<%= tag_classes(@tag, @category) %> is-large"><%= @tag.name %></span> </h1> +<p class="has-color-tertiary-900 has-font-weight-normal has-margin-0 has-font-family-brand"> + <% if @tag.parent_id.present? %> + Subtag of <%= link_to @tag.parent.name, tag_path(id: @category.id, tag_id: @tag.parent_id), + class: tag_classes(@tag.parent, @category) %> + <% end %> + <% child_count = @tag.children.count %> + <% if @tag.parent_id.present? && child_count > 0 %> + · + <% end %> + <% if child_count > 0 %> + <%= link_to pluralize(child_count, 'child tag'), tag_children_path(id: @category.id, tag_id: @tag.id) %> + <% end %> +</p> <div class="widget"> <div class="widget--body has-font-size-caption has-color-tertiary-900"> - <%= raw(sanitize(@tag.excerpt, scrubber: scrubber).gsub("\n", '<br/>')) %> + <%= raw(sanitize(@tag.excerpt, scrubber: scrubber)&.gsub("\n", '<br/>')) %> <% unless @tag.excerpt.present? %> <p class="has-font-size-caption has-margin-0"> <em> @@ -45,17 +58,10 @@ </div> <div class="button-list is-gutterless has-margin-2"> - <%= link_to 'Activity', query_url(sort: 'activity'), - class: "button is-muted is-outlined #{(params[:sort].nil?) && !current_page?(questions_lottery_path) || - params[:sort] == 'activity' ? 'is-active' : ''}" %> - <%= link_to 'Age', query_url(sort: 'age'), - class: "button is-muted is-outlined #{params[:sort] == 'age' ? 'is-active' : ''}" %> - <%= link_to 'Score', query_url(sort: 'score'), - class: "button is-muted is-outlined #{params[:sort] == 'score' ? 'is-active' : ''}" %> - <% if SiteSetting['AllowContentTransfer'] %> - <%= link_to 'Native', query_url(sort: 'native'), - class: "button is-muted is-outlined #{params[:sort] == 'native' ? 'is-active' : ''}" %> - <% end %> + <%= link_to 'Tag Only', query_url(self: 1), + class: "button is-muted is-outlined #{params[:self].present? ? 'is-active' : ''}" %> + <%= link_to 'Tag + Children', tag_path(id: @category.id, tag_id: @tag.id), + class: "button is-muted is-outlined #{params[:self].nil? ? 'is-active' : ''}" %> </div> </div> diff --git a/config/routes.rb b/config/routes.rb index 52a026f19b0af84a4465dd060efe0c12fa33c196..b4da278357d1a54a7c84cb926278b9972187e9cd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -182,9 +182,10 @@ Rails.application.routes.draw do delete ':id', to: 'categories#destroy', as: :destroy_category get ':id/feed', to: 'categories#rss_feed', as: :category_feed get ':id/tags', to: 'tags#category', as: :category_tags - get ':id/tags/:tag_id', to: 'tags#show', as: :tag - get ':id/tags/:tag_id/edit', to: 'tags#edit', as: :edit_tag - patch ':id/tags/:tag_id/edit', to: 'tags#update', as: :update_tag + get ':id/tags/:tag_id', to: 'tags#show', as: :tag + get ':id/tags/:tag_id/children', to: 'tags#children', as: :tag_children + get ':id/tags/:tag_id/edit', to: 'tags#edit', as: :edit_tag + patch ':id/tags/:tag_id/edit', to: 'tags#update', as: :update_tag end get 'warning', to: 'mod_warning#current', as: :current_mod_warning diff --git a/db/migrate/20200630105117_allow_tag_parents.rb b/db/migrate/20200630105117_allow_tag_parents.rb new file mode 100644 index 0000000000000000000000000000000000000000..864a821fcdeb0b62acd2697f942bb1c0730ecc02 --- /dev/null +++ b/db/migrate/20200630105117_allow_tag_parents.rb @@ -0,0 +1,5 @@ +class AllowTagParents < ActiveRecord::Migration[5.2] + def change + add_reference :tags, :parent, foreign_key: { to_table: :tags } + end +end diff --git a/db/schema.rb b/db/schema.rb index 8193a72045fb86b874e62a1a2a7760bf6a6aacb9..3578990e13a60f58a59fc67ec9d9f289d7681a41 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_06_30_001048) do +ActiveRecord::Schema.define(version: 2020_06_30_105117) do create_table "active_storage_attachments", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", force: :cascade do |t| t.string "name", null: false @@ -366,7 +366,9 @@ ActiveRecord::Schema.define(version: 2020_06_30_001048) do t.text "wiki_markdown" t.text "wiki" t.text "excerpt" + t.bigint "parent_id" t.index ["community_id"], name: "index_tags_on_community_id" + t.index ["parent_id"], name: "index_tags_on_parent_id" t.index ["tag_set_id"], name: "index_tags_on_tag_set_id" end @@ -468,6 +470,7 @@ ActiveRecord::Schema.define(version: 2020_06_30_001048) do add_foreign_key "suggested_edits", "users" add_foreign_key "suggested_edits", "users", column: "decided_by_id" add_foreign_key "tags", "communities" + add_foreign_key "tags", "tags", column: "parent_id" add_foreign_key "votes", "communities" add_foreign_key "warning_templates", "communities" add_foreign_key "warnings", "community_users" diff --git a/db/scripts/tag_children.sql b/db/scripts/tag_children.sql new file mode 100644 index 0000000000000000000000000000000000000000..0fbc433ba5b51b2c3b74100f7bb35b96a28afb9a --- /dev/null +++ b/db/scripts/tag_children.sql @@ -0,0 +1,10 @@ +WITH RECURSIVE CTE (id, group_id) AS ( + SELECT id, parent_id + FROM tags + WHERE parent_id = $ParentId + UNION ALL + SELECT t.id, t.parent_id + FROM tags t + INNER JOIN CTE ON t.parent_id = CTE.id +) +SELECT * FROM CTE; \ No newline at end of file diff --git a/test/fixtures/tags.yml b/test/fixtures/tags.yml index 8c9f95358088d31a604d2c1ff77e842670c5795f..d35e8f1c03e2499496f4668a33f426d6933f31c5 100644 --- a/test/fixtures/tags.yml +++ b/test/fixtures/tags.yml @@ -1,35 +1,29 @@ discussion: name: discussion - description: discussion community: sample tag_set: main support: name: support - description: support community: sample tag_set: main bug: name: bug - description: bug community: sample tag_set: main feature-request: name: feature-request - description: feature-request community: sample tag_set: main faq: name: faq - description: faq community: sample tag_set: meta status-completed: name: status-completed - description: status-completed community: sample tag_set: meta