Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -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

Expand Down
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
56 changes: 44 additions & 12 deletions app/controllers/posts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
11 changes: 9 additions & 2 deletions app/helpers/menu_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
14 changes: 13 additions & 1 deletion app/models/discussion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -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,
Expand Down
112 changes: 74 additions & 38 deletions app/models/post.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,48 @@ 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?}

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}

Expand All @@ -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

Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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

Expand Down
Loading