diff --git a/README.md b/README.md index ba5c5c999d1527fd37be71bf41bc64e2c172a0a7..3872875ef0349b266be8d917bd7a575323254b83 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ Set up the database: rails db:create rails db:schema:load + rails r db/scripts/create_tags_path_view.rb rails db:migrate rails db:seed diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index ad6a20dea1903130d6242a8127bbca64702d28bf..35705a7e260bf618f28560c8329a8430ae3244d2 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -23,10 +23,16 @@ class TagsController < ApplicationController @tag_set = @category.tag_set @tags = if params[:q].present? @tag_set.tags.search(params[:q]) + elsif params[:hierarchical].present? + @tag_set.tags_with_paths.order(:path) else @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 + @count = @tags.count + table = params[:hierarchical].present? ? 'tags_paths' : 'tags' + @tags = @tags.left_joins(:posts).group(Arel.sql("#{table}.id")) + .select(Arel.sql("#{table}.*, COUNT(posts.id) AS post_count")) + .paginate(per_page: 96, page: params[:page]) end def show @@ -65,10 +71,16 @@ class TagsController < ApplicationController def children @tags = if params[:q].present? @tag.children.search(params[:q]) + elsif params[:hierarchical].present? + @tag_set.tags_with_paths.order(:path) 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 + @count = @tags.count + table = params[:hierarchical].present? ? 'tags_paths' : 'tags' + @tags = @tags.left_joins(:posts).group(Arel.sql("#{table}.id")) + .select(Arel.sql("#{table}.*, COUNT(posts.id) AS post_count")) + .paginate(per_page: 96, page: params[:page]) end private diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 289eb912a683599b1ea0b06060e863f0ff63ab6c..d65fc384a5a380c8f0fa52a2c32ad5556c222304 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -37,6 +37,19 @@ class ApplicationRecord < ActiveRecord::Base ary = ary.map { |el| ActiveRecord::Base.sanitize_sql_array(['?', el]) } "(#{ary.join(', ')})" end + + # This is a BRILLIANT idea. BRILLIANT, I tell you. + def self.with_lax_group_rules + return unless block_given? + + transaction do + connection.execute "SET @old_sql_mode = @@sql_mode" + connection.execute "SET SESSION sql_mode = REPLACE(REPLACE(@@sql_mode, 'ONLY_FULL_GROUP_BY,', ''), " \ + "'ONLY_FULL_GROUP_BY', '')" + yield + connection.execute "SET SESSION sql_mode = @old_sql_mode" + end + end end module UserSortable diff --git a/app/models/tag.rb b/app/models/tag.rb index d6629cbdb86380142fabbb843d8f2dc2a4fcefd7..51bf767f37e1eaf83d5382e7d29c049f0dd56f66 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -22,6 +22,16 @@ class Tag < ApplicationRecord ActiveRecord::Base.connection.execute(query).to_a.map(&:first) end + def parent_chain + Enumerator.new do |enum| + parent_group = group + while parent_group != nil + enum.yield parent_group + parent_group = parent_group.group + end + end + end + private def parent_not_self diff --git a/app/models/tag_set.rb b/app/models/tag_set.rb index 8e23746e0dbdd3f8db9400b44a223f15c91a5908..0a3d04066a8efbe4ece7b08bd9a2301f3cd92501 100644 --- a/app/models/tag_set.rb +++ b/app/models/tag_set.rb @@ -1,6 +1,7 @@ class TagSet < ApplicationRecord include CommunityRelated has_many :tags + has_many :tags_with_paths, class_name: 'TagWithPath' has_many :categories validates :name, uniqueness: { scope: [:community_id] }, presence: true diff --git a/app/models/tag_with_path.rb b/app/models/tag_with_path.rb new file mode 100644 index 0000000000000000000000000000000000000000..dde39913adf7e5a4c21ea562b3d8c941bf4d1de6 --- /dev/null +++ b/app/models/tag_with_path.rb @@ -0,0 +1,3 @@ +class TagWithPath < Tag + self.table_name = 'tags_paths' +end \ No newline at end of file diff --git a/app/views/tags/_list.html.erb b/app/views/tags/_list.html.erb index 7b4ffc202edee459079d59a0278e4876177abb1c..cc7e4ea91d0ff381483c94768663366c39b2e4b3 100644 --- a/app/views/tags/_list.html.erb +++ b/app/views/tags/_list.html.erb @@ -3,11 +3,13 @@ <% 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 %> + <% ApplicationRecord.with_lax_group_rules do %> + <% @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 %> <% end %> </div> \ No newline at end of file diff --git a/app/views/tags/_tag.html.erb b/app/views/tags/_tag.html.erb index a5a3799572704f73b2f6406a0d67fe913b5d3ba9..b0d11c7c088c6f8d4596bb8bcff7c0409d447c78 100644 --- a/app/views/tags/_tag.html.erb +++ b/app/views/tags/_tag.html.erb @@ -1,6 +1,13 @@ <div class="grid--cell is-4-lg is-6-md is-12-sm"> + <% if tag.respond_to?(:path) && tag.path.present? %> + <span class="has-font-size-caption"> + <% tag.path.split(' > ')[0..-2].each do |tag| %> + <span class="badge is-tag is-small is-muted"><%= tag %></span> » + <% end %> + </span> + <% end %> <%= link_to tag.name, tag_path(id: category.id, tag_id: tag.id), class: classes %> - <span class="has-color-tertiary-900">× <%= tag.post_count %></span> + <span class="has-color-tertiary-900">× <%= tag.post_count %></span> <% if tag.excerpt.present? %> <p class="has-font-size-caption has-color-tertiary-900"> <% splat = split_words_max_length(tag.excerpt, 120) %> diff --git a/app/views/tags/category.html.erb b/app/views/tags/category.html.erb index 00d461de38226f46eb6b0ad5a55f96a73c8a3566..ba519acf32abf3787bf129d96f464bed0d89c21f 100644 --- a/app/views/tags/category.html.erb +++ b/app/views/tags/category.html.erb @@ -14,6 +14,18 @@ </div> <% end %> +<% unless params[:q].present? %> + <div class="category-meta"> + <h3><%= pluralize(@count, 'tag') %></h3> + <div class="button-list is-gutterless has-margin-2"> + <%= link_to 'Usage', category_tags_path(@category), + class: "button is-muted is-outlined #{params[:hierarchical].nil? ? 'is-active' : ''}" %> + <%= link_to 'Hierarchy', query_url(hierarchical: '1'), + class: "button is-muted is-outlined #{params[:hierarchical].present? ? 'is-active' : ''}" %> + </div> + </div> +<% end %> + <%= render 'list' %> <%= will_paginate @tags, renderer: BootstrapPagination::Rails %> diff --git a/app/views/tags/children.html.erb b/app/views/tags/children.html.erb index 92cc1e14b5339f1d1df57be9c687bcdac33a358f..442a20269a0f3c03ad06071cce3d577f0e3a9551 100644 --- a/app/views/tags/children.html.erb +++ b/app/views/tags/children.html.erb @@ -14,6 +14,18 @@ </div> <% end %> +<% unless params[:q].present? %> + <div class="category-meta"> + <h3><%= pluralize(@count, 'tag') %></h3> + <div class="button-list is-gutterless has-margin-2"> + <%= link_to 'Usage', category_tags_path(@category), + class: "button is-muted is-outlined #{params[:hierarchical].nil? ? 'is-active' : ''}" %> + <%= link_to 'Hierarchy', query_url(hierarchical: '1'), + class: "button is-muted is-outlined #{params[:hierarchical].present? ? 'is-active' : ''}" %> + </div> + </div> +<% end %> + <%= render 'list' %> <%= will_paginate @tags, renderer: BootstrapPagination::Rails %> diff --git a/config/initializers/activerecord_relation.rb b/config/initializers/activerecord_relation.rb new file mode 100644 index 0000000000000000000000000000000000000000..fd91f7f2ebebcff927a8a63e4d898fba8f35599e --- /dev/null +++ b/config/initializers/activerecord_relation.rb @@ -0,0 +1,32 @@ +class ActiveRecord::Relation + # Preload one level a chained association whose name is specified in attribute. + def preload_chain(attribute, collection: nil) + preloader = ActiveRecord::Associations::Preloader.new + preloader.preload(collection || records, attribute.to_sym) + self + end + + # Preload all levels of a chained association specified in attribute. Will cause infinite loops if there are cycles. + def deep_preload_chain(attribute, collection: nil) + return if (collection || records).empty? + preload_chain(attribute, collection: collection) + deep_preload_chain(attribute, collection: (collection || records).select(&attribute.to_sym).map(&attribute.to_sym)) + self + end + + # Preload one level of a chained association on a table referenced by the current table. + def preload_reference_chain(**reference_attribs) + reference_attribs.each do |t, a| + preload_chain(a, collection: records.map { |r| r.public_send(t.to_sym) }) + end + self + end + + # Preload all levels (including infinite loops) of a chained association on a referenced table. + def deep_preload_reference_chain(**reference_attribs) + reference_attribs.each do |t, a| + deep_preload_chain(a, collection: records.map { |r| r.public_send(t.to_sym) }) + end + self + end +end \ No newline at end of file diff --git a/db/scripts/create_tags_path_view.rb b/db/scripts/create_tags_path_view.rb new file mode 100644 index 0000000000000000000000000000000000000000..99ddc3b58c0b625195074ba31d406284ca8a8556 --- /dev/null +++ b/db/scripts/create_tags_path_view.rb @@ -0,0 +1 @@ +ActiveRecord::Base.connection.execute File.read(Rails.root.join('db/scripts/create_tags_path_view.sql')) \ No newline at end of file diff --git a/db/scripts/create_tags_path_view.sql b/db/scripts/create_tags_path_view.sql new file mode 100644 index 0000000000000000000000000000000000000000..92708f38b2f27e1ab369542c522404cbac7ae882 --- /dev/null +++ b/db/scripts/create_tags_path_view.sql @@ -0,0 +1,15 @@ +create view tags_paths as +WITH RECURSIVE tag_path (id, created_at, updated_at, community_id, tag_set_id, wiki_markdown, + wiki, excerpt, parent_id, name, path) AS + ( + SELECT id, created_at, updated_at, community_id, tag_set_id, wiki_markdown, + wiki, excerpt, parent_id, name, name as path + FROM tags + WHERE parent_id IS NULL + UNION ALL + SELECT t.id, t.created_at, t.updated_at, t.community_id, t.tag_set_id, t.wiki_markdown, + t.wiki, t.excerpt, t.parent_id, t.name, concat(tp.path, ' > ', t.name) as path + FROM tag_path AS tp JOIN tags AS t ON tp.id = t.parent_id + ) +SELECT * FROM tag_path +ORDER BY path; \ No newline at end of file