From 5c4370e24409dabaeafd0f645674091bafa8f4bd Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Tue, 2 Dec 2025 13:03:00 -0300 Subject: [PATCH 01/15] =?UTF-8?q?Adiciona=20gem=20ancestry=20para=20otimiz?= =?UTF-8?q?ar=20hierarquia=20de=20posts=20do=20f=C3=B3rum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adiciona ancestry ao Gemfile para gerenciar hierarquia de posts - Atualiza Gemfile.lock com as dependências - Atualiza versão do Ruby de 2.7.2 para 2.7.8 --- Dockerfile | 2 +- Gemfile | 5 +- Gemfile.lock | 5 +- app/controllers/posts_controller.rb | 16 ++--- app/models/discussion.rb | 23 +++++- app/models/post.rb | 70 +++++++++++++------ app/views/posts/_post.html.haml | 9 ++- app/views/posts/index.html.haml | 20 +++--- config/environment.rb | 2 +- ...add_ancestry_column_to_discussion_posts.rb | 9 +++ 10 files changed, 114 insertions(+), 47 deletions(-) create mode 100644 db/migrate/20251201180714_add_ancestry_column_to_discussion_posts.rb diff --git a/Dockerfile b/Dockerfile index 83929fe22..dccbd90ee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ ARG RUBY_PATH=/usr/local/ ARG RUBY_VERSION=2.7.2 -FROM ubuntu:16.04 AS rubybuild +FROM ubuntu:20.04 AS rubybuild ARG RUBY_PATH ARG RUBY_VERSION diff --git a/Gemfile b/Gemfile index 7c2898a62..8669b117b 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,7 @@ source "http://rubygems.org" -ruby "2.7.2" +#ruby "2.7.2" +ruby "2.7.8" #gem "rails", "~> 3.2.16" gem "rails", "5.1.7" @@ -106,6 +107,8 @@ gem "newrelic_rpm" gem "dotenv-rails" gem "tzinfo-data" +gem "ancestry" # Para otimização de queries de posts do fórum + group :development do gem "foreman", require: false diff --git a/Gemfile.lock b/Gemfile.lock index 997e653c1..a23a53483 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -51,6 +51,8 @@ GEM akami (1.2.2) gyoku (>= 0.4.0) nokogiri + ancestry (3.2.1) + activerecord (>= 4.2.0) arel (8.0.0) ast (2.4.2) attr_required (1.0.1) @@ -526,6 +528,7 @@ PLATFORMS DEPENDENCIES actionpack-action_caching (~> 1.2.0) actionpack-page_caching (~> 1.2.3) + ancestry awesome_print better_errors (~> 2.4.0) bigbluebutton-api-ruby (~> 1.6.0) @@ -615,7 +618,7 @@ DEPENDENCIES wkhtmltopdf-binary (~> 0.12.3) RUBY VERSION - ruby 2.7.2p137 + ruby 2.7.8p225 BUNDLED WITH 2.1.4 diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 8b0ec860a..9b956a197 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -41,16 +41,16 @@ def index if (p['display_mode'] == "list" || params[:format] == "json") # se for em forma de lista ou para o mobilis, pesquisa pelo método posts p['page'] ||= @current_page - p['type'] ||= "history" p['date'] = DateTime.parse(p['date']) if params[:format] == "json" && p.include?('date') @posts = @discussion.posts_not_limit(p, @allocation_tags, current_user.id).paginate(page: params[:page] || 1, per_page: Rails.application.config.items_per_page) - elsif (@display_mode == 'user' ) - my_list = true - @posts = @discussion.posts_by_allocation_tags_ids(@allocation_tags, current_user.id, my_list).paginate(page: params[:page] || 1, per_page: Rails.application.config.items_per_page) # caso contrário, recupera e reordena os posts do nível 1 a partir das datas de seus descendentes - else - @posts = @discussion.posts_by_allocation_tags_ids(@allocation_tags, current_user.id).paginate(page: params[:page] || 1, per_page: Rails.application.config.items_per_page) # caso contrário, recupera e reordena os posts do nível 1 a partir das datas de seus descendentes - end - + elsif (@display_mode == 'user' ) + my_list = true + @posts = @discussion.posts_by_allocation_tags_ids(@allocation_tags, current_user.id, my_list).paginate(page: params[:page] || 1, per_page: Rails.application.config.items_per_page) + else + # Carrega os posts para a visualização em árvore usando o método otimizado do modelo, + # que lida com a complexidade da consulta. + @posts = @discussion.posts_for_tree_view(@allocation_tags, current_user.id) + end if current_user.is_student?([@allocation_tags].flatten) @acu = AcademicAllocationUser.find_or_create_one(@academic_allocation.id, [@allocation_tags].flatten, current_user.id, nil, false, AcademicAllocationUser::STATUS[:empty]) end diff --git a/app/models/discussion.rb b/app/models/discussion.rb index 1db9f066e..2a1139557 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -139,6 +139,28 @@ def posts_by_allocation_tags_ids(allocation_tags_ids = nil, user_id = nil, my_li (opt[:grandparent] ? posts_list.map(&:grandparent).uniq.compact : posts_list.to_a.compact.uniq) end + # Retorna os posts da discussão organizados para a visualização em árvore. + # Este método é o coração da otimização: ele busca apenas os posts raiz (`.roots`) + # e aplica a ordenação hierárquica (`order_by_ancestry`). A view itera sobre + # estes posts raiz, e a gem `ancestry` encontra os filhos em memória, + # sem a necessidade de novas consultas ao banco. + def posts_for_tree_view(allocation_tags_ids = nil, user_id=nil) + # Passo 1: pegar os IDs das allocation tags relacionadas + allocation_tags_ids = AllocationTag.where(id: allocation_tags_ids).map(&:related).flatten.compact.uniq + + # Passo 2: buscar apenas posts raiz (parent_id IS NULL) da discussao + discussion_posts + .roots # Só posts sem pai (ancestry = NULL ou não tem ancestral) + .includes(:files, :user, :profile) # Eager load para evitar N+1 queries + .roots # <== NOVO: Método da `ancestry` que busca apenas os posts principais. + .includes(:files, :user, :profile) # Eager load para evitar N+1 queries na view. + .joins(academic_allocation: :allocation_tag) # Juntar com academic_allocations e allocation_tags + .where(allocation_tags: { id: allocation_tags_ids }) # Filtro de permissao + .where("(draft = ? ) OR (draft = ? AND user_id = ?)", false, true, user_id) # Filtro de rascunhos/drafts + .order_by_ancestry # Usa o scope definido em Post para ordenar por ancestralidade + .paginate(page: 1, per_page: Rails.application.config.items_per_page) + end + def posts_by_allocation_tags_ids_to_api(allocation_tags_ids = nil, user_id = nil, my_list=nil, opt = { grandparent: true, query: '', order: 'updated_at desc', limit: nil, offset: nil, select: 'DISTINCT discussion_posts.id, discussion_posts.*' }) allocation_tags_ids = AllocationTag.where(id: allocation_tags_ids).map(&:related).flatten.compact.uniq posts_list = discussion_posts.includes(:files).where(opt[:query]).order(opt[:order]).limit(opt[:limit]).offset(opt[:offset]).select(opt[:select]) @@ -149,7 +171,6 @@ def posts_by_allocation_tags_ids_to_api(allocation_tags_ids = nil, user_id = nil (opt[:grandparent] ? posts_list.map(&:grandparent).uniq.compact : posts_list.compact.uniq) end - def resume(allocation_tags_ids = nil) { id: id, diff --git a/app/models/post.rb b/app/models/post.rb index 626c41150..fd24bebad 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -2,6 +2,11 @@ class Post < ActiveRecord::Base include SentActivity self.table_name = 'discussion_posts' + # Adiciona a gem ancestry para gerenciar a hierarquia de forma otimizada, + # substituindo a recursão manual que causava lentidão. + # A opção `orphan_strategy: :destroy` apaga os posts filhos quando um pai é deletado, + # substituindo a necessidade do método `delete_with_dependents`. + has_ancestry orphan_strategy: :destroy belongs_to :profile belongs_to :parent, class_name: 'Post' @@ -13,8 +18,10 @@ class Post < ActiveRecord::Base validates :parent, presence: true, unless: -> {parent_id.blank?} before_destroy :verify_children_with_raise, :can_change?, if: -> {merge.nil?} validate :verify_children, on: :update - - has_many :children, class_name: 'Post', foreign_key: 'parent_id', dependent: :destroy + + # A associação `has_many :children` manual foi removida pois a gem `ancestry` + # já cria essa associação de forma otimizada, evitando conflitos. + # has_many :children, class_name: 'Post', foreign_key: 'parent_id', dependent: :destroy has_many :files, class_name: 'PostFile', foreign_key: 'discussion_post_id', dependent: :destroy accepts_nested_attributes_for :files, allow_destroy: true, reject_if: proc {|attributes| !attributes.include?(:attachment) || attributes[:attachment] == '0' || attributes[:attachment].blank?} @@ -132,32 +139,42 @@ def to_mobilis } end - ## Return latest date considering children - def get_latest_date - date = [(children_count <= 0 ? self.updated_at : children.map(&:get_latest_date))].flatten.compact - date.sort.last - end - - ## Recupera os posts mais recentes dos niveis inferiores aos posts analisados e, então, - ## reordena analisando ou as datas dos posts em questão ou a data do "filho/neto" mais recente - def self.reorder_by_latest_posts(posts) - return posts.sort_by{|post| - post.get_latest_date - }.reverse - end - - def delete_with_dependents - children.map(&:delete_with_dependents) - remove_all_files - self.delete - end + # deleta o post e todos os seus dependentes + # Os métodos abaixo (`get_latest_date`, `delete_with_dependents`, `reorder_by_latest_posts`) + # eram auxiliares para a antiga lógica de recursão manual e foram removidos. + # - `delete_with_dependents` foi substituído pelo `orphan_strategy: :destroy` da gem ancestry. + # - `reorder_by_latest_posts` e `get_latest_date` causavam problemas de N+1 queries. + # + + # ## Return latest date considering children + # def get_latest_date + # date = [(children_count <= 0 ? self.updated_at : children.map(&:get_latest_date))].flatten.compact + # date.sort.last + # end + + # def delete_with_dependents + # children.map(&:delete_with_dependents) + # remove_all_files + # self.delete + # end + + # ## Recupera os posts mais recentes dos niveis inferiores aos posts analisados e, então, + # ## reordena analisando ou as datas dos posts em questão ou a data do "filho/neto" mais recente + # def self.reorder_by_latest_posts(posts) + # return posts.sort_by{|post| + # post.get_latest_date + # }.reverse + # end - # obtain and reorder posts by its "children/grandchildren" def reordered_children(user_id, display_mode='three') if display_mode == 'list' children.where("draft = 'f' OR (draft = 't' AND user_id = ?)", user_id) else - Post.reorder_by_latest_posts(children.where("draft = 'f' OR (draft = 't' AND user_id = ?)", user_id)) + #Post.reorder_by_latest_posts(children.where("draft = 'f' OR (draft = 't' AND user_id = ?)", user_id)) + # A ordenação complexa e custosa (`reorder_by_latest_posts`) foi removida. + # Este método agora apenas filtra os filhos (visíveis para o usuário atual), + # pois a ordenação da árvore é feita de forma mais eficiente no controller. + children.where("draft = 'f' OR (draft = 't' AND user_id = ?)", user_id) end end @@ -172,6 +189,13 @@ def send_email end end + # Escopo para ordenar posts hierarquicamente usando a coluna ancestry + # A ordenação por 'id' garante uma ordem consistente para posts no mesmo nível. + # Este escopo (`order_by_ancestry`) fornece uma maneira padronizada e reutilizável + # de ordenar os resultados pela hierarquia da `ancestry`, garantindo que a árvore + # seja exibida corretamente na view. + scope :order_by_ancestry, -> { order(Arel.sql("ancestry, id")) } + private def set_level diff --git a/app/views/posts/_post.html.haml b/app/views/posts/_post.html.haml index 62fb01ea3..36358730f 100755 --- a/app/views/posts/_post.html.haml +++ b/app/views/posts/_post.html.haml @@ -1,7 +1,7 @@ - user, new_post_class = post.user, ((new_post && new_post.to_i == post.id) ? "new" : "") - post_draft = post.draft ? " draft" : "" -- children = post.reordered_children(current_user.id, display_mode) +# A definição manual da variável `children` foi removida. A gem `ancestry` agora gerencia os filhos de forma otimizada. - editable = ((post.user_id == current_user.id) && (post.children_count == 0 || post.children_drafts_count == post.children_count)) - files = post.files @@ -64,8 +64,11 @@ %li= button_tag content_tag(:i, nil, class: 'icon-reply'), class: 'btn btn_disabled', disabled: true, :'data-tooltip'=> t('.answer'), :'aria-label'=> t('.answer') .forum_post_reply - unless display_mode == 'list' - - children.each do |child| - = render partial: 'post', locals: { post: child, display_mode: display_mode, can_interact: can_interact, can_post: can_post, current_user: current_user, new_post: new_post } + -# A renderização dos filhos agora usa o método `post.children` da gem `ancestry`, + -# que é mais eficiente e evita o problema de N+1 queries. + - if post.has_children? + - post.children.each do |child| + = render partial: 'post', locals: { post: child, display_mode: display_mode, can_interact: can_interact, can_post: can_post, current_user: current_user, new_post: new_post } ._line :javascript diff --git a/app/views/posts/index.html.haml b/app/views/posts/index.html.haml index 856261f06..9096972ad 100644 --- a/app/views/posts/index.html.haml +++ b/app/views/posts/index.html.haml @@ -85,7 +85,8 @@ .forum_posts_wrapper - @hash = { } unless @display_mode == 'list' - - @posts = get_page_posts(@posts, @current_page) unless @display_mode == 'list' + -# REMOVIDO: get_page_posts não é mais necessário pois @posts já vem paginado do banco via WillPaginate + -# - @posts = get_page_posts(@posts, @current_page) unless @display_mode == 'list' - @posts.each do |post| = render partial: 'post', locals: { post: post, display_mode: @display_mode, can_interact: @can_interact, can_post: @can_post, current_user: @user, new_post: false, number: '', comments: (@acu.try(:comments).count rescue nil)} @@ -97,7 +98,10 @@ .form-inputs.block_content = raw t('posts.post.fv_explain') - +# A variável `ws_config` é definida aqui para ser processada no servidor +# antes de ser usada para gerar o JavaScript. Isso corrige um erro de escopo (`NameError`) +# que ocorria quando a definição estava dentro do bloco `:javascript`. +- ws_config = Rails.application.config_for(:global).fetch(:websocket, {}) = javascript_include_tag 'ckeditor/init', 'discussions', 'contextual_help/discussion_posts', 'audios', 'pagination', 'tooltip', 'academic_allocation_user' :javascript @@ -136,12 +140,12 @@ CKEDITOR_BASEPATH = "#{request.env['RAILS_RELATIVE_URL_ROOT']}/assets/ckeditor/"; - var websocket_host = "#{YAML::load(File.open("config/global.yml"))[Rails.env.to_s]["websocket"]["host"]}"; - var websocket_port = "#{YAML::load(File.open("config/global.yml"))[Rails.env.to_s]["websocket"]["port"]}"; - - $(".forum_button_attachment").call_fancybox({ - element_selector: '.focu' - }); + // O carregamento direto do YAML foi substituído pelo `Rails.application.config_for`, + // que é a maneira padrão, segura e performática do Rails para acessar configurações. + // O `.fetch` garante valores padrão ('localhost', '8080') caso a configuração não exista. + var websocket_host = "#{ws_config.fetch(:host, 'localhost')}"; + var websocket_port = "#{ws_config.fetch(:port, '8080')}"; + $(".forum_button_attachment").call_fancybox({ element_selector: '.focu' }); var display_list = ("#{escape_once(@display_mode) == 'list'}" == "true"); var ac = "#{@academic_allocation.id}"; diff --git a/config/environment.rb b/config/environment.rb index 6829c6a20..7ab56aa49 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -88,4 +88,4 @@ # número máximo de turmas exibidas sem expansão da div Max_Groups_Shown_Filter = 30 # no filtro -Max_Groups_Shown_Tags = 15 # nas tags \ No newline at end of file +Max_Groups_Shown_Tags = 15 # nas tags diff --git a/db/migrate/20251201180714_add_ancestry_column_to_discussion_posts.rb b/db/migrate/20251201180714_add_ancestry_column_to_discussion_posts.rb new file mode 100644 index 000000000..344a75964 --- /dev/null +++ b/db/migrate/20251201180714_add_ancestry_column_to_discussion_posts.rb @@ -0,0 +1,9 @@ + # Dentro de db/migrate/SEU_NOVO_ARQUIVO_DE_MIGRATION.rb + class AddAncestryColumnToDiscussionPosts < ActiveRecord::Migration[5.1] # Mantenha a versão do Rails + + def change + add_column :discussion_posts, :ancestry, :string + add_index :discussion_posts, :ancestry + end + end + From fa5a95d3e9642ba1daf8207c81db83a8a2ec6706 Mon Sep 17 00:00:00 2001 From: Bianca Date: Wed, 3 Dec 2025 15:08:18 -0300 Subject: [PATCH 02/15] =?UTF-8?q?Retornando=20a=20vers=C3=A3o=20do=20ruby;?= =?UTF-8?q?=20corrigindo=20coment=C3=A1rios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gemfile | 4 ++-- Gemfile.lock | 2 +- app/views/posts/_post.html.haml | 2 +- app/views/posts/index.html.haml | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Gemfile b/Gemfile index 8669b117b..f32898483 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source "http://rubygems.org" -#ruby "2.7.2" -ruby "2.7.8" +ruby "2.7.2" +# ruby "2.7.8" #gem "rails", "~> 3.2.16" gem "rails", "5.1.7" diff --git a/Gemfile.lock b/Gemfile.lock index a23a53483..1596d09a3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -618,7 +618,7 @@ DEPENDENCIES wkhtmltopdf-binary (~> 0.12.3) RUBY VERSION - ruby 2.7.8p225 + ruby 2.7.2p137 BUNDLED WITH 2.1.4 diff --git a/app/views/posts/_post.html.haml b/app/views/posts/_post.html.haml index 36358730f..755f7c708 100755 --- a/app/views/posts/_post.html.haml +++ b/app/views/posts/_post.html.haml @@ -1,7 +1,7 @@ - user, new_post_class = post.user, ((new_post && new_post.to_i == post.id) ? "new" : "") - post_draft = post.draft ? " draft" : "" -# A definição manual da variável `children` foi removida. A gem `ancestry` agora gerencia os filhos de forma otimizada. +// A definição manual da variável `children` foi removida. A gem `ancestry` agora gerencia os filhos de forma otimizada. - editable = ((post.user_id == current_user.id) && (post.children_count == 0 || post.children_drafts_count == post.children_count)) - files = post.files diff --git a/app/views/posts/index.html.haml b/app/views/posts/index.html.haml index 9096972ad..8829801dd 100644 --- a/app/views/posts/index.html.haml +++ b/app/views/posts/index.html.haml @@ -98,9 +98,9 @@ .form-inputs.block_content = raw t('posts.post.fv_explain') -# A variável `ws_config` é definida aqui para ser processada no servidor -# antes de ser usada para gerar o JavaScript. Isso corrige um erro de escopo (`NameError`) -# que ocorria quando a definição estava dentro do bloco `:javascript`. +// A variável `ws_config` é definida aqui para ser processada no servidor +// antes de ser usada para gerar o JavaScript. Isso corrige um erro de escopo (`NameError`) +// que ocorria quando a definição estava dentro do bloco `:javascript`. - ws_config = Rails.application.config_for(:global).fetch(:websocket, {}) = javascript_include_tag 'ckeditor/init', 'discussions', 'contextual_help/discussion_posts', 'audios', 'pagination', 'tooltip', 'academic_allocation_user' From 9fa07a63828cb0dc1e6445ecc8c9a8f85bc8bcea Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Thu, 4 Dec 2025 08:01:08 -0300 Subject: [PATCH 03/15] =?UTF-8?q?Corrige=20pagina=C3=A7=C3=A3o=20em=20post?= =?UTF-8?q?s=5Ffor=5Ftree=5Fview?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adiciona suporte dinâmico de paginação ao método posts_for_tree_view, permitindo navegação entre páginas no modo de visualização em árvore. Alterações: - Adiciona parâmetro 'page' ao método posts_for_tree_view - Atualiza controller para passar params[:page] ao método - Remove duplicação de .roots e .includes no model - Remove comentários desnecessários do código --- app/controllers/posts_controller.rb | 4 +--- app/models/discussion.rb | 25 ++++++++----------------- app/views/posts/_post.html.haml | 2 +- app/views/posts/index.html.haml | 6 +++--- 4 files changed, 13 insertions(+), 24 deletions(-) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 9b956a197..c01e45e2b 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -47,9 +47,7 @@ def index my_list = true @posts = @discussion.posts_by_allocation_tags_ids(@allocation_tags, current_user.id, my_list).paginate(page: params[:page] || 1, per_page: Rails.application.config.items_per_page) else - # Carrega os posts para a visualização em árvore usando o método otimizado do modelo, - # que lida com a complexidade da consulta. - @posts = @discussion.posts_for_tree_view(@allocation_tags, current_user.id) + @posts = @discussion.posts_for_tree_view(@allocation_tags, current_user.id, params[:page] || 1) end if current_user.is_student?([@allocation_tags].flatten) @acu = AcademicAllocationUser.find_or_create_one(@academic_allocation.id, [@allocation_tags].flatten, current_user.id, nil, false, AcademicAllocationUser::STATUS[:empty]) diff --git a/app/models/discussion.rb b/app/models/discussion.rb index 2a1139557..2aed26d37 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -139,26 +139,17 @@ def posts_by_allocation_tags_ids(allocation_tags_ids = nil, user_id = nil, my_li (opt[:grandparent] ? posts_list.map(&:grandparent).uniq.compact : posts_list.to_a.compact.uniq) end - # Retorna os posts da discussão organizados para a visualização em árvore. - # Este método é o coração da otimização: ele busca apenas os posts raiz (`.roots`) - # e aplica a ordenação hierárquica (`order_by_ancestry`). A view itera sobre - # estes posts raiz, e a gem `ancestry` encontra os filhos em memória, - # sem a necessidade de novas consultas ao banco. - def posts_for_tree_view(allocation_tags_ids = nil, user_id=nil) - # Passo 1: pegar os IDs das allocation tags relacionadas + def posts_for_tree_view(allocation_tags_ids = nil, user_id = nil, page = 1) allocation_tags_ids = AllocationTag.where(id: allocation_tags_ids).map(&:related).flatten.compact.uniq - # Passo 2: buscar apenas posts raiz (parent_id IS NULL) da discussao discussion_posts - .roots # Só posts sem pai (ancestry = NULL ou não tem ancestral) - .includes(:files, :user, :profile) # Eager load para evitar N+1 queries - .roots # <== NOVO: Método da `ancestry` que busca apenas os posts principais. - .includes(:files, :user, :profile) # Eager load para evitar N+1 queries na view. - .joins(academic_allocation: :allocation_tag) # Juntar com academic_allocations e allocation_tags - .where(allocation_tags: { id: allocation_tags_ids }) # Filtro de permissao - .where("(draft = ? ) OR (draft = ? AND user_id = ?)", false, true, user_id) # Filtro de rascunhos/drafts - .order_by_ancestry # Usa o scope definido em Post para ordenar por ancestralidade - .paginate(page: 1, per_page: Rails.application.config.items_per_page) + .roots + .includes(:files, :user, :profile) + .joins(academic_allocation: :allocation_tag) + .where(allocation_tags: { id: allocation_tags_ids }) + .where("(draft = ? ) OR (draft = ? AND user_id = ?)", false, true, user_id) + .order_by_ancestry + .paginate(page: page, per_page: Rails.application.config.items_per_page) end def posts_by_allocation_tags_ids_to_api(allocation_tags_ids = nil, user_id = nil, my_list=nil, opt = { grandparent: true, query: '', order: 'updated_at desc', limit: nil, offset: nil, select: 'DISTINCT discussion_posts.id, discussion_posts.*' }) diff --git a/app/views/posts/_post.html.haml b/app/views/posts/_post.html.haml index 755f7c708..87d52da18 100755 --- a/app/views/posts/_post.html.haml +++ b/app/views/posts/_post.html.haml @@ -1,7 +1,7 @@ - user, new_post_class = post.user, ((new_post && new_post.to_i == post.id) ? "new" : "") - post_draft = post.draft ? " draft" : "" -// A definição manual da variável `children` foi removida. A gem `ancestry` agora gerencia os filhos de forma otimizada. +-# A definição manual da variável `children` foi removida. A gem `ancestry` agora gerencia os filhos de forma otimizada. - editable = ((post.user_id == current_user.id) && (post.children_count == 0 || post.children_drafts_count == post.children_count)) - files = post.files diff --git a/app/views/posts/index.html.haml b/app/views/posts/index.html.haml index 8829801dd..957e96598 100644 --- a/app/views/posts/index.html.haml +++ b/app/views/posts/index.html.haml @@ -98,9 +98,9 @@ .form-inputs.block_content = raw t('posts.post.fv_explain') -// A variável `ws_config` é definida aqui para ser processada no servidor -// antes de ser usada para gerar o JavaScript. Isso corrige um erro de escopo (`NameError`) -// que ocorria quando a definição estava dentro do bloco `:javascript`. +-# A variável `ws_config` é definida aqui para ser processada no servidor +-# antes de ser usada para gerar o JavaScript. Isso corrige um erro de escopo (`NameError`) +-# que ocorria quando a definição estava dentro do bloco `:javascript`. - ws_config = Rails.application.config_for(:global).fetch(:websocket, {}) = javascript_include_tag 'ckeditor/init', 'discussions', 'contextual_help/discussion_posts', 'audios', 'pagination', 'tooltip', 'academic_allocation_user' From 87d0a2394c13337d31bca1db07fe0ce37f4cf2a4 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Thu, 4 Dec 2025 09:07:21 -0300 Subject: [PATCH 04/15] =?UTF-8?q?Remove=20coment=C3=A1rios=20desnecess?= =?UTF-8?q?=C3=A1rios=20das=20views=20de=20posts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/posts/_post.html.haml | 2 +- app/views/posts/index.html.haml | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/views/posts/_post.html.haml b/app/views/posts/_post.html.haml index 87d52da18..0b1874c4b 100755 --- a/app/views/posts/_post.html.haml +++ b/app/views/posts/_post.html.haml @@ -1,7 +1,7 @@ - user, new_post_class = post.user, ((new_post && new_post.to_i == post.id) ? "new" : "") - post_draft = post.draft ? " draft" : "" --# A definição manual da variável `children` foi removida. A gem `ancestry` agora gerencia os filhos de forma otimizada. + - editable = ((post.user_id == current_user.id) && (post.children_count == 0 || post.children_drafts_count == post.children_count)) - files = post.files diff --git a/app/views/posts/index.html.haml b/app/views/posts/index.html.haml index 957e96598..76f538236 100644 --- a/app/views/posts/index.html.haml +++ b/app/views/posts/index.html.haml @@ -98,9 +98,7 @@ .form-inputs.block_content = raw t('posts.post.fv_explain') --# A variável `ws_config` é definida aqui para ser processada no servidor --# antes de ser usada para gerar o JavaScript. Isso corrige um erro de escopo (`NameError`) --# que ocorria quando a definição estava dentro do bloco `:javascript`. + - ws_config = Rails.application.config_for(:global).fetch(:websocket, {}) = javascript_include_tag 'ckeditor/init', 'discussions', 'contextual_help/discussion_posts', 'audios', 'pagination', 'tooltip', 'academic_allocation_user' From 44d13c13e6d0383d9eb6bbb6f1773d810ece506f Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Tue, 9 Dec 2025 20:53:09 -0300 Subject: [PATCH 05/15] =?UTF-8?q?Desabilita=20timestamps=20autom=C3=A1tico?= =?UTF-8?q?s=20do=20ActiveRecord?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Configuração necessária para popular a coluna ancestry sem alterar os campos updated_at dos posts existentes. Isso preserva os timestamps originais que são importantes para ordenação por última atividade. --- config/application.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/application.rb b/config/application.rb index 599dbd24e..736f79462 100644 --- a/config/application.rb +++ b/config/application.rb @@ -29,6 +29,8 @@ class Application < Rails::Application # :all can be used as a placeholder for all plugins not explicitly named. # config.plugins = [ :exception_notification, :ssl_requirement, :all ] + config.active_record.record_timestamps = false + # Activate observers that should always be running. # config.active_record.observers = :cacher, :garbage_collector, :forum_observer #config.middleware.use "PDFKit::Middleware", :print_media_type => true From 1980015d7960714ad4ad00b0ac01a3a5b6e42a2f Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Tue, 9 Dec 2025 20:56:09 -0300 Subject: [PATCH 06/15] Adiciona funcionalidade de last_activity_at no model Post MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mudanças no model Post: - Adiciona callbacks para manter last_activity_at atualizado - Implementa método update_last_activity que propaga timestamp para o post e todos os ancestrais - Atualiza scope order_by_ancestry para ordenar por última atividade - Modifica can_change? para permitir atualização do updated_at O método update_last_activity usa path_ids do ancestry para atualizar toda a cadeia de ancestrais em apenas 2 queries SQL. --- app/models/post.rb | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/app/models/post.rb b/app/models/post.rb index fd24bebad..c95d858f2 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -29,10 +29,12 @@ class Post < ActiveRecord::Base before_create :set_level, :verify_level before_destroy :remove_all_files - after_create :increment_counter + # Mantém last_activity_at atualizado em toda a árvore quando há atividade + after_create :increment_counter, :update_last_activity after_destroy :decrement_counter, :remove_drafts_children, :decrement_counter_draft after_save :change_counter_draft, if: -> {!parent_id.nil? && saved_change_to_draft?} after_save :send_email, unless: -> {parent_id.blank? || draft || parent.user_id == user_id || !merge.nil?} + after_save :update_last_activity, if: -> {saved_change_to_content? || saved_change_to_updated_at?} validates :content, :profile_id, presence: true @@ -94,6 +96,8 @@ def verify_children_with_raise end def can_change? + # Permite atualização apenas do updated_at (ex: touch) + return true if changed == ["updated_at"] unless (user_id == User.current.try(:id) || (User.current.try(:id) == parent.try(:user_id) && !content_changed? && !draft_changed?)) errors.add(:base, I18n.t('posts.error.permission')) raise 'permission' @@ -189,12 +193,8 @@ def send_email end end - # Escopo para ordenar posts hierarquicamente usando a coluna ancestry - # A ordenação por 'id' garante uma ordem consistente para posts no mesmo nível. - # Este escopo (`order_by_ancestry`) fornece uma maneira padronizada e reutilizável - # de ordenar os resultados pela hierarquia da `ancestry`, garantindo que a árvore - # seja exibida corretamente na view. - scope :order_by_ancestry, -> { order(Arel.sql("ancestry, id")) } + # Ordena posts por última atividade (threads com comentários recentes aparecem primeiro) + scope :order_by_ancestry, -> { order(last_activity_at: :desc, id: :desc) } private @@ -229,4 +229,16 @@ def change_counter_draft end end + # Atualiza last_activity_at do post e de todos os ancestrais até a raiz + # Exemplo: quando F é criado/editado na árvore A -> C -> F, atualiza F, C e A + def update_last_activity + current_time = Time.current + self.update_column(:last_activity_at, current_time) + + ancestor_ids = self.path_ids - [self.id] + unless ancestor_ids.empty? + Post.where(id: ancestor_ids).update_all(last_activity_at: current_time) + end + end + end From 92dd034e2e8c43e8d3acb754ce14257ae70d80fc Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Tue, 9 Dec 2025 20:57:14 -0300 Subject: [PATCH 07/15] Adiciona eager loading para otimizar carregamento de posts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mudanças no PostsController: - Adiciona eager loading dos filhos dos posts da página atual - Evita N+1 queries ao renderizar post.children na view - Usa includes(:files, :user, :profile) para carregar associações --- app/controllers/posts_controller.rb | 33 +++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index c01e45e2b..f68daaff4 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -48,6 +48,15 @@ def index @posts = @discussion.posts_by_allocation_tags_ids(@allocation_tags, current_user.id, my_list).paginate(page: params[:page] || 1, per_page: Rails.application.config.items_per_page) else @posts = @discussion.posts_for_tree_view(@allocation_tags, current_user.id, params[:page] || 1) + + # Eager loading dos filhos apenas dos posts da página atual + # Evita N+1 queries ao renderizar post.children na view recursivamente + # Escalável: carrega apenas filhos dos 10 posts raízes da página, não de todos os posts do banco + if @posts.any? + Post.includes(:files, :user, :profile) + .where(parent_id: @posts.map(&:id)) + .load + end end if current_user.is_student?([@allocation_tags].flatten) @acu = AcademicAllocationUser.find_or_create_one(@academic_allocation.id, [@allocation_tags].flatten, current_user.id, nil, false, AcademicAllocationUser::STATUS[:empty]) @@ -179,6 +188,30 @@ def post_files render json: { alert: post.errors.full_messages.join('; ') }, status: :unprocessable_entity end + # TODO: Lazy loading não implementado na view + # Para implementar: adicionar botões .view-children-btn na view e descomentar esta action + # def children + # @post = Post.find(params[:id]) + # @discussion = @post.discussion + # + # # Re-creating instance variables needed by the partial + # authorize! :index, Discussion, { on: [@allocation_tags = active_tab[:url][:allocation_tag_id] || @discussion.allocation_tags.map(&:id)], read: true } + # @academic_allocation = AcademicAllocation.where(academic_tool_id: @discussion.id, academic_tool_type: 'Discussion', allocation_tag_id: [active_tab[:url][:allocation_tag_id], AllocationTag.find_by_offer_id(active_tab[:url][:id])&.id]).first + # @researcher = current_user.is_researcher?(AllocationTag.where(id: @allocation_tags).map(&:related).flatten.uniq) + # @class_participants = AllocationTag.get_participants(active_tab[:url][:allocation_tag_id], { all: true }).map(&:id) + # @can_interact = @discussion.user_can_interact?(current_user.id) + # @can_post = (can? :create, Post, on: [@allocation_tags]) + # @can_evaluate = can? :evaluate, Discussion, {on: [@allocation_tags]} + # + # render partial: 'post', collection: @post.children, layout: false, locals: { + # display_mode: 'tree', + # can_interact: @can_interact, + # can_post: @can_post, + # current_user: current_user, + # new_post: false + # } + # end + private def post_params From 10702fa3bbdb28283efcbb62d04cac062f25e0da Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Tue, 9 Dec 2025 20:57:37 -0300 Subject: [PATCH 08/15] =?UTF-8?q?Atualiza=20view=20para=20ordenar=20filhos?= =?UTF-8?q?=20por=20=C3=BAltima=20atividade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mudança na partial _post.html.haml: - Ordena post.children por last_activity_at DESC - Garante que comentários recentes apareçam primeiro - Usa id DESC como critério de desempate Comentários com atividade recente agora aparecem no topo dentro de cada post, melhorando a experiência do usuário. --- app/views/posts/_post.html.haml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/views/posts/_post.html.haml b/app/views/posts/_post.html.haml index 0b1874c4b..4f540a897 100755 --- a/app/views/posts/_post.html.haml +++ b/app/views/posts/_post.html.haml @@ -64,10 +64,9 @@ %li= button_tag content_tag(:i, nil, class: 'icon-reply'), class: 'btn btn_disabled', disabled: true, :'data-tooltip'=> t('.answer'), :'aria-label'=> t('.answer') .forum_post_reply - unless display_mode == 'list' - -# A renderização dos filhos agora usa o método `post.children` da gem `ancestry`, - -# que é mais eficiente e evita o problema de N+1 queries. + -# Ordena filhos por última atividade (comentários recentes aparecem primeiro) - if post.has_children? - - post.children.each do |child| + - post.children.order(last_activity_at: :desc, id: :desc).each do |child| = render partial: 'post', locals: { post: child, display_mode: display_mode, can_interact: can_interact, can_post: can_post, current_user: current_user, new_post: new_post } ._line From f495043c3ef6c747a44adfbb6c65cf5705f923ed Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Wed, 10 Dec 2025 13:36:14 -0300 Subject: [PATCH 09/15] Adiciona migration para coluna last_activity_at MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O que essa migration faz: 1. Adiciona coluna last_activity_at 2. Popula automaticamente para posts existentes usando query SQL otimizada 3. Para cada post, busca o updated_at mais recente de toda a sua subárvore 4. Adiciona índice para otimizar ordenação --- ...dd_last_activity_at_to_discussion_posts.rb | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 db/migrate/20251209113048_add_last_activity_at_to_discussion_posts.rb diff --git a/db/migrate/20251209113048_add_last_activity_at_to_discussion_posts.rb b/db/migrate/20251209113048_add_last_activity_at_to_discussion_posts.rb new file mode 100644 index 000000000..49f36c7de --- /dev/null +++ b/db/migrate/20251209113048_add_last_activity_at_to_discussion_posts.rb @@ -0,0 +1,31 @@ +# class AddLastActivityAtToDiscussionPosts < ActiveRecord::Migration[5.1] +# def up +# # Adiciona coluna para armazenar timestamp da última atividade na subárvore +# add_column :discussion_posts, :last_activity_at, :datetime + +# # Popula a coluna com dados existentes +# # Para cada post, busca o updated_at mais recente entre ele e seus descendentes +# say_with_time "Populando last_activity_at para posts existentes..." do +# execute <<-SQL +# UPDATE discussion_posts AS post +# SET last_activity_at = COALESCE( +# ( +# SELECT MAX(descendants.updated_at) +# FROM discussion_posts AS descendants +# WHERE descendants.ancestry LIKE post.id || '/%' +# OR descendants.id = post.id +# ), +# post.updated_at +# ) +# SQL +# end + +# # Índice para otimizar ordenação por última atividade +# add_index :discussion_posts, :last_activity_at +# end + +# def down +# remove_index :discussion_posts, :last_activity_at +# remove_column :discussion_posts, :last_activity_at +# end +# end From 9d68674cd24bf70694bab1202898e772fb8c9e0f Mon Sep 17 00:00:00 2001 From: Bianca Date: Wed, 10 Dec 2025 14:51:57 -0300 Subject: [PATCH 10/15] deixando o ignore de timestamp comentado; descomentando migrate de last_activity --- config/application.rb | 2 +- ...dd_last_activity_at_to_discussion_posts.rb | 44 +++++++++---------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/config/application.rb b/config/application.rb index 736f79462..6c1299bf2 100644 --- a/config/application.rb +++ b/config/application.rb @@ -29,7 +29,7 @@ class Application < Rails::Application # :all can be used as a placeholder for all plugins not explicitly named. # config.plugins = [ :exception_notification, :ssl_requirement, :all ] - config.active_record.record_timestamps = false + # config.active_record.record_timestamps = false # Activate observers that should always be running. # config.active_record.observers = :cacher, :garbage_collector, :forum_observer diff --git a/db/migrate/20251209113048_add_last_activity_at_to_discussion_posts.rb b/db/migrate/20251209113048_add_last_activity_at_to_discussion_posts.rb index 49f36c7de..1d787064f 100644 --- a/db/migrate/20251209113048_add_last_activity_at_to_discussion_posts.rb +++ b/db/migrate/20251209113048_add_last_activity_at_to_discussion_posts.rb @@ -1,31 +1,31 @@ -# class AddLastActivityAtToDiscussionPosts < ActiveRecord::Migration[5.1] -# def up -# # Adiciona coluna para armazenar timestamp da última atividade na subárvore -# add_column :discussion_posts, :last_activity_at, :datetime +class AddLastActivityAtToDiscussionPosts < ActiveRecord::Migration[5.1] + def up + # Adiciona coluna para armazenar timestamp da última atividade na subárvore + add_column :discussion_posts, :last_activity_at, :datetime # # Popula a coluna com dados existentes # # Para cada post, busca o updated_at mais recente entre ele e seus descendentes # say_with_time "Populando last_activity_at para posts existentes..." do # execute <<-SQL -# UPDATE discussion_posts AS post -# SET last_activity_at = COALESCE( -# ( -# SELECT MAX(descendants.updated_at) -# FROM discussion_posts AS descendants -# WHERE descendants.ancestry LIKE post.id || '/%' -# OR descendants.id = post.id -# ), -# post.updated_at -# ) + # UPDATE discussion_posts AS post + # SET last_activity_at = COALESCE( + # ( + # SELECT MAX(descendants.updated_at) + # FROM discussion_posts AS descendants + # WHERE descendants.ancestry LIKE post.id || '/%' + # OR descendants.id = post.id + # ), + # post.updated_at + # ) # SQL # end -# # Índice para otimizar ordenação por última atividade -# add_index :discussion_posts, :last_activity_at -# end + # Índice para otimizar ordenação por última atividade + add_index :discussion_posts, :last_activity_at + end -# def down -# remove_index :discussion_posts, :last_activity_at -# remove_column :discussion_posts, :last_activity_at -# end -# end + def down + remove_index :discussion_posts, :last_activity_at + remove_column :discussion_posts, :last_activity_at + end +end From 6c80815116c40564e0977948e73a7a54705ae91b Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Fri, 19 Dec 2025 08:18:17 -0300 Subject: [PATCH 11/15] Adiciona migration para remover coluna parent_id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove a coluna parent_id e seu índice da tabela discussion_posts, pois o relacionamento hierárquico agora é gerenciado pela gem ancestry através da coluna ancestry. --- ...112205_remove_parent_id_from_discussion_posts.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 db/migrate/20251218112205_remove_parent_id_from_discussion_posts.rb diff --git a/db/migrate/20251218112205_remove_parent_id_from_discussion_posts.rb b/db/migrate/20251218112205_remove_parent_id_from_discussion_posts.rb new file mode 100644 index 000000000..a60127cdc --- /dev/null +++ b/db/migrate/20251218112205_remove_parent_id_from_discussion_posts.rb @@ -0,0 +1,13 @@ +class RemoveParentIdFromDiscussionPosts < ActiveRecord::Migration[5.1] + def up + # Remove o index primeiro + remove_index :discussion_posts, name: :parent_id_idx if index_exists?(:discussion_posts, :parent_id, name: :parent_id_idx) + # Depois remove a coluna + remove_column :discussion_posts, :parent_id + end + + def down + add_column :discussion_posts, :parent_id, :integer + add_index :discussion_posts, :parent_id, name: :parent_id_idx + end +end From 96caa869f0a732159cc5322c1ac20b2745553dee Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Fri, 19 Dec 2025 08:18:18 -0300 Subject: [PATCH 12/15] =?UTF-8?q?Remove=20associa=C3=A7=C3=A3o=20belongs?= =?UTF-8?q?=5Fto=20:parent=20do=20model=20Post?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comenta a associação belongs_to :parent e substitui todas as referências de parent_id por ancestry nas validações, callbacks e métodos privados, adequando o model para uso da gem ancestry. --- app/models/post.rb | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/app/models/post.rb b/app/models/post.rb index c95d858f2..8e7a64377 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -9,19 +9,19 @@ class Post < ActiveRecord::Base has_ancestry orphan_strategy: :destroy belongs_to :profile - belongs_to :parent, class_name: 'Post' + # belongs_to :parent, class_name: 'Post' belongs_to :user belongs_to :academic_allocation, -> { where academic_tool_type: 'Discussion' } belongs_to :academic_allocation_user - validates :parent, presence: true, unless: -> {parent_id.blank?} + validates :parent, presence: true, unless: -> {ancestry.blank?} before_destroy :verify_children_with_raise, :can_change?, if: -> {merge.nil?} validate :verify_children, on: :update # A associação `has_many :children` manual foi removida pois a gem `ancestry` # já cria essa associação de forma otimizada, evitando conflitos. - # has_many :children, class_name: 'Post', foreign_key: 'parent_id', dependent: :destroy + # has_many :children, class_name: 'Post', foreign_key: 'ancestry', dependent: :destroy has_many :files, class_name: 'PostFile', foreign_key: 'discussion_post_id', dependent: :destroy accepts_nested_attributes_for :files, allow_destroy: true, reject_if: proc {|attributes| !attributes.include?(:attachment) || attributes[:attachment] == '0' || attributes[:attachment].blank?} @@ -32,18 +32,18 @@ class Post < ActiveRecord::Base # Mantém last_activity_at atualizado em toda a árvore quando há atividade after_create :increment_counter, :update_last_activity after_destroy :decrement_counter, :remove_drafts_children, :decrement_counter_draft - after_save :change_counter_draft, if: -> {!parent_id.nil? && saved_change_to_draft?} - after_save :send_email, unless: -> {parent_id.blank? || draft || parent.user_id == user_id || !merge.nil?} + after_save :change_counter_draft, if: -> {!ancestry.nil? && saved_change_to_draft?} + after_save :send_email, unless: -> {ancestry.blank? || draft || parent.user_id == user_id || !merge.nil?} after_save :update_last_activity, if: -> {saved_change_to_content? || saved_change_to_updated_at?} validates :content, :profile_id, presence: true validate :can_change?, if: -> {merge.nil?} - validate :parent_post, if: -> {merge.nil? && !parent_id.blank?} + validate :parent_post, if: -> {merge.nil? && !ancestry.blank?} validate :can_set_draft?, if: -> {!new_record? && saved_change_to_draft? && draft} - before_save :set_parent, if: -> {!new_record? && saved_change_to_parent_id?} + before_save :set_parent, if: -> {!new_record? && saved_change_to_ancestry?} before_save :set_draft, if: -> {draft.nil?} before_save :remove_draft_children, if: -> {saved_change_to_draft? && draft} @@ -57,9 +57,9 @@ def parent_post errors.add(:base, I18n.t('posts.error.draft')) if parent.draft end - # cant change parent_id + # cant change ancestry def set_parent - self.parent_id = parent_id_was + self.ancestry = ancestry_was return true end @@ -199,7 +199,7 @@ def send_email private def set_level - self.level = parent.level.to_i + 1 unless parent_id.nil? + self.level = parent.level.to_i + 1 unless ancestry.nil? end def remove_all_files @@ -210,22 +210,22 @@ def remove_all_files end def increment_counter - Post.increment_counter('children_count', parent_id) + Post.increment_counter('children_count', ancestry) end def decrement_counter - Post.decrement_counter('children_count', parent_id) unless parent.blank? || parent.try(:children_count) == 0 + Post.decrement_counter('children_count', ancestry) unless parent.blank? || parent.try(:children_count) == 0 end def decrement_counter_draft - Post.decrement_counter('children_drafts_count', parent_id) unless parent.blank? || parent.try(:children_drafts_count) == 0 || !draft_was + Post.decrement_counter('children_drafts_count', ancestry) unless parent.blank? || parent.try(:children_drafts_count) == 0 || !draft_was end def change_counter_draft if draft - Post.increment_counter('children_drafts_count', parent_id) + Post.increment_counter('children_drafts_count', ancestry) else - Post.decrement_counter('children_drafts_count', parent_id) unless parent.try(:children_drafts_count) == 0 + Post.decrement_counter('children_drafts_count', ancestry) unless parent.try(:children_drafts_count) == 0 end end From 45237bf4780f735549e0df8b0b17de029271a5c8 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Fri, 19 Dec 2025 08:18:19 -0300 Subject: [PATCH 13/15] Atualiza controller para usar ancestry no lugar de parent_id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Substitui parent_id por ancestry nas queries, respostas JSON e parâmetros permitidos, mantendo compatibilidade com a gem ancestry para hierarquia de posts. --- app/controllers/posts_controller.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index f68daaff4..5a6894ebd 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -7,6 +7,7 @@ class PostsController < ApplicationController before_action :prepare_for_pagination before_action :set_current_user, only: [:destroy, :create, :update, :publish, :post_files] + #autoriza é o metodo para autorizar todas as çãoes em um controlador de recursos load_and_authorize_resource except: [:index, :user_posts, :create, :show, :evaluate, :publish, :post_files] ## GET /discussions/1/posts @@ -54,7 +55,7 @@ def index # Escalável: carrega apenas filhos dos 10 posts raízes da página, não de todos os posts do banco if @posts.any? Post.includes(:files, :user, :profile) - .where(parent_id: @posts.map(&:id)) + .where(ancestry: @posts.map(&:id)) .load end end @@ -114,7 +115,7 @@ def create authorize! :create, Post if new_post_under_discussion(Discussion.find(params[:discussion_id])) - render json: {result: 1, post_id: @post.id, parent_id: @post.parent_id}, status: :created + render json: {result: 1, post_id: @post.id, ancestry: @post.ancestry}, status: :created else render json: { result: 0, alert: @post.errors.full_messages.join('; ') }, status: :unprocessable_entity end @@ -130,7 +131,7 @@ def create def update @post = Post.find(params[:id]) if @post.update_attributes post_params - render json: {success: true, post_id: @post.id, parent_id: @post.parent_id} + render json: {success: true, post_id: @post.id, ancestry: @post.ancestry} else render json: { result: 0, alert: @post.errors.full_messages.join('; ') }, status: :unprocessable_entity end @@ -145,7 +146,7 @@ def update def publish @post = Post.find(params[:id]) @post.update_attributes draft: false - render json: { success: true, post_id: @post.id, discussion_id: @post.discussion.id, content: @post.content, ac_id: @post.academic_allocation_id, parent_id: @post.parent_id }, status: :ok + render json: { success: true, post_id: @post.id, discussion_id: @post.discussion.id, content: @post.content, ac_id: @post.academic_allocation_id, ancestry: @post.ancestry }, status: :ok rescue => error render_json_error(error, 'discussions.error') end @@ -215,7 +216,7 @@ def post_files private def post_params - params.require(:post).permit(:content, :parent_id, :discussion_id, :draft, files_attributes: [:id, :attachment, :_destroy]) + params.require(:post).permit(:content, :ancestry, :discussion_id, :draft, files_attributes: [:id, :attachment, :_destroy]) end def new_post_under_discussion(discussion) From 1af7fc02f239da61edc43273e1e52d27120ceae3 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Fri, 19 Dec 2025 08:18:20 -0300 Subject: [PATCH 14/15] Atualiza view de posts para usar ancestry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Substitui parent_id por ancestry no formulário e em todas as variáveis JavaScript, garantindo que o frontend funcione corretamente com a gem ancestry. --- app/views/posts/index.html.haml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/views/posts/index.html.haml b/app/views/posts/index.html.haml index 76f538236..fe83477d3 100644 --- a/app/views/posts/index.html.haml +++ b/app/views/posts/index.html.haml @@ -48,7 +48,7 @@ .new_posts #new_post = simple_form_for(@post, html: { id: 'new_post_form', :'action-to-edit' => discussion_post_path(@discussion.id, ':id'), :"action-default" => discussion_posts_path(@discussion.id), method: :put, multipart: true }) do |form| - = form.hidden_field(:parent_id, value: '', id: 'discussion_post_parent_id') + = form.hidden_field(:ancestry, value: '', id: 'discussion_post_parent_id') = form.hidden_field(:display_mode, value: @display_mode, id: 'display_mode_1') = form.hidden_field(:current_page, value: @current_page, id: 'current_page_1') = form.text_area(:content) @@ -171,9 +171,9 @@ ws.onmessage = function (msg) { var data = JSON.parse(msg.data); var post_table = $("article#"+data.post_id); - var parent_table = $("article#"+data.parent_id); + var parent_table = $("article#"+data.ancestry); // recover grandparent if has parent, but it doesn't exists at view (so it must render all tree of posts) and isn't list view and it is at first page - var grandparent = (data.parent_id != null && !parent_table.length && !display_list && (1 == parseInt("#{escape_once(@current_page)}"))); + var grandparent = (data.ancestry != null && !parent_table.length && !display_list && (1 == parseInt("#{escape_once(@current_page)}"))); $.get("#{discussion_post_path(@discussion.id, ':id', researcher: @researcher)}".replace(':id', data.post_id), {new_post: true, grandparent: grandparent}, function(data2){ if (!!post_table.length) // if post exists @@ -183,7 +183,7 @@ $(parent_table.find("._cell.forum_post_content .forum_post_wrapper")[0]).after(data2); parent_table.find(".forum_post_buttons:first").find(".update_post:first, .forum_button_attachment:first, .btn_caution:first").remove(); }else // if hasn't parent and is at the same page or is list view - if (data.parent_id == null && (1 == parseInt("#{escape_once(@current_page)}")) || !!display_list || !!grandparent) + if (data.ancestry == null && (1 == parseInt("#{escape_once(@current_page)}")) || !!display_list || !!grandparent) $(".forum_posts_wrapper").prepend(data2); add_post_count_warning("#{escape_once(@current_page)}"); @@ -204,13 +204,13 @@ content = '

