kanbanworkflowstimelinescrumrubyroadmapproject-planningproject-managementopenprojectangularissue-trackerifcgantt-chartganttbug-trackerboardsbcf
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.
318 lines
11 KiB
318 lines
11 KiB
#-- encoding: UTF-8
|
|
|
|
#-- copyright
|
|
# OpenProject is an open source project management software.
|
|
# Copyright (C) 2012-2021 the OpenProject GmbH
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License version 3.
|
|
#
|
|
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
|
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
# Copyright (C) 2010-2013 the ChiliProject Team
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 2
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
#
|
|
# See docs/COPYRIGHT.rdoc for more details.
|
|
#++
|
|
|
|
###############################################################################
|
|
# The base activity provider class provides a default implementation for the #
|
|
# most common activity jobs. You may implement the following methods to set #
|
|
# the respective activity details: #
|
|
# - event_name #
|
|
# - event_title #
|
|
# - event_type #
|
|
# - event_description #
|
|
# - event_datetime #
|
|
# - event_path #
|
|
# - event_url #
|
|
# #
|
|
# See the comments on the methods to get additional information. #
|
|
###############################################################################
|
|
class Activities::BaseActivityProvider
|
|
include I18n
|
|
include Redmine::I18n
|
|
include OpenProject::StaticRouting
|
|
|
|
class_attribute :activity_provider_options
|
|
|
|
# Returns events of type event_type visible by user that occurred between from and to
|
|
def self.find_events(event_type, user, from, to, options)
|
|
raise "#{name} can not provide #{event_type} events." if activity_provider_options[:type] != event_type
|
|
|
|
activity_provider_options[:activities]
|
|
.map { |activity| new(activity).find_events(user, from, to, options) }
|
|
.flatten
|
|
end
|
|
|
|
def initialize(activity)
|
|
self.activity = activity
|
|
end
|
|
|
|
def self.activity_provider_for(options = {})
|
|
options.assert_valid_keys(:type, :permission, :activities, :aggregated)
|
|
|
|
self.activity_provider_options = {
|
|
type: name.underscore.pluralize,
|
|
activities: [:activity],
|
|
aggregated: false,
|
|
permission: "view_#{name.underscore.pluralize}".to_sym
|
|
}.merge(options)
|
|
end
|
|
|
|
def find_events(user, from, to, options)
|
|
query = if aggregated?
|
|
aggregated_event_selection_query(user, from, to, options)
|
|
else
|
|
event_selection_query(user, from, to, options)
|
|
end
|
|
|
|
query = apply_order(query)
|
|
query = apply_limit(query, options)
|
|
query = apply_event_projection(query)
|
|
fill_events(query)
|
|
end
|
|
|
|
def fill_events(events_query)
|
|
ActiveRecord::Base.connection.select_all(events_query.to_sql).map do |e|
|
|
params = event_params(e)
|
|
|
|
Activities::Event.new(**params) if params
|
|
end
|
|
end
|
|
|
|
#############################################################################
|
|
# Activities may need information not available in the journal table. Thus, #
|
|
# if you need further information from different tables (e.g., projects #
|
|
# table) you may extend the query in this method. #
|
|
#############################################################################
|
|
def extend_event_query(query)
|
|
query
|
|
end
|
|
|
|
#############################################################################
|
|
# This method returns a list of columns that the activity query needs to #
|
|
# return, so the activity provider can actually create an activity object. #
|
|
# You must at least return the column containing the project reference with #
|
|
# the alias 'project_id'. #
|
|
#############################################################################
|
|
def event_query_projection
|
|
[]
|
|
end
|
|
|
|
def event_datetime(event)
|
|
event['event_datetime'].is_a?(String) ? DateTime.parse(event['event_datetime']) : event['event_datetime']
|
|
end
|
|
|
|
def event_type(_event_data)
|
|
activity_provider_options[:type]
|
|
end
|
|
|
|
#############################################################################
|
|
# Override this method if the journal table does not contain a reference to #
|
|
# the 'projects' table. #
|
|
#############################################################################
|
|
def projects_reference_table
|
|
activity_journals_table
|
|
end
|
|
|
|
def activitied_type
|
|
activity_type = self.class.name
|
|
|
|
class_name = activity_type.demodulize
|
|
class_name.gsub('ActivityProvider', '').constantize
|
|
end
|
|
|
|
protected
|
|
|
|
def event_selection_query(user, from, to, options)
|
|
query = journals_with_data_query
|
|
query = extend_event_query(query) unless aggregated?
|
|
query = filter_for_event_datetime(query, from, to)
|
|
query = restrict_user(query, options)
|
|
restrict_projects(query, user, options)
|
|
end
|
|
|
|
def apply_event_projection(query)
|
|
projection = event_projection
|
|
projection << event_query_projection if respond_to?(:event_query_projection)
|
|
|
|
query.project(projection)
|
|
end
|
|
|
|
# When aggregating, we add the query that actually fetches the journals,
|
|
# restricted by from, to, user permission, etc. as a CTE. That way,
|
|
# that query has only to be executed once inside the aggregated journal query which
|
|
# considerably reduces execution time.
|
|
def aggregated_event_selection_query(user, from, to, options)
|
|
query = aggregated_journal_query
|
|
query = add_event_selection_query_as_cte(query, user, from, to, options)
|
|
query = join_activity_journals_table(query)
|
|
query = extend_event_query(query)
|
|
join_with_projects_table(query)
|
|
end
|
|
|
|
def apply_limit(query, options)
|
|
if options[:limit]
|
|
query.take(options[:limit])
|
|
else
|
|
query
|
|
end
|
|
end
|
|
|
|
def filter_for_event_datetime(query, from, to)
|
|
query = query.where(journals_table[:created_at].gteq(from)) if from
|
|
query = query.where(journals_table[:created_at].lteq(to)) if to
|
|
|
|
query
|
|
end
|
|
|
|
def apply_order(query)
|
|
query.order(journals_table[:id].desc)
|
|
end
|
|
|
|
def event_params(event_data)
|
|
params = { provider: self,
|
|
event_description: event_data['event_description'],
|
|
author_id: event_data['event_author'].to_i,
|
|
journable_id: event_data['journable_id'],
|
|
project_id: event_data['project_id'].to_i }
|
|
|
|
%i[event_name event_title event_type event_description event_datetime event_path event_url].each do |a|
|
|
params[a] = send(a, event_data) if self.class.method_defined? a
|
|
end
|
|
|
|
params
|
|
rescue StandardError => e
|
|
Rails.logger.error "Failed to deduce event params for #{event_data.inspect}: #{e}"
|
|
end
|
|
|
|
def event_projection
|
|
[[:id, 'event_id'],
|
|
[:created_at, 'event_datetime'],
|
|
[:user_id, 'event_author'],
|
|
[:notes, 'event_description'],
|
|
[:version, 'version'],
|
|
[:journable_id, 'journable_id']].map do |column, alias_name|
|
|
journals_table[column].as(alias_name)
|
|
end
|
|
end
|
|
|
|
def join_with_projects_table(query)
|
|
query.join(projects_table).on(projects_table[:id].eq(projects_reference_table['project_id']))
|
|
end
|
|
|
|
def restrict_user(query, options)
|
|
query = query.where(journals_table[:user_id].eq(options[:author].id)) if options[:author]
|
|
query
|
|
end
|
|
|
|
def restrict_projects(query, user, options)
|
|
query = join_with_projects_table(query)
|
|
query = restrict_projects_by_selection(options, query)
|
|
query = restrict_projects_by_activity_module(query)
|
|
restrict_projects_by_permission(query, user)
|
|
end
|
|
|
|
def restrict_projects_by_selection(options, query)
|
|
if (project = options[:project])
|
|
query = query.where(project.project_condition(options[:with_subprojects]))
|
|
end
|
|
|
|
query
|
|
end
|
|
|
|
def restrict_projects_by_activity_module(query)
|
|
# Have to use the string based where here as the resulting
|
|
# sql would otherwise expect a parameter for the prepared statement.
|
|
query.where(projects_table[:id].in(EnabledModule.where("name = 'activity'").select(:project_id).arel))
|
|
end
|
|
|
|
def restrict_projects_by_permission(query, user)
|
|
perm = activity_provider_options[:permission]
|
|
|
|
query.where(projects_table[:id].in(Project.allowed_to(user, perm).select(:id).arel))
|
|
end
|
|
|
|
def aggregated_journal_query
|
|
# As AggregatedJournal wraps the provided sql statement inside brackets we
|
|
# need to provide a fully valid statement and not only the alias string.
|
|
Journal.aggregated_journal(sql: "SELECT * FROM #{aggregated_journals_alias}").arel
|
|
end
|
|
|
|
def add_event_selection_query_as_cte(query, user, from, to, options)
|
|
cte_query = event_selection_query(user, from, to, options).project('journals.*')
|
|
cte = Arel::Nodes::As.new(Arel::Table.new(aggregated_journals_alias), cte_query)
|
|
|
|
query.with(cte)
|
|
end
|
|
|
|
attr_accessor :activity
|
|
|
|
def aggregated?
|
|
activity_provider_options[:aggregated]
|
|
end
|
|
|
|
def journals_with_data_query
|
|
join_activity_journals_table(journals_table)
|
|
.where(journals_table[:journable_type].eq(activitied_type.name))
|
|
end
|
|
|
|
def join_activity_journals_table(query)
|
|
query
|
|
.join(activity_journals_table).on(journals_table[:id].eq(activity_journals_table[:journal_id]))
|
|
end
|
|
|
|
def journals_table
|
|
Journal.arel_table
|
|
end
|
|
|
|
def activitied_table
|
|
@activitied_table ||= activitied_type.arel_table
|
|
end
|
|
|
|
def projects_table
|
|
@projects_table ||= Project.arel_table
|
|
end
|
|
|
|
def enabled_modules_table
|
|
@enabled_modules_table ||= EnabledModule.arel_table
|
|
end
|
|
|
|
def activity_journals_table
|
|
@activity_journals_table ||= activitied_type.journal_class.arel_table
|
|
end
|
|
|
|
def activity_journal_projection_statement(column, name)
|
|
projection_statement(activity_journals_table, column, name)
|
|
end
|
|
|
|
def projection_statement(table, column, name)
|
|
table[column].as(name)
|
|
end
|
|
|
|
def event_name(event)
|
|
I18n.t(event_type(event).underscore, scope: 'events')
|
|
end
|
|
|
|
def aggregated_journals_alias
|
|
:relevant_journals
|
|
end
|
|
|
|
def url_helpers
|
|
@url_helpers ||= OpenProject::StaticRouting::StaticUrlHelpers.new
|
|
end
|
|
end
|
|
|