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.
433 lines
10 KiB
433 lines
10 KiB
#-- encoding: UTF-8
|
|
#-- copyright
|
|
# OpenProject is a project management system.
|
|
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
|
#
|
|
# 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 doc/COPYRIGHT.rdoc for more details.
|
|
#++
|
|
|
|
class Timeline < ActiveRecord::Base
|
|
class Empty
|
|
attr_accessor :id, :name
|
|
|
|
def id
|
|
@id ||= -1
|
|
end
|
|
|
|
def name
|
|
@name ||= ::I18n.t('timelines.filter.noneElement')
|
|
end
|
|
end
|
|
|
|
serialize :options, Hash
|
|
|
|
self.table_name = 'timelines'
|
|
|
|
default_scope { order('name ASC') }
|
|
|
|
belongs_to :project, class_name: 'Project'
|
|
|
|
validates_presence_of :name, :project
|
|
validates_length_of :name, maximum: 255, unless: lambda { |e| e.name.blank? }
|
|
validate :validate_option_dates
|
|
validate :validate_option_numeric
|
|
|
|
before_save :remove_empty_options_values
|
|
before_save :split_joined_options_values
|
|
|
|
@@allowed_option_keys = [
|
|
'custom_fields',
|
|
'columns',
|
|
'compare_to_absolute',
|
|
'compare_to_relative',
|
|
'compare_to_relative_unit',
|
|
'compare_to_historical_one',
|
|
'compare_to_historical_two',
|
|
'comparison',
|
|
'exclude_empty',
|
|
'exclude_own_planning_elements',
|
|
'exclude_reporters',
|
|
'exist',
|
|
'grouping_one_enabled',
|
|
'grouping_one_selection',
|
|
'grouping_one_sort',
|
|
'hide_chart',
|
|
'hide_other_group',
|
|
'initial_outline_expansion',
|
|
'parents',
|
|
'planning_element_responsibles',
|
|
'planning_element_assignee',
|
|
'planning_element_status',
|
|
'planning_element_time',
|
|
'planning_element_time_absolute_one',
|
|
'planning_element_time_absolute_two',
|
|
'planning_element_time_relative_one',
|
|
'planning_element_time_relative_one_unit',
|
|
'planning_element_time_relative_two',
|
|
'planning_element_time_relative_two_unit',
|
|
'planning_element_time_types',
|
|
'planning_element_types',
|
|
'project_responsibles',
|
|
'project_sort',
|
|
'project_status',
|
|
'project_types',
|
|
'timeframe_end',
|
|
'timeframe_start',
|
|
'vertical_planning_elements',
|
|
'zoom_factor'
|
|
]
|
|
|
|
@@available_columns = [
|
|
'start_date',
|
|
'due_date',
|
|
'type',
|
|
'status',
|
|
'responsible',
|
|
'assigned_to'
|
|
]
|
|
|
|
@@available_zoom_factors = [
|
|
'years',
|
|
'quarters',
|
|
'months',
|
|
'weeks',
|
|
'days'
|
|
]
|
|
|
|
@@available_initial_outline_expansions = [
|
|
'aggregation',
|
|
'level1',
|
|
'level2',
|
|
'level3',
|
|
'level4',
|
|
'level5',
|
|
'all'
|
|
]
|
|
|
|
def filter_options
|
|
@@allowed_option_keys
|
|
end
|
|
|
|
def validate_option_numeric
|
|
numeric = ['compare_to_relative', 'planning_element_time_relative_one', 'planning_element_time_relative_two']
|
|
numeric.each do |field|
|
|
begin
|
|
if options[field] && options[field] != '' && options[field].to_i.to_s != options[field]
|
|
errors.add :options, l('timelines.filter.errors.' + field) + l('activerecord.errors.messages.not_a_number')
|
|
end
|
|
rescue ArgumentError
|
|
|
|
end
|
|
end
|
|
end
|
|
|
|
def validate_option_dates
|
|
date_fields = ['timeframe_start', 'timeframe_end', 'compare_to_absolute', 'planning_element_time_absolute_one', 'planning_element_time_absolute_two']
|
|
date_fields.each do |field|
|
|
begin
|
|
if options[field] && options[field] != ''
|
|
Date.parse(options[field])
|
|
end
|
|
rescue ArgumentError
|
|
errors.add :options, l('timelines.filter.errors.' + field) + l('activerecord.errors.messages.not_a_date')
|
|
end
|
|
end
|
|
end
|
|
|
|
def default_options
|
|
{}
|
|
end
|
|
|
|
def options
|
|
read_attribute(:options) || default_options
|
|
end
|
|
|
|
def options=(other)
|
|
other.assert_valid_keys(*filter_options)
|
|
write_attribute(:options, other)
|
|
end
|
|
|
|
def json_options
|
|
json = with_escape_html_entities_in_json { options.to_json }
|
|
json.html_safe
|
|
end
|
|
|
|
def custom_field_columns
|
|
project.all_work_package_custom_fields.map { |a| { name: a.name, id: "cf_#{a.id}" } }
|
|
end
|
|
|
|
def available_columns
|
|
@@available_columns
|
|
end
|
|
|
|
def available_initial_outline_expansions
|
|
@@available_initial_outline_expansions
|
|
end
|
|
|
|
def selected_initial_outline_expansion
|
|
if options['initial_outline_expansion'].present?
|
|
options['initial_outline_expansion'].first.to_i
|
|
else
|
|
-1
|
|
end
|
|
end
|
|
|
|
def available_zoom_factors
|
|
@@available_zoom_factors
|
|
end
|
|
|
|
def selected_zoom_factor
|
|
if options['zoom_factor'].present?
|
|
options['zoom_factor'].first.to_i
|
|
else
|
|
-1
|
|
end
|
|
end
|
|
|
|
def available_planning_element_types
|
|
# TODO: this should not be all planning element types, but instead
|
|
# all types that are available in the project the timeline is
|
|
# referencing, and all planning element types available in projects
|
|
# that are reporting into the project that this timeline is
|
|
# referencing.
|
|
|
|
::Type.order(:name)
|
|
end
|
|
|
|
def available_planning_element_status
|
|
types = Project.visible.includes(:types).map(&:types).flatten.uniq
|
|
types.map(&:statuses).flatten.uniq
|
|
end
|
|
|
|
def selected_planning_element_status
|
|
resolve_with_none_element(:planning_element_status) do |ary|
|
|
Status.where(id: ary)
|
|
end
|
|
end
|
|
|
|
def selected_planning_element_types
|
|
resolve_with_none_element(:planning_element_types) do |ary|
|
|
::Type.where(id: ary)
|
|
end
|
|
end
|
|
|
|
def selected_planning_element_time_types
|
|
resolve_with_none_element(:planning_element_time_types) do |ary|
|
|
::Type.where(id: ary)
|
|
end
|
|
end
|
|
|
|
def available_project_types
|
|
ProjectType.all
|
|
end
|
|
|
|
def selected_project_types
|
|
resolve_with_none_element(:project_types) do |ary|
|
|
ProjectType.where(id: ary)
|
|
end
|
|
end
|
|
|
|
def available_project_status
|
|
ReportedProjectStatus.order(:name)
|
|
end
|
|
|
|
def selected_project_status
|
|
resolve_with_none_element(:project_status) do |ary|
|
|
ReportedProjectStatus.where(id: ary)
|
|
end
|
|
end
|
|
|
|
def available_responsibles
|
|
User.all.sort_by(&:name)
|
|
end
|
|
|
|
def selected_project_responsibles
|
|
resolve_with_none_element(:project_responsibles) do |ary|
|
|
User.where(id: ary)
|
|
end
|
|
end
|
|
|
|
def selected_planning_element_responsibles
|
|
resolve_with_none_element(:planning_element_responsibles) do |ary|
|
|
User.where(id: ary)
|
|
end
|
|
end
|
|
|
|
def custom_field_list_value(field_id)
|
|
value = custom_fields_filter[field_id]
|
|
if value
|
|
value.join(',')
|
|
else
|
|
''
|
|
end
|
|
end
|
|
|
|
def custom_fields_filter
|
|
options['custom_fields'] || {}
|
|
end
|
|
|
|
def get_custom_fields
|
|
project.all_work_package_custom_fields.sort_by{ |n| n[:name].downcase }
|
|
end
|
|
|
|
def selected_planning_element_assignee
|
|
resolve_with_none_element(:planning_element_assignee) do |ary|
|
|
User.find(ary)
|
|
end
|
|
end
|
|
|
|
def available_parents
|
|
selectable_projects
|
|
end
|
|
|
|
def selected_parents
|
|
resolve_with_none_element(:parents) do |ary|
|
|
Project.where(id: ary)
|
|
end
|
|
end
|
|
|
|
def selected_columns
|
|
if options['columns'].present?
|
|
available = available_columns + custom_field_column_ids
|
|
|
|
options['columns'] & available
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
def planning_element_time
|
|
if options['planning_element_time'].present?
|
|
options['planning_element_time']
|
|
else
|
|
'absolute'
|
|
end
|
|
end
|
|
|
|
def comparison
|
|
if options['comparison'].present?
|
|
options['comparison']
|
|
else
|
|
'none'
|
|
end
|
|
end
|
|
|
|
def selected_grouping_projects
|
|
resolve_with_none_element(:grouping_one_selection) do |ary|
|
|
projects = Project.where(id: ary)
|
|
projectsHashMap = Hash[projects.map { |v| [v.id, v] }]
|
|
|
|
ary.map { |a| projectsHashMap[a] }
|
|
end
|
|
end
|
|
|
|
def available_grouping_projects
|
|
selectable_projects
|
|
end
|
|
|
|
def selectable_projects
|
|
Project.selectable_projects
|
|
end
|
|
|
|
def available_grouping_project_types
|
|
ProjectType.available_grouping_project_types
|
|
end
|
|
|
|
protected
|
|
|
|
def remove_empty_options_values
|
|
unless self[:options].nil?
|
|
self[:options].reject! do |_key, value|
|
|
value.instance_of?(Array) && value.length == 1 && value.first.empty?
|
|
end
|
|
end
|
|
end
|
|
|
|
def split_joined_options_values
|
|
unless self[:options].nil?
|
|
self[:options].each_pair do |key, value|
|
|
if value.instance_of?(Array) && value.length == 1
|
|
self[:options][key] = value[0].split(',')
|
|
end
|
|
end
|
|
|
|
unless self[:options][:custom_fields].nil?
|
|
self[:options][:custom_fields].each_pair do |key, value|
|
|
if value.instance_of?(Array) && value.length == 1
|
|
self[:options][:custom_fields][key] = value[0].split(',')
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def array_of_ids_or_empty_array(options_field)
|
|
array_or_empty(options_field) { |ary| ary.delete_if(&:empty?).map(&:to_i) }
|
|
end
|
|
|
|
def array_or_empty(options_field)
|
|
if options[options_field].present?
|
|
if block_given?
|
|
yield options[options_field]
|
|
else
|
|
return options[options_field]
|
|
end
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
def resolve_with_none_element(options_field, &block)
|
|
collection = []
|
|
collection += [Empty.new] if (ary = array_of_comma_separated(options_field)).delete(-1)
|
|
begin
|
|
collection += block.call(ary)
|
|
rescue
|
|
|
|
end
|
|
collection
|
|
end
|
|
|
|
def array_of_comma_separated(options_field)
|
|
array_or_empty(options_field) do |ary|
|
|
ary.map(&:to_i).reject do |value|
|
|
value < -1 || value == 0
|
|
end
|
|
end
|
|
end
|
|
|
|
# TODO: this should go somewhere else, once it is needed at multiple places
|
|
def with_escape_html_entities_in_json
|
|
oldvalue = ActiveSupport.escape_html_entities_in_json
|
|
ActiveSupport.escape_html_entities_in_json = true
|
|
|
|
yield
|
|
ensure
|
|
ActiveSupport.escape_html_entities_in_json = oldvalue
|
|
end
|
|
|
|
def custom_field_column_ids
|
|
custom_field_columns.map { |cf| cf[:id] }
|
|
end
|
|
end
|
|
|