'+content+'

'; } - // get parent_id information + // get ancestry information if (form.attr("method") == "POST") var parent_table = $($(this).parents("article")[0]); if (parent_table == undefined) - var parent_id = null; + var ancestry = null; else - var parent_id = parent_table.attr("id"); + var ancestry = parent_table.attr("id"); var draft = $(this).data('draft'); @@ -260,7 +260,7 @@ ws.send(JSON.stringify({ msg: content, post_id: data.post_id, - parent_id: data.parent_id, + ancestry: data.ancestry, academic_allocation_id: ac })); loadFocus(); @@ -359,7 +359,7 @@ ws.send(JSON.stringify({ msg: data.content, post_id: data.post_id, - parent_id: data.parent_id, + ancestry: data.ancestry, academic_allocation_id: data.ac_id })); }).fail(function(){ From 4e16681d240960bb30281fe7fadc732b3573a8f7 Mon Sep 17 00:00:00 2001 From: "francisco.juliao" Date: Fri, 19 Dec 2025 08:18:21 -0300 Subject: [PATCH 15/15] Adiciona tratamento de erro para rotas inexistentes no menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Envolve a geração de links do menu em um bloco begin/rescue para capturar ActionController::UrlGenerationError e ignorar menus cujas rotas não existem na aplicação, evitando que o menu quebre quando há menus cadastrados no banco mas sem rotas correspondentes implementadas. --- app/helpers/menu_helper.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/helpers/menu_helper.rb b/app/helpers/menu_helper.rb index fc32645d7..56663c83d 100644 --- a/app/helpers/menu_helper.rb +++ b/app/helpers/menu_helper.rb @@ -26,8 +26,15 @@ def menu_list menus.each_with_index do |menu, idx| contexts = menu.contexts.pluck(:id) code_to_shortcut = menu.name - menu_item_link = link_to(t(menu.name), url_for({controller: "/#{menu.resource.controller}", action: menu.resource.action, - bread: menu.name, contexts: contexts.join(',')}), onclick: 'focusTitle();', onkeypress: 'focusTitle();', onkeydown: 'click_on_keypress(event, this);', class: menu.parent.nil? ? 'mysolar_menu_title' : '', :'data-shortcut' => (I18n.translate!(code_to_shortcut, scope: "shortcut.vertical_menu.code", raise: true) rescue ''), :'data-shortcut-shift' => true, :'data-shortcut-complement' => (I18n.translate!(code_to_shortcut, scope: "shortcut.vertical_menu.complement", raise: true) rescue '')) + + # Ignora menus com rotas inexistentes (ex: FAQs em outra branch) + begin + menu_item_link = link_to(t(menu.name), url_for({controller: "/#{menu.resource.controller}", action: menu.resource.action, + bread: menu.name, contexts: contexts.join(',')}), onclick: 'focusTitle();', onkeypress: 'focusTitle();', onkeydown: 'click_on_keypress(event, this);', class: menu.parent.nil? ? 'mysolar_menu_title' : '', :'data-shortcut' => (I18n.translate!(code_to_shortcut, scope: "shortcut.vertical_menu.code", raise: true) rescue ''), :'data-shortcut-shift' => true, :'data-shortcut-complement' => (I18n.translate!(code_to_shortcut, scope: "shortcut.vertical_menu.complement", raise: true) rescue '')) + rescue ActionController::UrlGenerationError + next # Pula menus com rotas inexistentes + end + menu_item = {contexts: contexts, bread: menu.name, link: menu_item_link} if menu.parent.nil?