OpenProject is the leading open source project management software.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
openproject/modules/costs/app/models/work_package/abstract_costs.rb

146 lines
4.1 KiB

class WorkPackage
class AbstractCosts
attr_reader :user, :project
def initialize(user: User.current, project: nil)
@user = user
@project = project
end
##
# Adds to the given WorkPackage query's result an extra costs column.
#
# @param work_package_scope [WorkPackage::ActiveRecord_Relation]
# @return [WorkPackage::ActiveRecord_Relation] The query with the joined costs.
def add_to_work_packages(work_package_scope)
add_costs_to work_package_scope
end
##
# Adds to the given WorkPackage collection query an extra costs column
def add_to_work_package_collection(wp_collection_scope)
add_costs_to wp_collection_scope
end
##
# For the given work packages calculates the sum of all costs.
#
# @param [WorkPackage::ActiveRecord_Relation | Array[WorkPackage]] List of work packages.
# @return [Float] The sum of the work packages' costs.
def costs_of(work_packages:)
# N.B. Because of an AR quirks the code below uses statements like
# where(work_package_id: ids)
# You would expect to be able to simply write those as
# where(work_package: work_packages)
# However, AR (Rails 4.2) will not expand :includes + :references inside a subquery,
# which will render the query invalid. Therefore we manually extract the IDs in a separate (pluck) query.
wp_ids = work_package_ids work_packages
filter_authorized(costs_model.where(work_package_id: wp_ids).joins(work_package: :project))
.sum(costs_value)
.to_f
end
##
# The model on which the costs calculations are based.
# Can be any model which has the fields `overridden_costs` and `costs`
# and is related to work packages (i.e. has a `work_package_id` too).
#
# @return [Class] Class of the model the costs are based on, e.g. CostEntry or TimeEntry.
def costs_model
raise NotImplementedError, "subclass responsibility"
end
def costs_sum_alias
raise NotImplementedError, "subclass responsibility"
end
def subselect_alias
raise NotImplementedError, "subclass responsibility"
end
private
def work_package_ids(work_packages)
if work_packages.respond_to?(:pluck)
work_packages.pluck(:id)
else
Array(work_packages).map(&:id)
end
end
def costs_table_name
costs_model.table_name
end
def add_costs_to(scope)
scope
.joins(sum_arel(scope).join_sources)
.select(costs_sum_alias)
end
def costs_sum
"SUM(#{costs_value})"
end
def costs_value
"COALESCE(#{costs_table_name}.overridden_costs, #{costs_table_name}.costs)"
end
##
# Narrows down the query to only include costs visible to the user.
#
# @param [ActiveRecord::QueryMethods] scope Some query.
# @return [ActiveRecord::QueryMethods] The filtered query.
def filter_authorized(scope)
scope # allow all
end
def sum_arel(base_scope)
subselect = sum_subselect(base_scope)
.as(subselect_alias)
wp_table
.outer_join(subselect)
.on(subselect[:id].eq(wp_table[:id]))
end
def sum_subselect(base_scope)
base_scope
.dup
.left_join_self_and_descendants(user)
.except(:select)
.select("#{costs_sum} AS #{costs_sum_alias}")
.select(wp_table[:id])
.arel
.outer_join(ce_table)
.on(ce_table_join_condition)
.group(wp_table[:id])
end
def wp_table
WorkPackage.arel_table
end
def wp_table_descendants
# Relies on a table called descendants to exist in the scope
# which is provided by left_join_self_and_descendants
wp_table.alias 'descendants'
end
def ce_table
costs_model.arel_table
end
def ce_table_join_condition
authorization_scope = filter_authorized costs_model.all
authorization_where = authorization_scope.arel.ast.cores.last.wheres.last
ce_table[:work_package_id].eq(wp_table_descendants[:id]).and(authorization_where)
end
def projects_table
Project.arel_table
end
end
end