Files
openproject/modules/costs/app/models/work_package/abstract_costs.rb
T
Jan Sandbrink f9d8bc6614 Introduce SubclassResponsibility error
This error is intended for cases when a method is
intentionally not implemented, because the module/class defining
it expects a subclass (or class including the module) to implement
the method.

This is intended to distinguish it from other cases, such as:
* feature not implemented yet
* edge case of a method call not yet supported

Notably it avoids the misuse of the Ruby-defined NotImplementedError,
which is only intended for much more specific scenarios:

> Raised when a feature is not implemented on the current platform. For example, methods depending on the fsync or fork system calls may raise this exception [...]

Also see https://docs.ruby-lang.org/en/master/NotImplementedError.html
2026-03-27 08:14:56 +01:00

148 lines
4.1 KiB
Ruby

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)
scope = costs_model.where(entity_type: "WorkPackage", entity_id: wp_ids).joins(:project)
filter_authorized(scope)
.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 SubclassResponsibilityError
end
def costs_sum_alias
raise SubclassResponsibilityError
end
def subselect_alias
raise SubclassResponsibilityError
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[:entity_type].eq("WorkPackage").and(ce_table[:entity_id].eq(wp_table_descendants[:id]).and(authorization_where))
end
def projects_table
Project.arel_table
end
end
end