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..f32898483 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,7 @@ source "http://rubygems.org" 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..1596d09a3 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) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 8b0ec860a..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 @@ -41,16 +42,23 @@ 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 + @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(ancestry: @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]) end @@ -107,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 @@ -123,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 @@ -138,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 @@ -181,10 +189,34 @@ 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 - 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) 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? diff --git a/app/models/discussion.rb b/app/models/discussion.rb index 1db9f066e..2aed26d37 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -139,6 +139,19 @@ 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 + 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 + + discussion_posts + .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.*' }) 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 +162,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..8e7a64377 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -2,19 +2,26 @@ 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' + # 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 - - 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: '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?} @@ -22,19 +29,21 @@ 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 :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} @@ -48,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 @@ -87,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' @@ -132,32 +143,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,10 +193,13 @@ def send_email end end + # Ordena posts por última atividade (threads com comentários recentes aparecem primeiro) + scope :order_by_ancestry, -> { order(last_activity_at: :desc, id: :desc) } + 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 @@ -186,22 +210,34 @@ 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 + + # 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 diff --git a/app/views/posts/_post.html.haml b/app/views/posts/_post.html.haml index 62fb01ea3..4f540a897 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) + - editable = ((post.user_id == current_user.id) && (post.children_count == 0 || post.children_drafts_count == post.children_count)) - files = post.files @@ -64,8 +64,10 @@ %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 } + -# Ordena filhos por última atividade (comentários recentes aparecem primeiro) + - if post.has_children? + - 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 :javascript diff --git a/app/views/posts/index.html.haml b/app/views/posts/index.html.haml index 856261f06..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) @@ -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)} @@ -98,6 +99,7 @@ = raw t('posts.post.fv_explain') +- 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 +138,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}"; @@ -169,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 @@ -181,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)}"); @@ -202,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'); @@ -258,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(); @@ -357,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(){ diff --git a/config/application.rb b/config/application.rb index 599dbd24e..6c1299bf2 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 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 + 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..1d787064f --- /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 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