2021-04-18 14:34:55 +03:00
# frozen_string_literal: true
2024-06-12 08:36:06 +03:00
class Dashboard < AdditionalsApplicationRecord
2020-07-27 11:49:23 +03:00
include Redmine :: I18n
include Redmine :: SafeAttributes
include Additionals :: EntityMethods
2020-08-07 21:55:50 +03:00
class SystemDefaultChangeException < StandardError ; end
2020-11-24 17:52:16 +03:00
2020-08-25 07:29:39 +03:00
class ProjectSystemDefaultChangeException < StandardError ; end
2020-08-07 21:55:50 +03:00
2020-07-27 11:49:23 +03:00
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
2020-08-03 16:08:33 +03:00
has_many :dashboard_roles , dependent : :destroy
2020-07-27 11:49:23 +03:00
has_many :roles , through : :dashboard_roles
VISIBILITY_PRIVATE = 0
VISIBILITY_ROLES = 1
VISIBILITY_PUBLIC = 2
2023-12-05 21:24:41 +03:00
scope :by_project , - > ( project_id ) { where project_id : project_id if project_id . present? }
scope :sorted , - > { order :name }
scope :welcome_only , - > { where dashboard_type : DashboardContentWelcome :: TYPE_NAME }
scope :project_only , - > { where dashboard_type : DashboardContentProject :: TYPE_NAME }
2020-07-27 11:49:23 +03:00
2020-08-01 16:26:20 +03:00
safe_attributes 'name' , 'description' , 'enable_sidebar' ,
2023-03-05 21:09:16 +03:00
'locked' , 'always_expose' , 'project_id' , 'author_id' ,
2020-07-27 11:49:23 +03:00
if : ( lambda do | dashboard , user |
dashboard . new_record? ||
2020-08-01 16:26:20 +03:00
user . allowed_to? ( :save_dashboards , dashboard . project , global : true )
2020-07-27 11:49:23 +03:00
end )
safe_attributes 'dashboard_type' ,
if : ( lambda do | dashboard , _user |
dashboard . new_record?
end )
2020-08-07 19:10:18 +03:00
safe_attributes 'visibility' , 'role_ids' ,
2020-07-27 11:49:23 +03:00
if : ( lambda do | dashboard , user |
2020-08-07 19:10:18 +03:00
user . allowed_to? ( :share_dashboards , dashboard . project , global : true ) ||
2020-07-27 11:49:23 +03:00
user . allowed_to? ( :set_system_dashboards , dashboard . project , global : true )
end )
2020-08-01 16:26:20 +03:00
safe_attributes 'system_default' ,
if : ( lambda do | dashboard , user |
2021-04-18 14:34:55 +03:00
user . allowed_to? :set_system_dashboards , dashboard . project , global : true
2020-08-01 16:26:20 +03:00
end )
2021-08-07 11:22:59 +03:00
before_validation :strip_whitespace
2020-08-01 16:26:20 +03:00
before_save :dashboard_type_check , :visibility_check , :set_options_hash , :clear_unused_block_settings
2020-07-27 11:49:23 +03:00
2023-03-05 21:09:16 +03:00
before_destroy :check_locked
2020-07-27 11:49:23 +03:00
before_destroy :check_destroy_system_default
after_save :update_system_defaults
after_save :remove_unused_role_relations
2022-09-20 22:47:34 +03:00
validates :name , presence : true , length : { maximum : 255 }
validates :dashboard_type , :author , :visibility , presence : true
2020-07-27 11:49:23 +03:00
validates :visibility , inclusion : { in : [ VISIBILITY_PUBLIC , VISIBILITY_ROLES , VISIBILITY_PRIVATE ] }
validate :validate_roles
validate :validate_visibility
validate :validate_name
2020-08-07 21:55:50 +03:00
validate :validate_system_default
2020-08-25 07:29:39 +03:00
validate :validate_project_system_default
2020-07-27 11:49:23 +03:00
class << self
def system_default ( dashboard_type )
2024-08-27 10:30:58 +03:00
select ( :id ) . find_by ( dashboard_type : , system_default : true )
2020-07-27 11:49:23 +03:00
. try ( :id )
end
2023-08-01 14:02:23 +03:00
def default ( dashboard_type , project = nil , user = User . current , recently_id = nil )
recently_id || = User . current . pref . recently_used_dashboard dashboard_type , project
2020-07-27 11:49:23 +03:00
2024-08-27 10:30:58 +03:00
scope = where ( dashboard_type : )
2020-07-27 11:49:23 +03:00
scope = scope . where ( project_id : project . id ) . or ( scope . where ( project_id : nil ) ) if project . present?
2021-04-18 14:34:55 +03:00
dashboard = scope . visible . find_by id : recently_id if recently_id . present?
2020-07-27 11:49:23 +03:00
if dashboard . blank?
scope = scope . where ( system_default : true ) . or ( scope . where ( author_id : user . id ) )
2021-12-15 17:09:38 +03:00
scope = scope . order ( system_default : :desc )
. order ( Arel . sql ( " CASE WHEN #{ Dashboard . table_name } .project_id IS NOT NULL THEN 0 ELSE 1 END " ) )
. order ( id : :asc )
dashboard = scope . first
2020-07-27 11:49:23 +03:00
if recently_id . present?
Rails . logger . debug 'default cleanup required'
# Remove invalid recently_id
if project . present?
2021-04-18 14:34:55 +03:00
User . current . pref . recently_used_dashboards [ dashboard_type ] . delete project . id
2020-07-27 11:49:23 +03:00
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
2021-04-18 14:34:55 +03:00
def visible ( user = User . current , ** options )
2020-09-22 11:58:48 +03:00
scope = left_outer_joins :project
2020-07-28 20:09:34 +03:00
scope = scope . where ( projects : { id : nil } ) . or ( scope . where ( Project . allowed_to_condition ( user , :view_project , options ) ) )
2020-07-27 11:49:23 +03:00
if user . admin?
2020-09-22 11:58:48 +03:00
scope . where . not ( visibility : VISIBILITY_PRIVATE ) . or ( scope . where ( author_id : user . id ) )
2022-01-13 20:03:29 +03:00
elsif user . memberships . includes ( [ :memberships ] ) . any?
2023-11-21 19:52:18 +03:00
scope . where " #{ table_name } .visibility = :public " \
" OR ( #{ table_name } .visibility = :roles AND #{ table_name } .id IN ( " \
2022-06-27 10:08:53 +03:00
" SELECT DISTINCT d.id FROM #{ table_name } d " \
2022-06-17 14:26:18 +03:00
" INNER JOIN #{ DashboardRole . table_name } dr ON dr.dashboard_id = d.id " \
2021-06-30 20:30:58 +03:00
" INNER JOIN #{ MemberRole . table_name } mr ON mr.role_id = dr.role_id " \
2023-11-21 19:52:18 +03:00
" INNER JOIN #{ Member . table_name } m ON m.id = mr.member_id AND m.user_id = :user_id " \
" INNER JOIN #{ Project . table_name } p ON p.id = m.project_id AND p.status IN(:statuses) " \
2021-06-30 20:30:58 +03:00
' WHERE d.project_id IS NULL OR d.project_id = m.project_id))' \
2023-11-21 19:52:18 +03:00
" OR #{ table_name } .author_id = :user_id " ,
public : VISIBILITY_PUBLIC ,
roles : VISIBILITY_ROLES ,
user_id : user . id ,
statuses : Project . usable_status_ids
2020-07-27 11:49:23 +03:00
elsif user . logged?
2020-09-22 11:58:48 +03:00
scope . where ( visibility : VISIBILITY_PUBLIC ) . or ( scope . where ( author_id : user . id ) )
2020-07-27 11:49:23 +03:00
else
2020-09-22 11:58:48 +03:00
scope . where visibility : VISIBILITY_PUBLIC
2020-07-27 11:49:23 +03:00
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
2021-04-18 14:34:55 +03:00
h . update attr_name = > value
2020-07-27 11:49:23 +03:00
self [ :options ] = h
value
end
end
def visible? ( user = User . current )
return true if user . admin?
return false unless project . nil? || user . allowed_to? ( :view_project , project )
2020-08-07 19:10:18 +03:00
return true if user == author
2020-07-27 11:49:23 +03:00
case visibility
when VISIBILITY_PUBLIC
true
when VISIBILITY_ROLES
if project
2024-08-27 10:30:58 +03:00
user . roles_for_project ( project ) . intersect? ( roles )
2020-07-27 11:49:23 +03:00
else
user . memberships . joins ( :member_roles ) . where ( member_roles : { role_id : roles . map ( & :id ) } ) . any?
end
2023-06-23 17:13:42 +03:00
else
false
2020-07-27 11:49:23 +03:00
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 |
2020-09-22 11:58:48 +03:00
layout [ group ] . delete block
2020-07-27 11:49:23 +03:00
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
2020-09-22 11:58:48 +03:00
return unless content . valid_block? block , layout . values . flatten
2020-07-27 11:49:23 +03:00
remove_block block
# add it to the first group
# add it to the first group
group = available_groups . first
layout [ group ] || = [ ]
2020-09-22 11:58:48 +03:00
layout [ group ] . unshift block
2020-07-27 11:49:23 +03:00
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
2021-04-18 14:34:55 +03:00
blocks . each { | block | remove_block block }
2020-07-27 11:49:23 +03:00
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
2022-05-21 00:26:42 +03:00
def editable? ( user = User . current )
return false unless user
2020-07-27 11:49:23 +03:00
2023-12-05 21:24:41 +03:00
user . admin? || ( author == user && user . allowed_to? ( :save_dashboards , project , global : true ) )
2020-07-27 11:49:23 +03:00
end
2022-05-21 00:26:42 +03:00
def deletable? ( user = User . current )
2023-06-23 13:45:08 +03:00
return false unless editable? user
2020-07-31 09:35:08 +03:00
2020-08-07 21:55:50 +03:00
return ! system_default_was if dashboard_type != DashboardContentProject :: TYPE_NAME
2020-07-31 09:35:08 +03:00
# project dashboards needs special care
2020-08-07 21:55:50 +03:00
project . present? || ! system_default_was
2020-07-27 11:49:23 +03:00
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
2021-04-18 14:34:55 +03:00
s . join ' '
2020-07-27 11:49:23 +03:00
end
def allowed_target_projects ( user = User . current )
2024-08-27 19:05:49 +03:00
self . class . allowed_entity_target_projects ( user : ,
2022-05-30 20:15:10 +03:00
permission : :save_dashboards ,
2024-08-27 19:05:49 +03:00
project : )
2020-07-27 11:49:23 +03:00
end
# this is used to get unique cache for blocks
2021-04-18 14:34:55 +03:00
def async_params ( block , options , settings )
2020-07-27 11:49:23 +03:00
if block . blank?
msg = 'block is missing for dashboard_async'
Rails . log . error msg
raise msg
end
config = { dashboard_id : id ,
2024-08-27 10:30:58 +03:00
block : }
2020-07-27 11:49:23 +03:00
2021-12-07 20:51:09 +03:00
if RedminePluginKit . false? options [ :skip_user_id ]
2021-02-06 09:24:05 +03:00
settings [ :user_id ] = User . current . id
settings [ :user_is_admin ] = User . current . admin?
end
2020-07-27 11:49:23 +03:00
if settings . present?
settings . each do | key , setting |
2022-03-30 19:05:59 +03:00
settings [ key ] = setting . compact_blank . join ',' if setting . is_a? Array
2020-07-28 19:15:13 +03:00
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
2020-07-27 11:49:23 +03:00
end
unique_params = settings . flatten
2022-03-30 19:05:59 +03:00
unique_params += options [ :unique_params ] . compact_blank if options [ :unique_params ] . present?
2020-07-27 11:49:23 +03:00
2020-11-06 12:12:25 +03:00
# Rails.logger.debug "debug async_params for #{block}: unique_params=#{unique_params.inspect}"
2021-09-25 14:49:13 +03:00
# For truncating hash security, see https://crypto.stackexchange.com/questions/9435/is-truncating-a-sha512-hash-to-the-first-160-bits-as-secure-as-using-sha1
# truncating should solve problem with long filenames on some file systems
config [ :unique_key ] = Digest :: SHA256 . hexdigest ( unique_params . join ( '_' ) ) [ 0 .. - 20 ]
2020-07-27 11:49:23 +03:00
end
2020-11-06 12:12:25 +03:00
# Rails.logger.debug "debug async_params for #{block}: config=#{config.inspect}"
2020-07-27 11:49:23 +03:00
config
end
2020-08-25 07:29:39 +03:00
def project_id_can_change?
2023-06-23 17:02:28 +03:00
new_record? ||
dashboard_type != DashboardContentProject :: TYPE_NAME ||
! system_default_was ||
project_id_was . present?
2020-08-25 07:29:39 +03:00
end
2020-07-27 11:49:23 +03:00
private
2021-08-07 11:22:59 +03:00
def strip_whitespace
name & . strip!
end
2020-07-27 11:49:23 +03:00
def clear_unused_block_settings
blocks = layout . values . flatten
2021-04-18 14:34:55 +03:00
layout_settings . keep_if { | block , _settings | blocks . include? block }
2020-07-27 11:49:23 +03:00
end
def remove_unused_role_relations
2020-08-07 19:10:18 +03:00
return if ! saved_change_to_visibility? || visibility == VISIBILITY_ROLES
2020-07-27 11:49:23 +03:00
roles . clear
end
def validate_roles
return if visibility != VISIBILITY_ROLES || roles . present?
2020-08-06 09:18:30 +03:00
errors . add ( :base ,
[ l ( :label_role_plural ) , l ( 'activerecord.errors.messages.blank' ) ] . join ( ' ' ) )
2020-07-27 11:49:23 +03:00
end
def validate_system_default
2020-10-07 20:49:12 +03:00
return if new_record? ||
system_default_was == system_default ||
system_default? ||
project_id . present?
2020-07-27 11:49:23 +03:00
2020-08-07 21:55:50 +03:00
raise SystemDefaultChangeException
2020-07-27 11:49:23 +03:00
end
2020-08-25 07:29:39 +03:00
def validate_project_system_default
return if project_id_can_change?
raise ProjectSystemDefaultChangeException if project_id . present?
end
2023-03-05 21:09:16 +03:00
def check_locked
raise 'It is not allowed to delete dashboard, because it is locked' if locked?
end
2020-07-27 11:49:23 +03:00
def check_destroy_system_default
2022-05-21 00:26:42 +03:00
raise 'It is not allowed to delete dashboard, which is system default' unless deletable?
2020-07-27 11:49:23 +03:00
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
2024-08-27 10:30:58 +03:00
. where ( dashboard_type : )
. where . not ( id : )
2020-07-27 11:49:23 +03:00
2021-04-18 14:34:55 +03:00
scope = scope . where project : project if dashboard_type == DashboardContentProject :: TYPE_NAME
2020-07-27 11:49:23 +03:00
2020-09-22 11:58:48 +03:00
scope . update_all system_default : false
2020-07-27 11:49:23 +03:00
end
2020-08-01 16:26:20 +03:00
# check if permissions changed and dashboard settings have to be corrected
def visibility_check
user = User . current
2020-08-01 18:30:10 +03:00
return if system_default? ||
2020-08-07 19:10:18 +03:00
user . allowed_to? ( :share_dashboards , project , global : true ) ||
2020-08-01 16:26:20 +03:00
user . allowed_to? ( :set_system_dashboards , project , global : true )
# change to private
self . visibility = VISIBILITY_PRIVATE
end
2020-07-27 11:49:23 +03:00
def validate_visibility
2021-04-18 14:34:55 +03:00
errors . add :visibility , :must_be_for_everyone if system_default? && visibility != VISIBILITY_PUBLIC
2020-07-27 11:49:23 +03:00
end
def validate_name
return if name . blank?
2024-08-27 10:30:58 +03:00
scope = self . class . visible . where ( name : )
2020-07-27 11:49:23 +03:00
if dashboard_type == DashboardContentProject :: TYPE_NAME
scope = scope . project_only
2024-08-27 10:30:58 +03:00
scope = scope . where ( project_id : )
2021-04-18 14:34:55 +03:00
scope = scope . or scope . where ( project_id : nil ) if project_id . present?
2020-07-27 11:49:23 +03:00
else
scope = scope . welcome_only
end
2021-04-18 14:34:55 +03:00
scope = scope . where . not id : id unless new_record?
errors . add :name , :name_not_unique if scope . count . positive?
2020-07-27 11:49:23 +03:00
end
end