Skip to content
Snippets Groups Projects
Commit d5188828 authored by ArtOfCode-'s avatar ArtOfCode-
Browse files

Tag hierarchies

parent caff78fe
No related branches found
No related tags found
No related merge requests found
class TagsController < ApplicationController class TagsController < ApplicationController
before_action :authenticate_user!, only: [:edit, :update] before_action :authenticate_user!, only: [:edit, :update]
before_action :set_category, except: [:index] 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 def index
@tag_set = if params[:tag_set].present? @tag_set = if params[:tag_set].present?
...@@ -24,16 +24,21 @@ class TagsController < ApplicationController ...@@ -24,16 +24,21 @@ class TagsController < ApplicationController
@tags = if params[:q].present? @tags = if params[:q].present?
@tag_set.tags.search(params[:q]) @tag_set.tags.search(params[:q])
else else
@tag_set.tags.left_joins(:posts).group(Arel.sql('tags.id')).order(Arel.sql('COUNT(posts.id) DESC')) @tag_set.tags.order(Arel.sql('COUNT(posts.id) DESC'))
.select(Arel.sql('tags.*, COUNT(posts.id) AS post_count')) end.left_joins(:posts).group(Arel.sql('tags.id')).select(Arel.sql('tags.*, COUNT(posts.id) AS post_count'))
end .paginate(per_page: 96, page: params[:page])
end end
def show def show
sort_params = { activity: { last_activity: :desc }, age: { created_at: :desc }, score: { score: :desc }, sort_params = { activity: { last_activity: :desc }, age: { created_at: :desc }, score: { score: :desc },
native: Arel.sql('att_source IS NULL DESC, last_activity DESC') } native: Arel.sql('att_source IS NULL DESC, last_activity DESC') }
sort_param = sort_params[params[:sort]&.to_sym] || { 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) .includes(:post_type, :tags).list_includes.paginate(page: params[:page], per_page: 50)
.order(sort_param) .order(sort_param)
end end
...@@ -48,17 +53,19 @@ class TagsController < ApplicationController ...@@ -48,17 +53,19 @@ class TagsController < ApplicationController
end end
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 private
def set_tag def set_tag
@tag = Tag.find params[:tag_id] @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 end
def set_category def set_category
...@@ -66,6 +73,6 @@ class TagsController < ApplicationController ...@@ -66,6 +73,6 @@ class TagsController < ApplicationController
end end
def tag_params def tag_params
params.require(:tag).permit(:excerpt, :wiki_markdown) params.require(:tag).permit(:excerpt, :wiki_markdown, :parent_id)
end end
end end
...@@ -7,4 +7,14 @@ module TagsHelper ...@@ -7,4 +7,14 @@ module TagsHelper
topic_ids.include?(t.id) ? 0 : 1, t.id] topic_ids.include?(t.id) ? 0 : 1, t.id]
end end
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 end
...@@ -2,7 +2,9 @@ class Tag < ApplicationRecord ...@@ -2,7 +2,9 @@ class Tag < ApplicationRecord
include CommunityRelated include CommunityRelated
has_and_belongs_to_many :posts has_and_belongs_to_many :posts
has_many :children, class_name: 'Tag', foreign_key: :parent_id
belongs_to :tag_set belongs_to :tag_set
belongs_to :parent, class_name: 'Tag', optional: true
validates :excerpt, length: { maximum: 600 }, allow_blank: true validates :excerpt, length: { maximum: 600 }, allow_blank: true
validates :wiki_markdown, length: { maximum: 30000 }, allow_blank: true validates :wiki_markdown, length: { maximum: 30000 }, allow_blank: true
...@@ -11,4 +13,10 @@ class Tag < ApplicationRecord ...@@ -11,4 +13,10 @@ class Tag < ApplicationRecord
where('name LIKE ?', "%#{sanitize_sql_like(term)}%") where('name LIKE ?', "%#{sanitize_sql_like(term)}%")
.order(sanitize_sql_array(['name LIKE ? DESC, name', "#{sanitize_sql_like(term)}%"])) .order(sanitize_sql_array(['name LIKE ? DESC, name', "#{sanitize_sql_like(term)}%"]))
end 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 end
<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
...@@ -2,16 +2,18 @@ ...@@ -2,16 +2,18 @@
<h1>Tags for <%= @category.name %></h1> <h1>Tags for <%= @category.name %></h1>
<div class="grid"> <%= form_tag category_tags_path(@category), method: :get, class: 'form-inline' do %>
<% required_ids = @category&.required_tag_ids %> <div class="form-group-horizontal">
<% moderator_ids = @category&.moderator_tag_ids %> <div class="form-group">
<% topic_ids = @category&.topic_tag_ids %> <%= label_tag :q, 'Search', class: 'form-element' %>
<%= text_field_tag :q, params[:q], class: 'form-element' %>
<% @tags.each do |tag| %> </div>
<% required = required_ids&.include?(tag.id) ? 'is-filled' : '' %> <div class="actions has-padding-bottom-1">
<% topic = topic_ids&.include?(tag.id) ? 'is-outlined' : '' %> <button type="submit" class="button is-filled is-medium"><i class="fas fa-search"></i><span class="sr-only">Search</span></button>
<% moderator = moderator_ids&.include?(tag.id) ? 'is-red is-outlined' : '' %> </div>
<% classes = "badge is-tag #{required} #{topic} #{moderator}" %>
<%= render 'tag', category: @category, tag: tag, classes: classes %>
<% end %>
</div> </div>
<% end %>
<%= render 'list' %>
<%= will_paginate @tags, renderer: BootstrapPagination::Rails %>
<% 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 %>
...@@ -19,6 +19,17 @@ ...@@ -19,6 +19,17 @@
<% end %> <% end %>
<%= form_for @tag, url: update_tag_path(id: @category.id, tag_id: @tag.id) do |f| %> <%= 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"> <div class="form-group">
<%= f.label :excerpt, 'Usage guidance', class: 'form-element' %> <%= f.label :excerpt, 'Usage guidance', class: 'form-element' %>
<span class="form-caption"> <span class="form-caption">
......
<% content_for :title, "Posts tagged #{@tag.name}" %> <% content_for :title, "Posts tagged #{@tag.name}" %>
<h1> <h1 class="has-margin-0 has-margin-top-4">
Posts tagged <span class="<%= @classes %> is-large"><%= @tag.name %></span> Posts tagged <span class="<%= tag_classes(@tag, @category) %> is-large"><%= @tag.name %></span>
</h1> </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 %>
&middot;
<% 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">
<div class="widget--body has-font-size-caption has-color-tertiary-900"> <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? %> <% unless @tag.excerpt.present? %>
<p class="has-font-size-caption has-margin-0"> <p class="has-font-size-caption has-margin-0">
<em> <em>
...@@ -45,17 +58,10 @@ ...@@ -45,17 +58,10 @@
</div> </div>
<div class="button-list is-gutterless has-margin-2"> <div class="button-list is-gutterless has-margin-2">
<%= link_to 'Activity', query_url(sort: 'activity'), <%= link_to 'Tag Only', query_url(self: 1),
class: "button is-muted is-outlined #{(params[:sort].nil?) && !current_page?(questions_lottery_path) || class: "button is-muted is-outlined #{params[:self].present? ? 'is-active' : ''}" %>
params[:sort] == 'activity' ? 'is-active' : ''}" %> <%= link_to 'Tag + Children', tag_path(id: @category.id, tag_id: @tag.id),
<%= link_to 'Age', query_url(sort: 'age'), class: "button is-muted is-outlined #{params[:self].nil? ? 'is-active' : ''}" %>
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 %>
</div> </div>
</div> </div>
......
...@@ -183,6 +183,7 @@ Rails.application.routes.draw do ...@@ -183,6 +183,7 @@ Rails.application.routes.draw do
get ':id/feed', to: 'categories#rss_feed', as: :category_feed get ':id/feed', to: 'categories#rss_feed', as: :category_feed
get ':id/tags', to: 'tags#category', as: :category_tags get ':id/tags', to: 'tags#category', as: :category_tags
get ':id/tags/:tag_id', to: 'tags#show', as: :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 get ':id/tags/:tag_id/edit', to: 'tags#edit', as: :edit_tag
patch ':id/tags/:tag_id/edit', to: 'tags#update', as: :update_tag patch ':id/tags/:tag_id/edit', to: 'tags#update', as: :update_tag
end end
......
class AllowTagParents < ActiveRecord::Migration[5.2]
def change
add_reference :tags, :parent, foreign_key: { to_table: :tags }
end
end
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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| create_table "active_storage_attachments", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
...@@ -366,7 +366,9 @@ ActiveRecord::Schema.define(version: 2020_06_30_001048) do ...@@ -366,7 +366,9 @@ ActiveRecord::Schema.define(version: 2020_06_30_001048) do
t.text "wiki_markdown" t.text "wiki_markdown"
t.text "wiki" t.text "wiki"
t.text "excerpt" t.text "excerpt"
t.bigint "parent_id"
t.index ["community_id"], name: "index_tags_on_community_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" t.index ["tag_set_id"], name: "index_tags_on_tag_set_id"
end end
...@@ -468,6 +470,7 @@ ActiveRecord::Schema.define(version: 2020_06_30_001048) do ...@@ -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"
add_foreign_key "suggested_edits", "users", column: "decided_by_id" add_foreign_key "suggested_edits", "users", column: "decided_by_id"
add_foreign_key "tags", "communities" add_foreign_key "tags", "communities"
add_foreign_key "tags", "tags", column: "parent_id"
add_foreign_key "votes", "communities" add_foreign_key "votes", "communities"
add_foreign_key "warning_templates", "communities" add_foreign_key "warning_templates", "communities"
add_foreign_key "warnings", "community_users" add_foreign_key "warnings", "community_users"
......
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
discussion: discussion:
name: discussion name: discussion
description: discussion
community: sample community: sample
tag_set: main tag_set: main
support: support:
name: support name: support
description: support
community: sample community: sample
tag_set: main tag_set: main
bug: bug:
name: bug name: bug
description: bug
community: sample community: sample
tag_set: main tag_set: main
feature-request: feature-request:
name: feature-request name: feature-request
description: feature-request
community: sample community: sample
tag_set: main tag_set: main
faq: faq:
name: faq name: faq
description: faq
community: sample community: sample
tag_set: meta tag_set: meta
status-completed: status-completed:
name: status-completed name: status-completed
description: status-completed
community: sample community: sample
tag_set: meta tag_set: meta
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment