Actualizar plugin Additionals a 3.0.0
This commit is contained in:
parent
3d976f1b3b
commit
a26f5567af
399 changed files with 70374 additions and 4093 deletions
64
plugins/additionals/app/models/additionals_chart.rb
Normal file
64
plugins/additionals/app/models/additionals_chart.rb
Normal file
|
@ -0,0 +1,64 @@
|
|||
class AdditionalsChart < ActiveRecord::Base
|
||||
include Redmine::I18n
|
||||
|
||||
CHART_DEFAULT_HEIGHT = 350
|
||||
CHART_DEFAULT_WIDTH = 400
|
||||
|
||||
class << self
|
||||
def color_schema
|
||||
Redmine::Plugin.installed?('redmine_reporting') ? RedmineReporting.setting(:chart_color_schema) : 'tableau.Classic20'
|
||||
end
|
||||
|
||||
def data
|
||||
raise 'overwrite it!'
|
||||
end
|
||||
|
||||
# build return value
|
||||
def build_chart_data(datasets, options = {})
|
||||
cached_labels = labels
|
||||
data = { datasets: datasets.to_json,
|
||||
labels: cached_labels.keys,
|
||||
label_ids: cached_labels.values }
|
||||
|
||||
required_labels = options.key?(:required_labels) ? options.delete(:required_labels) : 2
|
||||
|
||||
data[:valid] = cached_labels.any? && cached_labels.count >= required_labels unless options.key?(:valid)
|
||||
data[:width] = self::CHART_DEFAULT_WIDTH unless options.key?(:width)
|
||||
data[:height] = self::CHART_DEFAULT_HEIGHT unless options.key?(:height)
|
||||
data[:value_link_method] = '_project_issues_path' unless options.key?(:value_link_method)
|
||||
data[:color_schema] = color_schema
|
||||
|
||||
data.merge(options)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_values_without_gaps(data, gap_value = 0)
|
||||
values = []
|
||||
labels.each do |label, _label_id|
|
||||
values << if data.key?(label)
|
||||
data[label]
|
||||
else
|
||||
gap_value
|
||||
end
|
||||
end
|
||||
|
||||
values
|
||||
end
|
||||
|
||||
def init_labels
|
||||
@labels = {}
|
||||
end
|
||||
|
||||
def labels
|
||||
# NOTE: do not sort it, because color changes if user switch language
|
||||
@labels.to_h
|
||||
end
|
||||
|
||||
def add_label(label, id)
|
||||
return if @labels.key? label
|
||||
|
||||
@labels[label] = id
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,12 +1,12 @@
|
|||
class AdditionalsFontAwesome
|
||||
include Redmine::I18n
|
||||
|
||||
FORMAT_REGEXP = /\Afa[rsb]_[a-zA-Z0-9]+[a-zA-Z0-9\-]*\z/.freeze
|
||||
SEARCH_LIMIT = 50
|
||||
|
||||
class << self
|
||||
def load_icons(type)
|
||||
data = YAML.safe_load(ERB.new(IO.read(Rails.root.join('plugins',
|
||||
'additionals',
|
||||
'config',
|
||||
'fontawesome_icons.yml'))).result) || {}
|
||||
data = YAML.safe_load(ERB.new(IO.read(Rails.root.join('plugins/additionals/config/fontawesome_icons.yml'))).result) || {}
|
||||
icons = {}
|
||||
data.each do |key, values|
|
||||
icons[key] = { unicode: values['unicode'], label: values['label'] } if values['styles'].include?(convert_type2style(type))
|
||||
|
@ -62,12 +62,6 @@ class AdditionalsFontAwesome
|
|||
FONTAWESOME_ICONS[type].collect { |fa_symbol, values| [values[:label], key2value(fa_symbol, type[-1])] }
|
||||
end
|
||||
|
||||
def json_for_select
|
||||
[{ text: l(:label_fontawesome_regular), children: json_values(:far) },
|
||||
{ text: l(:label_fontawesome_solid), children: json_values(:fas) },
|
||||
{ text: l(:label_fontawesome_brands), children: json_values(:fab) }].to_json
|
||||
end
|
||||
|
||||
# show only one value as current selected
|
||||
# (all other options are retrieved by select2
|
||||
def active_option_for_select(selected)
|
||||
|
@ -77,12 +71,6 @@ class AdditionalsFontAwesome
|
|||
[[info[:label], selected]]
|
||||
end
|
||||
|
||||
def options_for_select
|
||||
[[l(:label_fontawesome_regular), select_values(:far)],
|
||||
[l(:label_fontawesome_solid), select_values(:fas)],
|
||||
[l(:label_fontawesome_brands), select_values(:fab)]]
|
||||
end
|
||||
|
||||
def value_info(value, options = {})
|
||||
return {} if value.blank?
|
||||
|
||||
|
@ -104,8 +92,60 @@ class AdditionalsFontAwesome
|
|||
info
|
||||
end
|
||||
|
||||
def search_for_select(search, selected = nil)
|
||||
# could be more then one
|
||||
selected_store = selected.to_s.split(',')
|
||||
icons = search_in_type(:far, search, selected_store)
|
||||
cnt = icons.count
|
||||
return icons if cnt >= SEARCH_LIMIT
|
||||
|
||||
icons += search_in_type(:fas, search, selected_store, cnt)
|
||||
cnt = icons.count
|
||||
return icons if cnt >= SEARCH_LIMIT
|
||||
|
||||
icons + search_in_type(:fab, search, selected_store, cnt)
|
||||
end
|
||||
|
||||
def convert2mermaid(icon)
|
||||
return if icon.blank?
|
||||
|
||||
parts = icon.split('_')
|
||||
return unless parts.count == 2
|
||||
|
||||
"#{parts.first}:fa-#{parts.last}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def search_in_type(type, search, selected_store, cnt = 0)
|
||||
icons = []
|
||||
|
||||
search_length = search.to_s.length
|
||||
first_letter_search = if search_length == 1
|
||||
search[0].downcase
|
||||
elsif search_length.zero? && selected_store.any?
|
||||
selected = selected_store.first
|
||||
fa = selected.split('_')
|
||||
search = fa[1][0] if fa.count > 1
|
||||
search
|
||||
end
|
||||
|
||||
FONTAWESOME_ICONS[type].each do |fa_symbol, values|
|
||||
break if SEARCH_LIMIT == cnt
|
||||
|
||||
id = key2value(fa_symbol, type[-1])
|
||||
next if selected_store.exclude?(id) &&
|
||||
search.present? &&
|
||||
(first_letter_search.present? && !values[:label].downcase.start_with?(first_letter_search) ||
|
||||
first_letter_search.blank? && values[:label] !~ /#{search}/i)
|
||||
|
||||
icons << { id: id, text: values[:label] }
|
||||
cnt += 1
|
||||
end
|
||||
|
||||
icons
|
||||
end
|
||||
|
||||
def load_details(type, name)
|
||||
return {} unless FONTAWESOME_ICONS.key?(type)
|
||||
|
||||
|
|
|
@ -25,8 +25,6 @@ class AdditionalsImport < Import
|
|||
value = case v.custom_field.field_format
|
||||
when 'date'
|
||||
row_date(row, "cf_#{v.custom_field.id}")
|
||||
when 'list'
|
||||
row_value(row, "cf_#{v.custom_field.id}").try(:split, ',')
|
||||
else
|
||||
row_value(row, "cf_#{v.custom_field.id}")
|
||||
end
|
||||
|
@ -34,7 +32,7 @@ class AdditionalsImport < Import
|
|||
|
||||
h[v.custom_field.id.to_s] =
|
||||
if value.is_a?(Array)
|
||||
value.map { |val| v.custom_field.value_from_keyword(val.strip, object) }.compact.flatten
|
||||
value.map { |val| v.custom_field.value_from_keyword(val.strip, object) }.flatten!&.compact
|
||||
else
|
||||
v.custom_field.value_from_keyword(value, object)
|
||||
end
|
||||
|
|
71
plugins/additionals/app/models/additionals_journal.rb
Normal file
71
plugins/additionals/app/models/additionals_journal.rb
Normal file
|
@ -0,0 +1,71 @@
|
|||
class AdditionalsJournal
|
||||
class << self
|
||||
def save_journal_history(journal, prop_key, ids_old, ids)
|
||||
ids_all = (ids_old + ids).uniq
|
||||
|
||||
ids_all.each do |id|
|
||||
next if ids_old.include?(id) && ids.include?(id)
|
||||
|
||||
if ids.include?(id)
|
||||
value = id
|
||||
old_value = nil
|
||||
else
|
||||
old_value = id
|
||||
value = nil
|
||||
end
|
||||
|
||||
journal.details << JournalDetail.new(property: 'attr',
|
||||
prop_key: prop_key,
|
||||
old_value: old_value,
|
||||
value: value)
|
||||
journal.save
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def validate_relation(entries, entry_id)
|
||||
old_entries = entries.select { |entry| entry.id.present? }
|
||||
new_entries = entries.select { |entry| entry.id.blank? }
|
||||
return true if new_entries.blank?
|
||||
|
||||
new_entries.map! { |entry| entry.send(entry_id) }
|
||||
return false if new_entries.count != new_entries.uniq.count
|
||||
|
||||
old_entries.map! { |entry| entry.send(entry_id) }
|
||||
return false unless (old_entries & new_entries).count.zero?
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
# Preloads visible last notes for a collection of entity
|
||||
# this is a copy of Issue.load_visible_last_notes, but usable for all entities
|
||||
# @see https://www.redmine.org/projects/redmine/repository/entry/trunk/app/models/issue.rb#L1214
|
||||
def load_visible_last_notes(entries, entity, user = User.current)
|
||||
return unless entries.any?
|
||||
|
||||
ids = entries.map(&:id)
|
||||
|
||||
journal_class = (entity == Issue ? Journal : "#{entity}Journal").constantize
|
||||
journal_ids = journal_class.joins(entity.name.underscore.to_sym => :project)
|
||||
.where(journalized_type: entity.to_s, journalized_id: ids)
|
||||
.where(journal_class.visible_notes_condition(user, skip_pre_condition: true))
|
||||
.where.not(notes: '')
|
||||
.group(:journalized_id)
|
||||
.maximum(:id)
|
||||
.values
|
||||
|
||||
journals = Journal.where(id: journal_ids).to_a
|
||||
|
||||
entries.each do |entry|
|
||||
journal = journals.detect { |j| j.journalized_id == entry.id }
|
||||
entry.instance_variable_set('@last_notes', journal.try(:notes) || '')
|
||||
end
|
||||
end
|
||||
|
||||
def set_relation_detail(entity, detail, value_key)
|
||||
value = detail.send value_key
|
||||
detail[value_key] = (entity.find_by(id: value) || value) if value.present?
|
||||
end
|
||||
end
|
||||
end
|
|
@ -62,7 +62,7 @@ class AdditionalsMacro
|
|||
permission: :view_contacts },
|
||||
{ list: %i[db db_query db_tag db_tag_count],
|
||||
permission: :view_db_entries },
|
||||
{ list: %i[child_pages calendar last_updated_at last_updated_by lastupdated_at lastupdated_by
|
||||
{ list: %i[child_pages last_updated_at last_updated_by lastupdated_at lastupdated_by
|
||||
new_page recently_updated recent comments comment_form tags taggedpages tagcloud
|
||||
show_count count vote show_vote terms_accept terms_reject],
|
||||
permission: :view_wiki_pages,
|
||||
|
|
|
@ -1,150 +1,215 @@
|
|||
module AdditionalsQuery
|
||||
def self.included(base)
|
||||
base.send :include, InstanceMethods
|
||||
def column_with_prefix?(prefix)
|
||||
columns.detect { |c| c.name.to_s.start_with?("#{prefix}.") }.present?
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
def initialize_ids_filter(options = {})
|
||||
if options[:label]
|
||||
add_available_filter 'ids', type: :integer, label: options[:label]
|
||||
def available_column_names(options = {})
|
||||
names = available_columns.dup
|
||||
names.flatten!
|
||||
names.select! { |col| col.sortable.present? } if options[:only_sortable]
|
||||
names.map(&:name)
|
||||
end
|
||||
|
||||
def sql_for_enabled_module(table_field, module_names)
|
||||
module_names = Array(module_names)
|
||||
|
||||
sql = []
|
||||
module_names.each do |module_name|
|
||||
sql << "EXISTS(SELECT 1 FROM #{EnabledModule.table_name} WHERE #{EnabledModule.table_name}.project_id=#{table_field}" \
|
||||
" AND #{EnabledModule.table_name}.name='#{module_name}')"
|
||||
end
|
||||
|
||||
sql.join(' AND ')
|
||||
end
|
||||
|
||||
def initialize_ids_filter(options = {})
|
||||
if options[:label]
|
||||
add_available_filter 'ids', type: :integer, label: options[:label]
|
||||
else
|
||||
add_available_filter 'ids', type: :integer, name: '#'
|
||||
end
|
||||
end
|
||||
|
||||
def sql_for_ids_field(_field, operator, value)
|
||||
if operator == '='
|
||||
# accepts a comma separated list of ids
|
||||
ids = value.first.to_s.scan(/\d+/).map(&:to_i)
|
||||
if ids.present?
|
||||
"#{queried_table_name}.id IN (#{ids.join ','})"
|
||||
else
|
||||
add_available_filter 'ids', type: :integer, name: '#'
|
||||
'1=0'
|
||||
end
|
||||
else
|
||||
sql_for_field 'id', operator, value, queried_table_name, 'id'
|
||||
end
|
||||
end
|
||||
|
||||
def sql_for_ids_field(_field, operator, value)
|
||||
if operator == '='
|
||||
# accepts a comma separated list of ids
|
||||
ids = value.first.to_s.scan(/\d+/).map(&:to_i)
|
||||
if ids.present?
|
||||
"#{queried_table_name}.id IN (#{ids.join(',')})"
|
||||
else
|
||||
'1=0'
|
||||
end
|
||||
else
|
||||
sql_for_field('id', operator, value, queried_table_name, 'id')
|
||||
end
|
||||
end
|
||||
def sql_for_project_status_field(field, operator, value)
|
||||
sql_for_field field, operator, value, Project.table_name, 'status'
|
||||
end
|
||||
|
||||
def initialize_project_filter(options = {})
|
||||
if project.nil?
|
||||
add_available_filter('project_id', order: options[:position],
|
||||
type: :list,
|
||||
values: -> { project_values })
|
||||
end
|
||||
return if project.nil? || project.leaf? || subproject_values.empty?
|
||||
def initialize_project_status_filter
|
||||
return if project&.leaf?
|
||||
|
||||
add_available_filter('subproject_id', order: options[:position],
|
||||
type: :list_subprojects,
|
||||
values: -> { subproject_values })
|
||||
end
|
||||
add_available_filter('project.status',
|
||||
type: :list,
|
||||
name: l(:label_attribute_of_project, name: l(:field_status)),
|
||||
values: -> { project_statuses_values })
|
||||
end
|
||||
|
||||
def initialize_created_filter(options = {})
|
||||
add_available_filter 'created_on', order: options[:position],
|
||||
type: :date_past,
|
||||
label: options[:label].presence
|
||||
end
|
||||
|
||||
def initialize_updated_filter(options = {})
|
||||
add_available_filter 'updated_on', order: options[:position],
|
||||
type: :date_past,
|
||||
label: options[:label].presence
|
||||
end
|
||||
|
||||
def initialize_tags_filter(options = {})
|
||||
values = if project
|
||||
queried_class.available_tags(project: project.id)
|
||||
else
|
||||
queried_class.available_tags
|
||||
end
|
||||
return if values.blank?
|
||||
|
||||
add_available_filter 'tags', order: options[:position],
|
||||
type: :list,
|
||||
values: values.collect { |t| [t.name, t.name] }
|
||||
end
|
||||
|
||||
def initialize_author_filter(options = {})
|
||||
return if author_values.empty?
|
||||
|
||||
add_available_filter('author_id', order: options[:position],
|
||||
type: :list_optional,
|
||||
values: options[:no_lambda].nil? ? author_values : -> { author_values })
|
||||
end
|
||||
|
||||
def initialize_assignee_filter(options = {})
|
||||
return if author_values.empty?
|
||||
|
||||
add_available_filter('assigned_to_id', order: options[:position],
|
||||
type: :list_optional,
|
||||
values: options[:no_lambda] ? author_values : -> { author_values })
|
||||
end
|
||||
|
||||
def initialize_watcher_filter(options = {})
|
||||
return if watcher_values.empty? || !User.current.logged?
|
||||
|
||||
add_available_filter('watcher_id', order: options[:position],
|
||||
def initialize_project_filter(options = {})
|
||||
if project.nil? || options[:always]
|
||||
add_available_filter('project_id', order: options[:position],
|
||||
type: :list,
|
||||
values: options[:no_lambda] ? watcher_values : -> { watcher_values })
|
||||
values: -> { project_values })
|
||||
end
|
||||
return if project.nil? || project.leaf? || subproject_values.empty?
|
||||
|
||||
def watcher_values
|
||||
watcher_values = [["<< #{l(:label_me)} >>", 'me']]
|
||||
watcher_values += users.collect { |s| [s.name, s.id.to_s] } if User.current.allowed_to?(:manage_public_queries, project, global: true)
|
||||
watcher_values
|
||||
add_available_filter('subproject_id', order: options[:position],
|
||||
type: :list_subprojects,
|
||||
values: -> { subproject_values })
|
||||
end
|
||||
|
||||
def initialize_created_filter(options = {})
|
||||
add_available_filter 'created_on', order: options[:position],
|
||||
type: :date_past,
|
||||
label: options[:label].presence
|
||||
end
|
||||
|
||||
def initialize_updated_filter(options = {})
|
||||
add_available_filter 'updated_on', order: options[:position],
|
||||
type: :date_past,
|
||||
label: options[:label].presence
|
||||
end
|
||||
|
||||
def initialize_tags_filter(options = {})
|
||||
values = if project
|
||||
queried_class.available_tags(project: project.id)
|
||||
else
|
||||
queried_class.available_tags
|
||||
end
|
||||
return if values.blank?
|
||||
|
||||
add_available_filter 'tags', order: options[:position],
|
||||
type: :list,
|
||||
values: values.collect { |t| [t.name, t.name] }
|
||||
end
|
||||
|
||||
def initialize_approved_filter
|
||||
add_available_filter 'approved',
|
||||
type: :list,
|
||||
values: [[l(:label_hrm_approved), '1'],
|
||||
[l(:label_hrm_not_approved), '0'],
|
||||
[l(:label_hrm_to_approval), '2'],
|
||||
[l(:label_hrm_without_approval), '3']],
|
||||
label: :field_approved
|
||||
end
|
||||
|
||||
def initialize_author_filter(options = {})
|
||||
return if author_values.empty?
|
||||
|
||||
add_available_filter('author_id', order: options[:position],
|
||||
type: :list_optional,
|
||||
values: options[:no_lambda].nil? ? author_values : -> { author_values })
|
||||
end
|
||||
|
||||
def initialize_assignee_filter(options = {})
|
||||
return if author_values.empty?
|
||||
|
||||
add_available_filter('assigned_to_id', order: options[:position],
|
||||
type: :list_optional,
|
||||
values: options[:no_lambda] ? assigned_to_all_values : -> { assigned_to_all_values })
|
||||
end
|
||||
|
||||
def initialize_watcher_filter(options = {})
|
||||
return if watcher_values.empty? || !User.current.logged?
|
||||
|
||||
add_available_filter('watcher_id', order: options[:position],
|
||||
type: :list,
|
||||
values: options[:no_lambda] ? watcher_values : -> { watcher_values })
|
||||
end
|
||||
|
||||
# issue independend values. Use assigned_to_values from Redmine, if you want it only for issues
|
||||
def assigned_to_all_values
|
||||
assigned_to_values = []
|
||||
assigned_to_values << ["<< #{l :label_me} >>", 'me'] if User.current.logged?
|
||||
assigned_to_values += principals.sort_by(&:status).collect { |s| [s.name, s.id.to_s, l("status_#{User::LABEL_BY_STATUS[s.status]}")] }
|
||||
|
||||
assigned_to_values
|
||||
end
|
||||
|
||||
def watcher_values
|
||||
watcher_values = [["<< #{l :label_me} >>", 'me']]
|
||||
watcher_values += users.collect { |s| [s.name, s.id.to_s] } if User.current.allowed_to?(:manage_public_queries, project, global: true)
|
||||
watcher_values
|
||||
end
|
||||
|
||||
def sql_for_watcher_id_field(field, operator, value)
|
||||
watchable_type = queried_class == User ? 'Principal' : queried_class.to_s
|
||||
|
||||
db_table = Watcher.table_name
|
||||
"#{queried_table_name}.id #{operator == '=' ? 'IN' : 'NOT IN'}" \
|
||||
" (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='#{watchable_type}' AND" \
|
||||
" #{sql_for_field field, '=', value, db_table, 'user_id'})"
|
||||
end
|
||||
|
||||
def sql_for_tags_field(field, _operator, value)
|
||||
AdditionalsTag.sql_for_tags_field(queried_class, operator_for(field), value)
|
||||
end
|
||||
|
||||
def sql_for_is_private_field(_field, operator, value)
|
||||
if bool_operator(operator, value)
|
||||
return '' if value.count > 1
|
||||
|
||||
"#{queried_table_name}.is_private = #{self.class.connection.quoted_true}"
|
||||
else
|
||||
return '1=0' if value.count > 1
|
||||
|
||||
"#{queried_table_name}.is_private = #{self.class.connection.quoted_false}"
|
||||
end
|
||||
end
|
||||
|
||||
def sql_for_watcher_id_field(field, operator, value)
|
||||
watchable_type = queried_class == User ? 'Principal' : queried_class.to_s
|
||||
# use for list fields with to values 1 (true) and 0 (false)
|
||||
def bool_operator(operator, values)
|
||||
operator == '=' && values.first == '1' || operator != '=' && values.first != '1'
|
||||
end
|
||||
|
||||
db_table = Watcher.table_name
|
||||
"#{queried_table_name}.id #{operator == '=' ? 'IN' : 'NOT IN'}
|
||||
(SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='#{watchable_type}' AND " +
|
||||
sql_for_field(field, '=', value, db_table, 'user_id') + ')'
|
||||
end
|
||||
# use for list
|
||||
def bool_values
|
||||
[[l(:general_text_yes), '1'], [l(:general_text_no), '0']]
|
||||
end
|
||||
|
||||
def sql_for_tags_field(field, _operator, value)
|
||||
AdditionalsTag.sql_for_tags_field(queried_class, operator_for(field), value)
|
||||
end
|
||||
def query_count
|
||||
objects_scope.count
|
||||
rescue ::ActiveRecord::StatementInvalid => e
|
||||
raise queried_class::StatementInvalid, e.message if defined? queried_class::StatementInvalid
|
||||
|
||||
def sql_for_is_private_field(_field, operator, value)
|
||||
if bool_operator(operator, value)
|
||||
return '1=1' if value.count > 1
|
||||
raise ::Query::StatementInvalid, e.message
|
||||
end
|
||||
|
||||
"#{queried_table_name}.is_private = #{self.class.connection.quoted_true}"
|
||||
else
|
||||
return '1=0' if value.count > 1
|
||||
def results_scope(options = {})
|
||||
order_option = [group_by_sort_order, (options[:order] || sort_clause)].flatten!.to_a.reject(&:blank?)
|
||||
|
||||
"#{queried_table_name}.is_private = #{self.class.connection.quoted_false}"
|
||||
objects_scope(options)
|
||||
.order(order_option)
|
||||
.joins(joins_for_order_statement(order_option.join(',')))
|
||||
.limit(options[:limit])
|
||||
.offset(options[:offset])
|
||||
rescue ::ActiveRecord::StatementInvalid => e
|
||||
raise queried_class::StatementInvalid, e.message if defined? queried_class::StatementInvalid
|
||||
|
||||
raise ::Query::StatementInvalid, e.message
|
||||
end
|
||||
|
||||
def grouped_name_for(group_name, replace_fields = {})
|
||||
return unless group_name
|
||||
|
||||
if grouped? && group_by_column.present?
|
||||
replace_fields.each do |field, new_name|
|
||||
return new_name.presence || group_name if group_by_column.name == field
|
||||
end
|
||||
end
|
||||
|
||||
# use for list fields with to values 1 (true) and 0 (false)
|
||||
def bool_operator(operator, values)
|
||||
operator == '=' && values.first == '1' || operator != '=' && values.first != '1'
|
||||
end
|
||||
|
||||
# use for list
|
||||
def bool_values
|
||||
[[l(:general_text_yes), '1'], [l(:general_text_no), '0']]
|
||||
end
|
||||
|
||||
def query_count
|
||||
objects_scope.count
|
||||
rescue ::ActiveRecord::StatementInvalid => e
|
||||
raise StatementInvalid, e.message
|
||||
end
|
||||
|
||||
def results_scope(options = {})
|
||||
order_option = [group_by_sort_order, (options[:order] || sort_clause)].flatten.reject(&:blank?)
|
||||
|
||||
objects_scope(options)
|
||||
.order(order_option)
|
||||
.joins(joins_for_order_statement(order_option.join(',')))
|
||||
.limit(options[:limit])
|
||||
.offset(options[:offset])
|
||||
rescue ::ActiveRecord::StatementInvalid => e
|
||||
raise StatementInvalid, e.message
|
||||
end
|
||||
group_name
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,68 +3,74 @@ class AdditionalsTag
|
|||
TAGGING_TABLE_NAME = RedmineCrm::Tagging.table_name if defined? RedmineCrm
|
||||
PROJECT_TABLE_NAME = Project.table_name
|
||||
|
||||
def self.get_available_tags(klass, options = {})
|
||||
scope = RedmineCrm::Tag.where({})
|
||||
scope = scope.where("#{PROJECT_TABLE_NAME}.id = ?", options[:project]) if options[:project]
|
||||
if options[:permission]
|
||||
scope = scope.where(tag_access(options[:permission]))
|
||||
elsif options[:visible_condition]
|
||||
scope = scope.where(klass.visible_condition(User.current))
|
||||
end
|
||||
scope = scope.where("LOWER(#{TAG_TABLE_NAME}.name) LIKE ?", "%#{options[:name_like].downcase}%") if options[:name_like]
|
||||
scope = scope.where("#{TAG_TABLE_NAME}.name=?", options[:name]) if options[:name]
|
||||
scope = scope.where("#{TAGGING_TABLE_NAME}.taggable_id!=?", options[:exclude_id]) if options[:exclude_id]
|
||||
scope = scope.where(options[:where_field] => options[:where_value]) if options[:where_field].present? && options[:where_value]
|
||||
|
||||
scope = scope.select("#{TAG_TABLE_NAME}.*, COUNT(DISTINCT #{TAGGING_TABLE_NAME}.taggable_id) AS count")
|
||||
scope = scope.joins(tag_joins(klass, options))
|
||||
scope = scope.group("#{TAG_TABLE_NAME}.id, #{TAG_TABLE_NAME}.name").having('COUNT(*) > 0')
|
||||
scope = scope.order("#{TAG_TABLE_NAME}.name")
|
||||
scope
|
||||
end
|
||||
|
||||
def self.tag_joins(klass, options = {})
|
||||
table_name = klass.table_name
|
||||
|
||||
joins = ["JOIN #{TAGGING_TABLE_NAME} ON #{TAGGING_TABLE_NAME}.tag_id = #{TAG_TABLE_NAME}.id"]
|
||||
joins << "JOIN #{table_name} " \
|
||||
"ON #{table_name}.id = #{TAGGING_TABLE_NAME}.taggable_id AND #{TAGGING_TABLE_NAME}.taggable_type = '#{klass}'"
|
||||
|
||||
if options[:project_join]
|
||||
joins << options[:project_join]
|
||||
elsif options[:project] || !options[:without_projects]
|
||||
joins << "JOIN #{PROJECT_TABLE_NAME} ON #{table_name}.project_id = #{PROJECT_TABLE_NAME}.id"
|
||||
class << self
|
||||
def all_type_tags(klass, options = {})
|
||||
RedmineCrm::Tag.where({})
|
||||
.joins(tag_joins(klass, options))
|
||||
.distinct
|
||||
.order("#{TAG_TABLE_NAME}.name")
|
||||
end
|
||||
|
||||
joins
|
||||
end
|
||||
def get_available_tags(klass, options = {})
|
||||
scope = RedmineCrm::Tag.where({})
|
||||
scope = scope.where("#{PROJECT_TABLE_NAME}.id = ?", options[:project]) if options[:project]
|
||||
if options[:permission]
|
||||
scope = scope.where(tag_access(options[:permission]))
|
||||
elsif options[:visible_condition]
|
||||
scope = scope.where(klass.visible_condition(User.current))
|
||||
end
|
||||
scope = scope.where("LOWER(#{TAG_TABLE_NAME}.name) LIKE ?", "%#{options[:name_like].downcase}%") if options[:name_like]
|
||||
scope = scope.where("#{TAG_TABLE_NAME}.name=?", options[:name]) if options[:name]
|
||||
scope = scope.where("#{TAGGING_TABLE_NAME}.taggable_id!=?", options[:exclude_id]) if options[:exclude_id]
|
||||
scope = scope.where(options[:where_field] => options[:where_value]) if options[:where_field].present? && options[:where_value]
|
||||
|
||||
def self.tag_access(permission)
|
||||
projects_allowed = if permission.nil?
|
||||
Project.visible.pluck(:id)
|
||||
else
|
||||
Project.where(Project.allowed_to_condition(User.current, permission)).pluck(:id)
|
||||
end
|
||||
|
||||
if projects_allowed.present?
|
||||
"#{PROJECT_TABLE_NAME}.id IN (#{projects_allowed.join(',')})" unless projects_allowed.empty?
|
||||
else
|
||||
'1=0'
|
||||
scope.select("#{TAG_TABLE_NAME}.*, COUNT(DISTINCT #{TAGGING_TABLE_NAME}.taggable_id) AS count")
|
||||
.joins(tag_joins(klass, options))
|
||||
.group("#{TAG_TABLE_NAME}.id, #{TAG_TABLE_NAME}.name").having('COUNT(*) > 0')
|
||||
.order("#{TAG_TABLE_NAME}.name")
|
||||
end
|
||||
end
|
||||
|
||||
def self.remove_unused_tags
|
||||
unused = RedmineCrm::Tag.find_by_sql(<<-SQL)
|
||||
SELECT * FROM tags WHERE id NOT IN (
|
||||
SELECT DISTINCT tag_id FROM taggings
|
||||
)
|
||||
SQL
|
||||
unused.each(&:destroy)
|
||||
end
|
||||
def remove_unused_tags
|
||||
RedmineCrm::Tag.where.not(id: RedmineCrm::Tagging.select(:tag_id).distinct)
|
||||
.each(&:destroy)
|
||||
end
|
||||
|
||||
def self.sql_for_tags_field(klass, operator, value)
|
||||
compare = operator.eql?('=') ? 'IN' : 'NOT IN'
|
||||
ids_list = klass.tagged_with(value).collect(&:id).push(0).join(',')
|
||||
"( #{klass.table_name}.id #{compare} (#{ids_list}) ) "
|
||||
def sql_for_tags_field(klass, operator, value)
|
||||
compare = operator.eql?('=') ? 'IN' : 'NOT IN'
|
||||
ids_list = klass.tagged_with(value).collect(&:id).push(0).join(',')
|
||||
"( #{klass.table_name}.id #{compare} (#{ids_list}) ) "
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tag_access(permission)
|
||||
projects_allowed = if permission.nil?
|
||||
Project.visible.ids
|
||||
else
|
||||
Project.where(Project.allowed_to_condition(User.current, permission)).ids
|
||||
end
|
||||
|
||||
if projects_allowed.present?
|
||||
"#{PROJECT_TABLE_NAME}.id IN (#{projects_allowed.join ','})" unless projects_allowed.empty?
|
||||
else
|
||||
'1=0'
|
||||
end
|
||||
end
|
||||
|
||||
def tag_joins(klass, options = {})
|
||||
table_name = klass.table_name
|
||||
|
||||
joins = ["JOIN #{TAGGING_TABLE_NAME} ON #{TAGGING_TABLE_NAME}.tag_id = #{TAG_TABLE_NAME}.id"]
|
||||
joins << "JOIN #{table_name} " \
|
||||
"ON #{table_name}.id = #{TAGGING_TABLE_NAME}.taggable_id AND #{TAGGING_TABLE_NAME}.taggable_type = '#{klass}'"
|
||||
|
||||
if options[:project_join]
|
||||
joins << options[:project_join]
|
||||
elsif options[:project] || !options[:without_projects]
|
||||
joins << "JOIN #{PROJECT_TABLE_NAME} ON #{table_name}.project_id = #{PROJECT_TABLE_NAME}.id"
|
||||
end
|
||||
|
||||
joins
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
423
plugins/additionals/app/models/dashboard.rb
Normal file
423
plugins/additionals/app/models/dashboard.rb
Normal file
|
@ -0,0 +1,423 @@
|
|||
class Dashboard < ActiveRecord::Base
|
||||
include Redmine::I18n
|
||||
include Redmine::SafeAttributes
|
||||
include Additionals::EntityMethods
|
||||
|
||||
class SystemDefaultChangeException < StandardError; end
|
||||
class ProjectSystemDefaultChangeException < StandardError; end
|
||||
|
||||
belongs_to :project
|
||||
belongs_to :author, class_name: 'User'
|
||||
|
||||
# current active project (belongs_to :project can be nil, because this is system default)
|
||||
attr_accessor :content_project
|
||||
|
||||
serialize :options
|
||||
|
||||
has_many :dashboard_roles, dependent: :destroy
|
||||
has_many :roles, through: :dashboard_roles
|
||||
|
||||
VISIBILITY_PRIVATE = 0
|
||||
VISIBILITY_ROLES = 1
|
||||
VISIBILITY_PUBLIC = 2
|
||||
|
||||
scope :by_project, (->(project_id) { where(project_id: project_id) if project_id.present? })
|
||||
scope :sorted, (-> { order("#{Dashboard.table_name}.name") })
|
||||
scope :welcome_only, (-> { where(dashboard_type: DashboardContentWelcome::TYPE_NAME) })
|
||||
scope :project_only, (-> { where(dashboard_type: DashboardContentProject::TYPE_NAME) })
|
||||
|
||||
safe_attributes 'name', 'description', 'enable_sidebar',
|
||||
'always_expose', 'project_id', 'author_id',
|
||||
if: (lambda do |dashboard, user|
|
||||
dashboard.new_record? ||
|
||||
user.allowed_to?(:save_dashboards, dashboard.project, global: true)
|
||||
end)
|
||||
|
||||
safe_attributes 'dashboard_type',
|
||||
if: (lambda do |dashboard, _user|
|
||||
dashboard.new_record?
|
||||
end)
|
||||
|
||||
safe_attributes 'visibility', 'role_ids',
|
||||
if: (lambda do |dashboard, user|
|
||||
user.allowed_to?(:share_dashboards, dashboard.project, global: true) ||
|
||||
user.allowed_to?(:set_system_dashboards, dashboard.project, global: true)
|
||||
end)
|
||||
|
||||
safe_attributes 'system_default',
|
||||
if: (lambda do |dashboard, user|
|
||||
user.allowed_to?(:set_system_dashboards, dashboard.project, global: true)
|
||||
end)
|
||||
|
||||
before_save :dashboard_type_check, :visibility_check, :set_options_hash, :clear_unused_block_settings
|
||||
|
||||
before_destroy :check_destroy_system_default
|
||||
after_save :update_system_defaults
|
||||
after_save :remove_unused_role_relations
|
||||
|
||||
validates :name, :dashboard_type, :author, :visibility, presence: true
|
||||
validates :visibility, inclusion: { in: [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
|
||||
validate :validate_roles
|
||||
validate :validate_visibility
|
||||
validate :validate_name
|
||||
validate :validate_system_default
|
||||
validate :validate_project_system_default
|
||||
|
||||
class << self
|
||||
def system_default(dashboard_type)
|
||||
select(:id).find_by(dashboard_type: dashboard_type, system_default: true)
|
||||
.try(:id)
|
||||
end
|
||||
|
||||
def default(dashboard_type, project = nil, user = User.current)
|
||||
recently_id = User.current.pref.recently_used_dashboard dashboard_type, project
|
||||
|
||||
scope = where(dashboard_type: dashboard_type)
|
||||
scope = scope.where(project_id: project.id).or(scope.where(project_id: nil)) if project.present?
|
||||
|
||||
dashboard = scope.visible.find_by(id: recently_id) if recently_id.present?
|
||||
|
||||
if dashboard.blank?
|
||||
scope = scope.where(system_default: true).or(scope.where(author_id: user.id))
|
||||
dashboard = scope.order(system_default: :desc, project_id: :desc, id: :asc).first
|
||||
|
||||
if recently_id.present?
|
||||
Rails.logger.debug 'default cleanup required'
|
||||
# Remove invalid recently_id
|
||||
if project.present?
|
||||
User.current.pref.recently_used_dashboards[dashboard_type].delete(project.id)
|
||||
else
|
||||
User.current.pref.recently_used_dashboards[dashboard_type] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
dashboard
|
||||
end
|
||||
|
||||
def fields_for_order_statement(table = nil)
|
||||
table ||= table_name
|
||||
["#{table}.name"]
|
||||
end
|
||||
|
||||
def visible(user = User.current, options = {})
|
||||
scope = left_outer_joins :project
|
||||
scope = scope.where(projects: { id: nil }).or(scope.where(Project.allowed_to_condition(user, :view_project, options)))
|
||||
|
||||
if user.admin?
|
||||
scope.where.not(visibility: VISIBILITY_PRIVATE).or(scope.where(author_id: user.id))
|
||||
elsif user.memberships.any?
|
||||
scope.where("#{table_name}.visibility = ?" \
|
||||
" OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" \
|
||||
"SELECT DISTINCT d.id FROM #{table_name} d" \
|
||||
" INNER JOIN #{table_name_prefix}dashboard_roles#{table_name_suffix} dr ON dr.dashboard_id = d.id" \
|
||||
" INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = dr.role_id" \
|
||||
" INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" \
|
||||
" INNER JOIN #{Project.table_name} p ON p.id = m.project_id AND p.status <> ?" \
|
||||
' WHERE d.project_id IS NULL OR d.project_id = m.project_id))' \
|
||||
" OR #{table_name}.author_id = ?",
|
||||
VISIBILITY_PUBLIC,
|
||||
VISIBILITY_ROLES,
|
||||
user.id,
|
||||
Project::STATUS_ARCHIVED,
|
||||
user.id)
|
||||
elsif user.logged?
|
||||
scope.where(visibility: VISIBILITY_PUBLIC).or(scope.where(author_id: user.id))
|
||||
else
|
||||
scope.where visibility: VISIBILITY_PUBLIC
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(attributes = nil, *args)
|
||||
super
|
||||
set_options_hash
|
||||
end
|
||||
|
||||
def set_options_hash
|
||||
self.options ||= {}
|
||||
end
|
||||
|
||||
def [](attr_name)
|
||||
if has_attribute? attr_name
|
||||
super
|
||||
else
|
||||
options ? options[attr_name] : nil
|
||||
end
|
||||
end
|
||||
|
||||
def []=(attr_name, value)
|
||||
if has_attribute? attr_name
|
||||
super
|
||||
else
|
||||
h = (self[:options] || {}).dup
|
||||
h.update(attr_name => value)
|
||||
self[:options] = h
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
# Returns true if the dashboard is visible to +user+ or the current user.
|
||||
def visible?(user = User.current)
|
||||
return true if user.admin?
|
||||
return false unless project.nil? || user.allowed_to?(:view_project, project)
|
||||
return true if user == author
|
||||
|
||||
case visibility
|
||||
when VISIBILITY_PUBLIC
|
||||
true
|
||||
when VISIBILITY_ROLES
|
||||
if project
|
||||
(user.roles_for_project(project) & roles).any?
|
||||
else
|
||||
user.memberships.joins(:member_roles).where(member_roles: { role_id: roles.map(&:id) }).any?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def content
|
||||
@content ||= "DashboardContent#{dashboard_type[0..-10]}".constantize.new(project: content_project.presence || project)
|
||||
end
|
||||
|
||||
def available_groups
|
||||
content.groups
|
||||
end
|
||||
|
||||
def layout
|
||||
self[:layout] ||= content.default_layout.deep_dup
|
||||
end
|
||||
|
||||
def layout=(arg)
|
||||
self[:layout] = arg
|
||||
end
|
||||
|
||||
def layout_settings(block = nil)
|
||||
s = self[:layout_settings] ||= {}
|
||||
if block
|
||||
s[block] ||= {}
|
||||
else
|
||||
s
|
||||
end
|
||||
end
|
||||
|
||||
def layout_settings=(arg)
|
||||
self[:layout_settings] = arg
|
||||
end
|
||||
|
||||
def remove_block(block)
|
||||
block = block.to_s.underscore
|
||||
layout.each_key do |group|
|
||||
layout[group].delete block
|
||||
end
|
||||
layout
|
||||
end
|
||||
|
||||
# Adds block to the user page layout
|
||||
# Returns nil if block is not valid or if it's already
|
||||
# present in the user page layout
|
||||
def add_block(block)
|
||||
block = block.to_s.underscore
|
||||
return unless content.valid_block? block, layout.values.flatten
|
||||
|
||||
remove_block block
|
||||
# add it to the first group
|
||||
# add it to the first group
|
||||
group = available_groups.first
|
||||
layout[group] ||= []
|
||||
layout[group].unshift block
|
||||
end
|
||||
|
||||
# Sets the block order for the given group.
|
||||
# Example:
|
||||
# preferences.order_blocks('left', ['issueswatched', 'news'])
|
||||
def order_blocks(group, blocks)
|
||||
group = group.to_s
|
||||
return if content.groups.exclude?(group) || blocks.blank?
|
||||
|
||||
blocks = blocks.map(&:underscore) & layout.values.flatten
|
||||
blocks.each { |block| remove_block(block) }
|
||||
layout[group] = blocks
|
||||
end
|
||||
|
||||
def update_block_settings(block, settings)
|
||||
block = block.to_s
|
||||
block_settings = layout_settings(block).merge(settings.symbolize_keys)
|
||||
layout_settings[block] = block_settings
|
||||
end
|
||||
|
||||
def private?(user = User.current)
|
||||
author_id == user.id && visibility == VISIBILITY_PRIVATE
|
||||
end
|
||||
|
||||
def public?
|
||||
visibility != VISIBILITY_PRIVATE
|
||||
end
|
||||
|
||||
def editable_by?(usr = User.current, prj = nil)
|
||||
prj ||= project
|
||||
usr && (usr.admin? ||
|
||||
(author == usr && usr.allowed_to?(:save_dashboards, prj, global: true)))
|
||||
end
|
||||
|
||||
def editable?(usr = User.current)
|
||||
@editable ||= editable_by?(usr)
|
||||
end
|
||||
|
||||
def destroyable_by?(usr = User.current)
|
||||
return unless editable_by? usr, project
|
||||
|
||||
return !system_default_was if dashboard_type != DashboardContentProject::TYPE_NAME
|
||||
|
||||
# project dashboards needs special care
|
||||
project.present? || !system_default_was
|
||||
end
|
||||
|
||||
def destroyable?
|
||||
@destroyable ||= destroyable_by?(User.current)
|
||||
end
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
# Returns a string of css classes that apply to the entry
|
||||
def css_classes(user = User.current)
|
||||
s = ['dashboard']
|
||||
s << 'created-by-me' if author_id == user.id
|
||||
s.join(' ')
|
||||
end
|
||||
|
||||
def allowed_target_projects(user = User.current)
|
||||
Project.where Project.allowed_to_condition(user, :save_dashboards)
|
||||
end
|
||||
|
||||
# this is used to get unique cache for blocks
|
||||
def async_params(block, options, settings = {})
|
||||
if block.blank?
|
||||
msg = 'block is missing for dashboard_async'
|
||||
Rails.log.error msg
|
||||
raise msg
|
||||
end
|
||||
|
||||
config = { dashboard_id: id,
|
||||
block: block }
|
||||
|
||||
settings[:user_id] = User.current.id if !options.key?(:skip_user_id) || !options[:skip_user_id]
|
||||
|
||||
if settings.present?
|
||||
settings.each do |key, setting|
|
||||
settings[key] = setting.reject(&:blank?).join(',') if setting.is_a? Array
|
||||
|
||||
next if options[:exposed_params].blank?
|
||||
|
||||
options[:exposed_params].each do |exposed_param|
|
||||
if key == exposed_param
|
||||
config[key] = settings[key]
|
||||
settings.delete key
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
unique_params = settings.flatten
|
||||
unique_params += options[:unique_params].reject(&:blank?) if options[:unique_params].present?
|
||||
|
||||
Rails.logger.debug "debug async_params for #{block}: unique_params=#{unique_params.inspect}"
|
||||
config[:unique_key] = Digest::SHA256.hexdigest(unique_params.join('_'))
|
||||
end
|
||||
|
||||
Rails.logger.debug "debug async_params for #{block}: config=#{config.inspect}"
|
||||
|
||||
config
|
||||
end
|
||||
|
||||
def project_id_can_change?
|
||||
return true if new_record? ||
|
||||
dashboard_type != DashboardContentProject::TYPE_NAME ||
|
||||
!system_default_was ||
|
||||
project_id_was.present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def clear_unused_block_settings
|
||||
blocks = layout.values.flatten
|
||||
layout_settings.keep_if { |block, _settings| blocks.include?(block) }
|
||||
end
|
||||
|
||||
def remove_unused_role_relations
|
||||
return if !saved_change_to_visibility? || visibility == VISIBILITY_ROLES
|
||||
|
||||
roles.clear
|
||||
end
|
||||
|
||||
def validate_roles
|
||||
return if visibility != VISIBILITY_ROLES || roles.present?
|
||||
|
||||
errors.add(:base,
|
||||
[l(:label_role_plural), l('activerecord.errors.messages.blank')].join(' '))
|
||||
end
|
||||
|
||||
def validate_system_default
|
||||
return if new_record? || system_default_was == system_default || system_default?
|
||||
|
||||
raise SystemDefaultChangeException
|
||||
end
|
||||
|
||||
def validate_project_system_default
|
||||
return if project_id_can_change?
|
||||
|
||||
raise ProjectSystemDefaultChangeException if project_id.present?
|
||||
end
|
||||
|
||||
def check_destroy_system_default
|
||||
raise 'It is not allowed to delete dashboard, which is system default' unless destroyable?
|
||||
end
|
||||
|
||||
def dashboard_type_check
|
||||
self.project_id = nil if dashboard_type == DashboardContentWelcome::TYPE_NAME
|
||||
end
|
||||
|
||||
def update_system_defaults
|
||||
return unless system_default? && User.current.allowed_to?(:set_system_dashboards, project, global: true)
|
||||
|
||||
scope = self.class
|
||||
.where(dashboard_type: dashboard_type)
|
||||
.where.not(id: id)
|
||||
|
||||
scope = scope.where(project: project) if dashboard_type == DashboardContentProject::TYPE_NAME
|
||||
|
||||
scope.update_all system_default: false
|
||||
end
|
||||
|
||||
# check if permissions changed and dashboard settings have to be corrected
|
||||
def visibility_check
|
||||
user = User.current
|
||||
|
||||
return if system_default? ||
|
||||
user.allowed_to?(:share_dashboards, project, global: true) ||
|
||||
user.allowed_to?(:set_system_dashboards, project, global: true)
|
||||
|
||||
# change to private
|
||||
self.visibility = VISIBILITY_PRIVATE
|
||||
end
|
||||
|
||||
def validate_visibility
|
||||
errors.add(:visibility, :must_be_for_everyone) if system_default? && visibility != VISIBILITY_PUBLIC
|
||||
end
|
||||
|
||||
def validate_name
|
||||
return if name.blank?
|
||||
|
||||
scope = self.class.visible.where(name: name)
|
||||
if dashboard_type == DashboardContentProject::TYPE_NAME
|
||||
scope = scope.project_only
|
||||
scope = scope.where project_id: project_id
|
||||
scope = scope.or(scope.where(project_id: nil)) if project_id.present?
|
||||
else
|
||||
scope = scope.welcome_only
|
||||
end
|
||||
|
||||
scope = scope.where.not(id: id) unless new_record?
|
||||
errors.add(:name, :name_not_unique) if scope.count.positive?
|
||||
end
|
||||
end
|
111
plugins/additionals/app/models/dashboard_content.rb
Normal file
111
plugins/additionals/app/models/dashboard_content.rb
Normal file
|
@ -0,0 +1,111 @@
|
|||
class DashboardContent
|
||||
include Redmine::I18n
|
||||
|
||||
attr_accessor :user, :project
|
||||
|
||||
MAX_MULTIPLE_OCCURS = 8
|
||||
DEFAULT_MAX_ENTRIES = 10
|
||||
RENDER_ASYNC_CACHE_EXPIRES_IN = 30
|
||||
|
||||
class << self
|
||||
def types
|
||||
descendants.map { |dc| dc::TYPE_NAME }
|
||||
end
|
||||
end
|
||||
|
||||
def with_chartjs?
|
||||
false
|
||||
end
|
||||
|
||||
def initialize(attr = {})
|
||||
self.user = attr[:user].presence || User.current
|
||||
self.project = attr[:project].presence
|
||||
end
|
||||
|
||||
def groups
|
||||
%w[top left right bottom]
|
||||
end
|
||||
|
||||
def block_definitions
|
||||
{
|
||||
'issuequery' => { label: l(:label_query_with_name, l(:label_issue_plural)),
|
||||
permission: :view_issues,
|
||||
query_block: {
|
||||
label: l(:label_issue_plural),
|
||||
list_partial: 'issues/list',
|
||||
class: IssueQuery,
|
||||
link_helper: '_project_issues_path',
|
||||
count_method: 'issue_count',
|
||||
entries_method: 'issues',
|
||||
entities_var: :issues,
|
||||
with_project: true
|
||||
},
|
||||
max_occurs: DashboardContent::MAX_MULTIPLE_OCCURS },
|
||||
'text' => { label: l(:label_text),
|
||||
max_occurs: MAX_MULTIPLE_OCCURS,
|
||||
partial: 'dashboards/blocks/text' },
|
||||
'news' => { label: l(:label_news_latest),
|
||||
permission: :view_news },
|
||||
'documents' => { label: l(:label_document_plural),
|
||||
permission: :view_documents },
|
||||
'my_spent_time' => { label: l(:label_my_spent_time),
|
||||
permission: :log_time },
|
||||
'feed' => { label: l(:label_additionals_feed),
|
||||
max_occurs: DashboardContent::MAX_MULTIPLE_OCCURS,
|
||||
async: { required_settings: %i[url],
|
||||
cache_expires_in: 600,
|
||||
skip_user_id: true,
|
||||
partial: 'dashboards/blocks/feed' } }
|
||||
}
|
||||
end
|
||||
|
||||
# Returns the available blocks
|
||||
def available_blocks
|
||||
return @available_blocks if defined? @available_blocks
|
||||
|
||||
available_blocks = begin block_definitions.reject do |_block_name, block_specs|
|
||||
block_specs.key?(:permission) && !user.allowed_to?(block_specs[:permission], project, global: true) ||
|
||||
block_specs.key?(:admin_only) && block_specs[:admin_only] && !user.admin? ||
|
||||
block_specs.key?(:if) && !block_specs[:if].call(project)
|
||||
end
|
||||
end
|
||||
|
||||
@available_blocks = available_blocks.sort_by { |_k, v| v[:label] }.to_h
|
||||
end
|
||||
|
||||
def block_options(blocks_in_use = [])
|
||||
options = []
|
||||
available_blocks.each do |block, block_options|
|
||||
indexes = blocks_in_use.map do |n|
|
||||
Regexp.last_match(2).to_i if n =~ /\A#{block}(__(\d+))?\z/
|
||||
end
|
||||
indexes.compact!
|
||||
|
||||
occurs = indexes.size
|
||||
block_id = indexes.any? ? "#{block}__#{indexes.max + 1}" : block
|
||||
disabled = (occurs >= (available_blocks[block][:max_occurs] || 1))
|
||||
block_id = nil if disabled
|
||||
|
||||
options << [block_options[:label], block_id]
|
||||
end
|
||||
options
|
||||
end
|
||||
|
||||
def valid_block?(block, blocks_in_use = [])
|
||||
block.present? && block_options(blocks_in_use).map(&:last).include?(block)
|
||||
end
|
||||
|
||||
def find_block(block)
|
||||
block.to_s =~ /\A(.*?)(__\d+)?\z/
|
||||
name = Regexp.last_match(1)
|
||||
available_blocks.key?(name) ? available_blocks[name].merge(name: name) : nil
|
||||
end
|
||||
|
||||
# Returns the default layout for a new dashboard
|
||||
def default_layout
|
||||
{
|
||||
'left' => ['legacy_left'],
|
||||
'right' => ['legacy_right']
|
||||
}
|
||||
end
|
||||
end
|
52
plugins/additionals/app/models/dashboard_content_project.rb
Normal file
52
plugins/additionals/app/models/dashboard_content_project.rb
Normal file
|
@ -0,0 +1,52 @@
|
|||
class DashboardContentProject < DashboardContent
|
||||
TYPE_NAME = 'ProjectDashboard'.freeze
|
||||
|
||||
def block_definitions
|
||||
blocks = super
|
||||
|
||||
# legacy_left or legacy_right should not be moved to DashboardContent,
|
||||
# because DashboardContent is used for areas in other plugins
|
||||
blocks['legacy_left'] = { label: l(:label_dashboard_legacy_left),
|
||||
no_settings: true }
|
||||
|
||||
blocks['legacy_right'] = { label: l(:label_dashboard_legacy_right),
|
||||
no_settings: true }
|
||||
|
||||
blocks['projectinformation'] = { label: l(:label_project_information),
|
||||
no_settings: true,
|
||||
if: (lambda do |project|
|
||||
project.description.present? ||
|
||||
project.homepage.present? ||
|
||||
project.visible_custom_field_values.any? { |o| o.value.present? }
|
||||
end),
|
||||
partial: 'dashboards/blocks/project_information' }
|
||||
|
||||
blocks['projectissues'] = { label: l(:label_issues_summary),
|
||||
no_settings: true,
|
||||
permission: :view_issues,
|
||||
partial: 'dashboards/blocks/project_issues' }
|
||||
|
||||
blocks['projecttimeentries'] = { label: l(:label_time_tracking),
|
||||
no_settings: true,
|
||||
permission: :view_time_entries,
|
||||
partial: 'dashboards/blocks/project_time_entries' }
|
||||
|
||||
blocks['projectmembers'] = { label: l(:label_member_plural),
|
||||
no_settings: true,
|
||||
partial: 'projects/members_box' }
|
||||
|
||||
blocks['projectsubprojects'] = { label: l(:label_subproject_plural),
|
||||
no_settings: true,
|
||||
partial: 'dashboards/blocks/project_subprojects' }
|
||||
|
||||
blocks
|
||||
end
|
||||
|
||||
# Returns the default layout for a new dashboard
|
||||
def default_layout
|
||||
{
|
||||
'left' => %w[projectinformation projectissues projecttimeentries],
|
||||
'right' => %w[projectmembers projectsubprojects]
|
||||
}
|
||||
end
|
||||
end
|
33
plugins/additionals/app/models/dashboard_content_welcome.rb
Normal file
33
plugins/additionals/app/models/dashboard_content_welcome.rb
Normal file
|
@ -0,0 +1,33 @@
|
|||
class DashboardContentWelcome < DashboardContent
|
||||
TYPE_NAME = 'WelcomeDashboard'.freeze
|
||||
|
||||
def block_definitions
|
||||
blocks = super
|
||||
|
||||
# legacy_left or legacy_right should not be moved to DashboardContent,
|
||||
# because DashboardContent is used for areas in other plugins
|
||||
blocks['legacy_left'] = { label: l(:label_dashboard_legacy_left),
|
||||
no_settings: true }
|
||||
|
||||
blocks['legacy_right'] = { label: l(:label_dashboard_legacy_right),
|
||||
no_settings: true }
|
||||
|
||||
blocks['welcome'] = { label: l(:setting_welcome_text),
|
||||
no_settings: true,
|
||||
partial: 'dashboards/blocks/welcome' }
|
||||
|
||||
blocks['activity'] = { label: l(:label_activity),
|
||||
async: { data_method: 'activity_dashboard_data',
|
||||
partial: 'dashboards/blocks/activity' } }
|
||||
|
||||
blocks
|
||||
end
|
||||
|
||||
# Returns the default layout for a new dashboard
|
||||
def default_layout
|
||||
{
|
||||
'left' => %w[welcome legacy_left],
|
||||
'right' => ['legacy_right']
|
||||
}
|
||||
end
|
||||
end
|
6
plugins/additionals/app/models/dashboard_role.rb
Normal file
6
plugins/additionals/app/models/dashboard_role.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
class DashboardRole < ActiveRecord::Base
|
||||
include Redmine::SafeAttributes
|
||||
|
||||
belongs_to :dashboard
|
||||
belongs_to :role
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue