Actualiza el plugin Additionals a 3.0.2-master

This commit is contained in:
Manuel Cillero 2021-03-20 11:12:56 +01:00
parent 3b6a41320c
commit cfa0d58b18
164 changed files with 2027 additions and 58190 deletions

View file

@ -13,7 +13,7 @@ class AdditionalsChangeStatusController < ApplicationController
return
end
@issue.init_journal(User.current)
@issue.init_journal User.current
@issue.status_id = new_status_id
@issue.assigned_to = User.current if @issue.status_x_affected?(new_status_id) && issue_old_user != User.current

View file

@ -6,7 +6,6 @@ class DashboardAsyncBlocksController < ApplicationController
helper :additionals_routes
helper :additionals_queries
helper :additionals_tag
helper :queries
helper :issues
helper :activities
@ -14,6 +13,12 @@ class DashboardAsyncBlocksController < ApplicationController
include DashboardsHelper
# support for redmine_contacts_helpdesk plugin
if Redmine::Plugin.installed? 'redmine_contacts_helpdesk'
include HelpdeskHelper
helper :helpdesk
end
rescue_from Query::StatementInvalid, with: :query_statement_invalid
rescue_from StandardError, with: :dashboard_with_invalid_block

View file

@ -17,7 +17,6 @@ class DashboardsController < ApplicationController
helper :dashboards
helper :additionals_issues
helper :additionals_queries
helper :additionals_tag
include AdditionalsRoutesHelper
include AdditionalsQueriesHelper
@ -92,7 +91,7 @@ class DashboardsController < ApplicationController
respond_to do |format|
format.html
format.xml {}
format.api
end
end

View file

@ -6,7 +6,7 @@ module AdditionalsChartjsHelper
end
def select_options_for_chartjs_colorscheme(selected)
data = YAML.safe_load(ERB.new(IO.read(Rails.root.join('plugins/additionals/config/colorschemes.yml'))).result) || {}
data = YAML.safe_load(ERB.new(IO.read(File.join(Additionals.plugin_dir, 'config', 'colorschemes.yml'))).result) || {}
grouped_options_for_select(data, selected)
end
end

View file

@ -3,20 +3,34 @@ module AdditionalsClipboardjsHelper
render_clipboardjs_button(target, clipboard_text_from_button) + render_clipboardjs_javascript(target)
end
def render_text_with_clipboardjs(text)
return if text.blank?
tag.acronym text,
class: 'clipboard-text',
title: l(:label_copy_to_clipboard),
data: clipboardjs_data(text: text)
end
def clipboardjs_data(clipboard_data)
data = { 'label-copied' => l(:label_copied_to_clipboard),
'label-to-copy' => l(:label_copy_to_clipboard) }
clipboard_data.each do |key, value|
data["clipboard-#{key}"] = value if value.present?
end
data
end
private
def render_clipboardjs_button(target, clipboard_text_from_button)
data = { 'clipboard-target' => "##{target}",
'label-copied' => l(:label_copied_to_clipboard),
'label-to-copy' => l(:label_copy_to_clipboard) }
data['clipboard-text'] = clipboard_text_from_button if clipboard_text_from_button.present?
tag.button id: "zc_#{target}",
class: 'clipboard_button far fa-copy',
class: 'clipboard-button far fa-copy',
type: 'button',
title: l(:label_copy_to_clipboard),
data: data
data: clipboardjs_data(target: "##{target}", text: clipboard_text_from_button)
end
def render_clipboardjs_javascript(target)

View file

@ -100,12 +100,9 @@ module AdditionalsMenuHelper
def additionals_custom_top_menu_item(item, user_roles)
show_entry = false
item[:roles].each do |role|
if user_roles.empty? && role.to_i == Role::BUILTIN_ANONYMOUS
show_entry = true
break
elsif User.current.logged? && role.to_i == Role::BUILTIN_NON_MEMBER
# if user is logged in and non_member is active in item,
# always show it
if user_roles.empty? && role.to_i == Role::BUILTIN_ANONYMOUS ||
# if user is logged in and non_member is active in item, always show it
User.current.logged? && role.to_i == Role::BUILTIN_NON_MEMBER
show_entry = true
break
end

View file

@ -4,8 +4,8 @@ module AdditionalsQueriesHelper
end
def additionals_retrieve_query(object_type, options = {})
session_key = additionals_query_session_key(object_type)
query_class = Object.const_get("#{object_type.camelcase}Query")
session_key = additionals_query_session_key object_type
query_class = Object.const_get "#{object_type.camelcase}Query"
if params[:query_id].present?
additionals_load_query_id(query_class, session_key, params[:query_id], options, object_type)
elsif api_request? ||
@ -28,7 +28,7 @@ module AdditionalsQueriesHelper
else
# retrieve from session
@query = query_class.find_by(id: session[session_key][:id]) if session[session_key][:id]
session_data = Rails.cache.read(additionals_query_cache_key(object_type))
session_data = Rails.cache.read additionals_query_cache_key(object_type)
@query ||= query_class.new(name: '_',
filters: session_data.nil? ? nil : session_data[:filters],
group_by: session_data.nil? ? nil : session_data[:group_by],
@ -79,11 +79,11 @@ module AdditionalsQueriesHelper
def additionals_select2_search_users(options = {})
q = params[:q].to_s.strip
exclude_id = params[:user_id].to_i
scope = User.active.where(type: 'User')
scope = User.active.where type: 'User'
scope = scope.visible unless options[:all_visible]
scope = scope.where.not(id: exclude_id) if exclude_id.positive?
scope = scope.where(options[:where_filter], options[:where_params]) if options[:where_filter]
q.split(' ').map { |search_string| scope = scope.like(search_string) } if q.present?
q.split.map { |search_string| scope = scope.like(search_string) } if q.present?
scope = scope.order(last_login_on: :desc)
.limit(Additionals::SELECT2_INIT_ENTRIES)
@users = scope.to_a.sort! { |x, y| x.name <=> y.name }
@ -140,13 +140,13 @@ module AdditionalsQueriesHelper
def xlsx_write_header_row(workbook, worksheet, columns)
columns_width = []
columns.each_with_index do |c, index|
value = if c.class.name == 'String'
value = if c.is_a? String
c
else
c.caption.to_s
end
worksheet.write(0, index, value, workbook.add_format(xlsx_cell_format(:header)))
worksheet.write 0, index, value, workbook.add_format(xlsx_cell_format(:header))
columns_width << xlsx_get_column_width(value)
end
columns_width
@ -224,13 +224,11 @@ module AdditionalsQueriesHelper
def xlsx_hyperlink_cell?(token)
# Match http, https or ftp URL
if %r{\A[fh]tt?ps?://}.match?(token)
true
# Match mailto:
elsif token.present? && token.start_with?('mailto:')
true
# Match internal or external sheet link
elsif /\A(?:in|ex)ternal:/.match?(token)
if %r{\A[fh]tt?ps?://}.match?(token) ||
# Match mailto:
token.present? && token.start_with?('mailto:') ||
# Match internal or external sheet link
/\A(?:in|ex)ternal:/.match?(token)
true
end
end

View file

@ -1,9 +1,10 @@
module AdditionalsSelect2Helper
def additionals_select2_tag(name, option_tags = nil, options = {})
s = select_tag(name, option_tags, options)
id = options.delete(:id) || sanitize_to_id(name)
s << hidden_field_tag("#{name}[]", '') if options[:multiple] && options.fetch(:include_hidden, true)
s + javascript_tag("select2Tag('#{sanitize_to_id name}', #{options.to_json});")
s + javascript_tag("select2Tag('#{id}', #{options.to_json});")
end
# Transforms select filters of +type+ fields into select2

View file

@ -4,7 +4,6 @@ module AdditionalsSettingsHelper
{ name: 'wiki', partial: 'additionals/settings/wiki', label: :label_wiki },
{ name: 'macros', partial: 'additionals/settings/macros', label: :label_macro_plural },
{ name: 'rules', partial: 'additionals/settings/issues', label: :label_issue_plural },
{ name: 'users', partial: 'additionals/settings/users', label: :label_user_plural },
{ name: 'web', partial: 'additionals/settings/web_apis', label: :label_web_apis }]
unless Redmine::Plugin.installed? 'redmine_hrm'
@ -46,6 +45,22 @@ module AdditionalsSettingsHelper
text_field_tag("settings[#{name}]", value, options)]
end
def additionals_settings_passwordfield(name, options = {})
label_title = options.delete(:label).presence || l("label_#{name}")
value = options.delete(:value).presence || @settings[name]
safe_join [label_tag("settings[#{name}]", label_title),
password_field_tag("settings[#{name}]", value, options)]
end
def additionals_settings_urlfield(name, options = {})
label_title = options.delete(:label).presence || l("label_#{name}")
value = options.delete(:value).presence || @settings[name]
safe_join [label_tag("settings[#{name}]", label_title),
url_field_tag("settings[#{name}]", value, options)]
end
def additionals_settings_textarea(name, options = {})
label_title = options.delete(:label).presence || l("label_#{name}")
value = options.delete(:value).presence || @settings[name]

View file

@ -1,113 +0,0 @@
require 'digest/md5'
module AdditionalsTagHelper
def additionals_tag_cloud(tags, options = {})
return if tags.blank?
options[:show_count] = true
# prevent ActsAsTaggableOn::TagsHelper from calling `all`
# otherwise we will need sort tags after `tag_cloud`
tags = tags.all if tags.respond_to?(:all)
s = []
tags.each do |tag|
s << additionals_tag_link(tag, options)
end
sep = if options[:tags_without_color]
', '
else
' '
end
tag.div safe_join(s, sep), class: 'tags'
end
# plain list of tags
def render_additionals_tags(tags, sep = ' ')
s = if tags.blank?
['']
else
tags.map(&:name)
end
s.join(sep)
end
def additionals_tag_links(tag_list, options = {})
return unless tag_list
sep = if options[:tags_without_color]
', '
else
' '
end
safe_join(tag_list.map do |tag|
additionals_tag_link tag, options
end, sep)
end
def additionals_tag_link(tag_object, options = {})
tag_name = []
tag_name << tag_object.name
unless options[:tags_without_color]
tag_bg_color = additionals_tag_color tag_object.name
tag_fg_color = additionals_tag_fg_color tag_bg_color
tag_style = "background-color: #{tag_bg_color}; color: #{tag_fg_color}"
end
tag_name << tag.span("(#{tag_object.count})", class: 'tag-count') if options[:show_count]
if options[:tags_without_color]
tag.span link_to(safe_join(tag_name), additionals_tag_url(tag_object.name, options)),
class: 'tag-label'
else
tag.span link_to(safe_join(tag_name),
additionals_tag_url(tag_object.name, options),
style: tag_style),
class: 'additionals-tag-label-color',
style: tag_style
end
end
def additionals_tag_url(tag_name, options = {})
action = options[:tag_action].presence || (controller_name == 'hrm_user_resources' ? 'show' : 'index')
{ controller: options[:tag_controller].presence || controller_name,
action: action,
set_filter: 1,
project_id: options[:project],
fields: [:tags],
values: { tags: [tag_name] },
operators: { tags: '=' } }
end
private
def tag_cloud(tags, classes)
return [] if tags.empty?
max_count = tags.max_by(&:count).count.to_f
tags.each do |tag|
index = ((tag.count / max_count) * (classes.size - 1))
yield tag, classes[index.nan? ? 0 : index.round]
end
end
def additionals_tag_color(tag_name)
"##{Digest::MD5.hexdigest(tag_name)[0..5]}"
end
def additionals_tag_fg_color(bg_color)
# calculate contrast text color according to YIQ method
# https://24ways.org/2010/calculating-color-contrast/
# https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color
r = bg_color[1..2].hex
g = bg_color[3..4].hex
b = bg_color[5..6].hex
(r * 299 + g * 587 + b * 114) >= 128_000 ? 'black' : 'white'
end
end

View file

@ -86,10 +86,10 @@ module DashboardsHelper
tag.div class: 'active-dashboards' do
out = [tag.h3(l(:label_active_dashboard)),
tag.ul do
concat tag.ul "#{l :field_name}: #{dashboard.name}"
concat tag.ul safe_join([l(:field_author), link_to_user(dashboard.author)], ': ')
concat tag.ul "#{l :field_created_on}: #{format_time dashboard.created_at}"
concat tag.ul "#{l :field_updated_on}: #{format_time dashboard.updated_at}"
concat tag.li "#{l :field_name}: #{dashboard.name}"
concat tag.li safe_join([l(:field_author), link_to_user(dashboard.author)], ': ')
concat tag.li "#{l :field_created_on}: #{format_time dashboard.created_at}"
concat tag.li "#{l :field_updated_on}: #{format_time dashboard.updated_at}"
end]
if dashboard.description.present?
@ -105,7 +105,7 @@ module DashboardsHelper
return '' unless dashboards.any?
tag.h3(title, class: 'dashboards') +
tag.ul do
tag.ul(class: 'dashboards') do
dashboards.each do |dashboard|
selected = dashboard.id == if params[:dashboard_id].present?
params[:dashboard_id].to_i
@ -115,19 +115,24 @@ module DashboardsHelper
css = 'dashboard'
css << ' selected' if selected
li_class = nil
link = [dashboard_link(dashboard, project, class: css)]
if dashboard.system_default?
link << if dashboard.project_id.nil?
font_awesome_icon('fas_cube',
li_class = 'global'
font_awesome_icon 'fas_cube',
title: l(:field_system_default),
class: 'dashboard-system-default global')
class: "dashboard-system-default #{li_class}"
else
font_awesome_icon('fas_cube',
li_class = 'project'
font_awesome_icon 'fas_cube',
title: l(:field_project_system_default),
class: 'dashboard-system-default project')
class: "dashboard-system-default #{li_class}"
end
end
concat tag.li safe_join(link)
concat tag.li safe_join(link), class: li_class
end
end
end
@ -325,7 +330,7 @@ module DashboardsHelper
max_entries = settings[:max_entries] || DashboardContent::DEFAULT_MAX_ENTRIES
news = if dashboard.content_project.nil?
News.latest User.current
News.latest User.current, max_entries
else
dashboard.content_project
.news
@ -378,7 +383,7 @@ module DashboardsHelper
max_entries = max_entries.present? ? max_entries.to_i : 10
begin
URI.open(url) do |rss_feed|
URI.parse(url).open do |rss_feed|
rss = RSS::Parser.parse(rss_feed)
rss.items.each do |item|
cnt += 1

View file

@ -1,5 +0,0 @@
class AdditionalsRemoveUnusedTagJob < AdditionalsJob
def perform
AdditionalsTag.remove_unused_tags
end
end

View file

@ -1,4 +1,5 @@
class AdditionalsChart < ActiveRecord::Base
class AdditionalsChart
include ActiveRecord::Sanitization
include Redmine::I18n
CHART_DEFAULT_HEIGHT = 350

View file

@ -6,7 +6,7 @@ class AdditionalsFontAwesome
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(File.join(Additionals.plugin_dir, '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))

View file

@ -31,7 +31,7 @@ class AdditionalsImport < Import
next unless value
h[v.custom_field.id.to_s] =
if value.is_a?(Array)
if value.is_a? Array
value.map { |val| v.custom_field.value_from_keyword(val.strip, object) }.flatten!&.compact
else
v.custom_field.value_from_keyword(value, object)

View file

@ -0,0 +1,73 @@
class AdditionalsInfo
include Redmine::I18n
class << self
def system_infos
{ system_info: { label: l(:label_system_info), value: system_info },
system_uptime: { label: l(:label_uptime), value: system_uptime } }
end
def system_info
if windows_platform?
win_info = `wmic os get Caption,CSDVersion,BuildNumber /value`
return 'unknown' if win_info.blank?
windows_version = ''
windows_build = ''
build_names = %w[BuildNumber CSDVersion]
win_info.split(/\n+/).each do |line|
line_info = line.split '='
if line_info[0] == 'Caption'
windows_version = line_info[1]
elsif build_names.include?(line_info[0]) && line_info[1]&.present?
windows_build = line_info[1]
end
end
"#{windows_version} build #{windows_build}"
else
`uname -a`
end
end
def system_uptime(format: :time_tag)
if windows_platform?
`net stats srv | find "Statist"`
elsif File.exist? '/proc/uptime'
secs = `cat /proc/uptime`.to_i
min = 0
hours = 0
days = 0
if secs.positive?
min = (secs / 60).round
hours = (secs / 3_600).round
days = (secs / 86_400).round
end
if days >= 1
"#{days} #{l(:days, count: days)}"
elsif hours >= 1
"#{hours} #{l(:hours, count: hours)}"
else
"#{min} #{l(:minutes, count: min)}"
end
else
# this should be work on macOS
seconds = `sysctl -n kern.boottime | awk '{print $4}'`.tr ',', ''
so = DateTime.strptime seconds.strip, '%s'
if so.present?
if format == :datetime
so
else
ApplicationController.helpers.time_tag so
end
else
days = `uptime | awk '{print $3}'`.to_i.round
"#{days} #{l(:days, count: days)}"
end
end
end
def windows_platform?
/cygwin|mswin|mingw|bccwin|wince|emx/.match? RUBY_PLATFORM
end
end
end

View file

@ -22,6 +22,17 @@ module AdditionalsQuery
sql.join(' AND ')
end
def fix_sql_for_text_field(field, operator, value, table_name = nil, target_field = nil)
table_name = queried_table_name if table_name.blank?
target_field = field if target_field.blank?
sql = []
sql << "(#{sql_for_field(field, operator, value, table_name, target_field)})"
sql << "#{table_name}.#{target_field} != ''" if operator == '*'
sql.join(' AND ')
end
def initialize_ids_filter(options = {})
if options[:label]
add_available_filter 'ids', type: :integer, label: options[:label]
@ -44,30 +55,44 @@ module AdditionalsQuery
end
end
def sql_for_project_identifier_field(field, operator, values)
value = values.first
values = value.split(',').map(&:strip) if ['=', '!'].include?(operator) && value.include?(',')
sql_for_field field, operator, values, Project.table_name, 'identifier'
end
def sql_for_project_status_field(field, operator, value)
sql_for_field field, operator, value, Project.table_name, 'status'
end
def initialize_project_status_filter
return if project&.leaf?
def initialize_project_identifier_filter
return if project
add_available_filter('project.status',
add_available_filter 'project.identifier',
type: :string,
name: l(:label_attribute_of_project, name: l(:field_identifier))
end
def initialize_project_status_filter
return if project
add_available_filter 'project.status',
type: :list,
name: l(:label_attribute_of_project, name: l(:field_status)),
values: -> { project_statuses_values })
values: -> { project_statuses_values }
end
def initialize_project_filter(options = {})
if project.nil? || options[:always]
add_available_filter('project_id', order: options[:position],
add_available_filter 'project_id', order: options[:position],
type: :list,
values: -> { project_values })
values: -> { project_values }
end
return if project.nil? || project.leaf? || subproject_values.empty?
add_available_filter('subproject_id', order: options[:position],
add_available_filter 'subproject_id', order: options[:position],
type: :list_subprojects,
values: -> { subproject_values })
values: -> { subproject_values }
end
def initialize_created_filter(options = {})
@ -83,16 +108,9 @@ module AdditionalsQuery
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] }
type: :list_optional,
values: -> { tag_values(project) }
end
def initialize_approved_filter
@ -106,27 +124,35 @@ module AdditionalsQuery
end
def initialize_author_filter(options = {})
return if author_values.empty?
add_available_filter('author_id', order: options[:position],
add_available_filter 'author_id', order: options[:position],
type: :list_optional,
values: options[:no_lambda].nil? ? author_values : -> { author_values })
values: -> { author_values }
end
def initialize_assignee_filter(options = {})
return if author_values.empty?
add_available_filter('assigned_to_id', order: options[:position],
add_available_filter 'assigned_to_id', order: options[:position],
type: :list_optional,
values: options[:no_lambda] ? assigned_to_all_values : -> { assigned_to_all_values })
values: -> { assigned_to_all_values }
end
def initialize_watcher_filter(options = {})
return if watcher_values.empty? || !User.current.logged?
return unless User.current.logged?
add_available_filter('watcher_id', order: options[:position],
add_available_filter 'watcher_id', order: options[:position],
type: :list,
values: options[:no_lambda] ? watcher_values : -> { watcher_values })
values: -> { watcher_values }
end
def tag_values(project)
values = if project
queried_class.available_tags project: project.id
else
queried_class.available_tags
end
return [] if values.blank?
values.collect { |t| [t.name, t.name] }
end
# issue independend values. Use assigned_to_values from Redmine, if you want it only for issues
@ -154,7 +180,7 @@ module AdditionalsQuery
end
def sql_for_tags_field(field, _operator, value)
AdditionalsTag.sql_for_tags_field(queried_class, operator_for(field), value)
AdditionalTags.sql_for_tags_field queried_class, operator_for(field), value
end
def sql_for_is_private_field(_field, operator, value)

View file

@ -1,76 +0,0 @@
class AdditionalsTag
TAG_TABLE_NAME = RedmineCrm::Tag.table_name if defined? RedmineCrm
TAGGING_TABLE_NAME = RedmineCrm::Tagging.table_name if defined? RedmineCrm
PROJECT_TABLE_NAME = Project.table_name
class << self
def all_type_tags(klass, options = {})
RedmineCrm::Tag.where({})
.joins(tag_joins(klass, options))
.distinct
.order("#{TAG_TABLE_NAME}.name")
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]
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
def remove_unused_tags
RedmineCrm::Tag.where.not(id: RedmineCrm::Tagging.select(:tag_id).distinct)
.each(&:destroy)
end
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

View file

@ -4,6 +4,7 @@ class Dashboard < ActiveRecord::Base
include Additionals::EntityMethods
class SystemDefaultChangeException < StandardError; end
class ProjectSystemDefaultChangeException < StandardError; end
belongs_to :project
@ -302,7 +303,10 @@ class Dashboard < ActiveRecord::Base
config = { dashboard_id: id,
block: block }
settings[:user_id] = User.current.id if !options.key?(:skip_user_id) || !options[:skip_user_id]
if !options.key?(:skip_user_id) || !options[:skip_user_id]
settings[:user_id] = User.current.id
settings[:user_is_admin] = User.current.admin?
end
if settings.present?
settings.each do |key, setting|
@ -321,11 +325,11 @@ class Dashboard < ActiveRecord::Base
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}"
# 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}"
# Rails.logger.debug "debug async_params for #{block}: config=#{config.inspect}"
config
end
@ -358,7 +362,10 @@ class Dashboard < ActiveRecord::Base
end
def validate_system_default
return if new_record? || system_default_was == system_default || system_default?
return if new_record? ||
system_default_was == system_default ||
system_default? ||
project_id.present?
raise SystemDefaultChangeException
end

View file

@ -41,9 +41,13 @@ class DashboardContent
with_project: true
},
max_occurs: DashboardContent::MAX_MULTIPLE_OCCURS },
'text' => { label: l(:label_text),
'text' => { label: l(:label_text_sync),
max_occurs: MAX_MULTIPLE_OCCURS,
partial: 'dashboards/blocks/text' },
'text_async' => { label: l(:label_text_async),
max_occurs: MAX_MULTIPLE_OCCURS,
async: { required_settings: %i[text],
partial: 'dashboards/blocks/text_async' } },
'news' => { label: l(:label_news_latest),
permission: :view_news },
'documents' => { label: l(:label_document_plural),

View file

@ -3,4 +3,7 @@ class DashboardRole < ActiveRecord::Base
belongs_to :dashboard
belongs_to :role
validates :dashboard, :role,
presence: true
end

View file

@ -1,5 +0,0 @@
Deface::Override.new virtual_path: 'account/register',
name: 'add-invisble-captcha',
insert_top: 'div.box',
original: 'a9c303821376a8d83cba32654629d71cc3926a1d',
partial: 'account/invisible_captcha'

View file

@ -1,8 +1,15 @@
unless Redmine::Plugin.installed? 'redmine_servicedesk'
# redmine_contacts does not provide hook
Deface::Override.new virtual_path: 'contacts/_form',
name: 'contacts-form-hook',
insert_bottom: 'div#contact_data',
original: 'df6cae24cfd26e5299c45c427fbbd4e5f23c313e',
partial: 'hooks/view_contacts_form'
if defined?(CONTACTS_VERSION_TYPE) && CONTACTS_VERSION_TYPE == 'PRO version'
Deface::Override.new virtual_path: 'contacts/_form',
name: 'contacts-pro-form-hook',
insert_bottom: 'div#contact_data',
original: 'df6cae24cfd26e5299c45c427fbbd4e5f23c313e',
partial: 'hooks/view_contacts_form'
else
Deface::Override.new virtual_path: 'contacts/_form',
name: 'contacts-form-hook',
insert_bottom: 'div#contact_data',
original: '217049684a0bcd7e404dc6b5b2348aae47ac8a72',
partial: 'hooks/view_contacts_form'
end
end

View file

@ -0,0 +1,5 @@
Deface::Override.new virtual_path: 'wiki/edit',
name: 'wiki-edit-bottom',
insert_before: 'fieldset',
original: 'ededb6cfd5adfe8a9723d00ce0ee23575c7cc44c',
partial: 'hooks/view_wiki_form_bottom'

View file

@ -0,0 +1,5 @@
Deface::Override.new virtual_path: 'wiki/show',
name: 'wiki-show-bottom',
insert_before: 'p.wiki-update-info',
original: 'd9f52aa98f1cb335314570d3f5403690f1b29145',
partial: 'hooks/view_wiki_show_bottom'

View file

@ -1,2 +0,0 @@
- if Additionals.setting?(:invisible_captcha)
= f.invisible_captcha :url, autocomplete: 'off'

View file

@ -1,7 +1,7 @@
- footer = Additionals.setting(:global_footer)
- footer = Additionals.setting :global_footer
- if footer.present?
.additionals-footer
= textilizable(footer)
= textilizable footer
- if @additionals_help_items.present?
javascript:
@ -9,7 +9,7 @@
$('a.help').parent().append("<ul class=\"menu-children\">#{escape_javascript @additionals_help_items}</ul>");
});
- if Additionals.setting?(:open_external_urls)
- if Additionals.setting? :open_external_urls
javascript:
$(function() {
$('a.external').attr({ 'target': '_blank',

View file

@ -1,2 +1,2 @@
- if Additionals.setting?(:add_go_to_top)
- if Additionals.setting? :add_go_to_top
a#gototop

View file

@ -7,8 +7,7 @@ table.list.issue-report.table-of-values
tbody
- options = { set_filter: 1 }
- @chart[:filters].each do |line|
- if line[:filter]
- options.merge! line[:filter]
- options.merge! line[:filter] if line[:filter]
tr class="#{cycle 'odd', 'even'}"
td.name class="#{line[:id].to_s == '0' ? 'summary' : ''}"
- if line[:filter].nil?

View file

@ -1,11 +1,13 @@
= render(partial: 'additionals/live_search_ajax_call.js', layout: false, formats: [:js])
- unless defined? classes
- classes = 'title'
- classes = 'title' unless defined? classes
h2 class="#{classes}"
= @query.new_record? ? l(title) : h(@query.name)
span.additionals-live-search
= text_field_tag(:search,
= text_field_tag :search,
q,
autocomplete: 'off',
class: 'live-search-field',
placeholder: l(:label_query_name_search))
placeholder: defined?(placeholder) ? placeholder : l(:label_query_name_search)
javascript:
observeLiveSearchField('search',
'query-result-list')

View file

@ -2,5 +2,4 @@
= additionals_library_load :font_awesome
= stylesheet_link_tag 'additionals', plugin: 'additionals'
= javascript_include_tag 'additionals', plugin: 'additionals'
- unless Redmine::Plugin.installed? 'redmine_hrm'
- render_custom_top_menu_item
- render_custom_top_menu_item unless Redmine::Plugin.installed? 'redmine_hrm'

View file

@ -1,18 +0,0 @@
javascript:
$(function() {
// when the #search field changes
$('#search').live_observe_field(2, function() {
var form = $('#query_form'); // grab the form wrapping the search bar.
var url = form.attr('action');
form.find('[name="c[]"] option').each(function(i, elem) {
$(elem).attr('selected', true)
})
var formData = form.serialize();
form.find('[name="c[]"] option').each(function(i, elem) {
$(elem).attr('selected', false)
})
$.get(url, formData, function(data) { // perform an AJAX get, the trailing function is what happens on successful get.
$("#query-result-list").html(data); // replace the "results" div with the result of action taken
});
});
});

View file

@ -21,7 +21,7 @@ javascript:
placeholder: "#{options[:placeholder].presence}",
allowClear: #{options[:allow_clear].present? && options[:allow_clear] ? 'true' : 'false'},
minimumInputLength: 0,
width: "#{options[:width].presence || '60%'}",
width: "#{options[:width].presence || '90%'}",
templateResult: #{options[:template_result].presence || 'formatNameWithIcon'},
#{options[:template_selection].present? ? ('templateSelection: ' + options[:template_selection]) : nil}
})

View file

@ -16,8 +16,8 @@ fieldset.box
- columns.each do |s|
label.inline
- value = @settings[setting_name_totals.to_sym].present? ? @settings[setting_name_totals.to_sym].include?(s.name.to_s) : false
= check_box_tag("settings[#{setting_name_totals}][]",
= check_box_tag "settings[#{setting_name_totals}][]",
s.name,
value,
id: nil)
id: nil
= s.caption

View file

@ -1,30 +0,0 @@
- if defined?(show_always) && show_always || entry.tag_list.present?
.tags.attribute
- unless defined? hide_label
span.label
= l(:field_tag_list)
' :
- if defined?(editable) && editable
#tags-data
= additionals_tag_links(entry.tags,
project: @project,
tags_without_color: defined?(tags_without_color) ? tags_without_color : false)
'
span.contextual
= link_to l(:label_edit_tags),
{},
onclick: "$('#edit_tags_form').show(); $('#tags-data').hide(); return false;",
id: 'edit_tags_link'
#edit_tags_form style="display: none;"
= form_tag(update_url, method: :put, multipart: true) do
= render partial: 'tags_form'
'
= submit_tag l(:button_save), class: 'button-small'
'
= link_to l(:button_cancel), {}, onclick: "$('#edit_tags_form').hide(); $('#tags-data').show(); return false;"
- else
= additionals_tag_links(entry.tags,
project: @project,
tags_without_color: defined?(tags_without_color) ? tags_without_color : false)

View file

@ -1,7 +1,3 @@
' Need Help? :
= link_to(l(:label_additionals_doc),
'https://additionals.readthedocs.io/en/latest/',
class: 'external',
target: '_blank',
rel: 'noopener')
= link_to_external l(:label_additionals_doc), 'https://additionals.readthedocs.io/en/latest/'
= render_tabs additionals_settings_tabs

View file

@ -1,5 +0,0 @@
p
= additionals_settings_checkbox :invisible_captcha,
disabled: (true unless Setting.self_registration?)
em.info
= t(:invisible_captcha_info_html)

View file

@ -1,6 +1,6 @@
h2 = l(:label_settings_macros) + " (#{@available_macros.count})"
.info = t(:label_top_macros_help_html)
.info = t :label_top_macros_help_html
br
.box
- @available_macros.each do |macro, options|

View file

@ -1,13 +1,6 @@
table.list
tr
td.name
= l :label_system_info
| :
td.name
= system_info
tr
td.name
= l :label_uptime
| :
td.name
= system_uptime
- AdditionalsInfo.system_infos.each_value do |system_info|
tr
td.name
= "#{system_info[:label]}:"
td.name = system_info[:value]

View file

@ -1 +0,0 @@
== @tags.collect { |tag| { 'id' => tag.name, 'text' => tag.name } }.to_json

View file

@ -9,10 +9,7 @@
.splitcontent
.splitcontentleft
- if @dashboard.new_record?
= hidden_field_tag 'dashboard[dashboard_type]', @dashboard.dashboard_type
= hidden_field_tag 'dashboard[dashboard_type]', @dashboard.dashboard_type if @dashboard.new_record?
- if @project && @allowed_projects.present? && @allowed_projects.count > 1
p
= f.select :project_id,

View file

@ -6,6 +6,6 @@
- events_by_day = activity_dashboard_data settings, dashboard
- title = Additionals.true?(settings[:me_only]) ? l(:label_my_activity) : l(:label_activity)
h3 = link_to title, activity_path(user_id: User.current,
from: events_by_day.keys.first)
from: events_by_day.keys.first)
= render partial: 'activities/activities', locals: { events_by_day: events_by_day }

View file

@ -1,17 +1,17 @@
- max_entries = settings[:max_entries].presence || DashboardContent::DEFAULT_MAX_ENTRIES
div id="#{block}-settings" style="#{'display: none;' if hide}"
= form_tag(_update_layout_setting_dashboard_path(@project, @dashboard), remote: true) do
= hidden_field_tag "settings[#{block}][me_only]", '0'
.box
p
label
= l(:label_max_entries)
= l :label_max_entries
' :
= number_field_tag "settings[#{block}][max_entries]", max_entries, min: 1, max: 1000, required: true
= number_field_tag "settings[#{block}][max_entries]",
settings[:max_entries].presence || DashboardContent::DEFAULT_MAX_ENTRIES,
min: 1, max: 1000, required: true
p
label
= l(:label_only_my_activities)
= l :label_only_my_activities
' :
= check_box_tag "settings[#{block}][me_only]", '1', Additionals.true?(settings[:me_only])

View file

@ -11,10 +11,7 @@
- if feed[:items].count.positive?
ul.reporting-list.feed
- feed[:items].each do |item|
li
= link_to item[:title],
item[:link],
class: 'external', rel: 'noopener noreferrer', target: '_blank'
li = link_to_external item[:title], item[:link]
- else
p.nodata = l :label_no_data
- elsif settings[:url].blank?

View file

@ -6,7 +6,7 @@ h3.icon.icon-news = l :label_news_latest
.box
p
label
= l(:label_max_entries)
= l :label_max_entries
' :
= number_field_tag "settings[#{block}][max_entries]", max_entries, min: 1, max: 1000, required: true
p

View file

@ -1,7 +1,7 @@
- if @subprojects.any?
h3.icon.icon-projects
= l(:label_subproject_plural)
= l :label_subproject_plural
ul.subprojects
- @subprojects.each do |project|
li
= link_to(project.name, project_path(project), class: project.css_classes).html_safe
= link_to project.name, project_path(project), class: project.css_classes

View file

@ -22,6 +22,7 @@
- if count.positive?
/ required by some helpers of other plugins
- @query = query
= render partial: query_block[:list_partial],
locals: { query_block[:entities_var] => query.send(query_block[:entries_method],
limit: settings[:max_entries] || DashboardContent::DEFAULT_MAX_ENTRIES),

View file

@ -1,26 +1,12 @@
ruby:
title = settings[:title] || l(:label_text)
text = settings[:text]
- if title.present?
h3 = title
- if settings[:text].nil?
h3
= l :label_text_sync
- elsif settings[:title].present?
h3
= settings[:title]
- if @can_edit
div id="#{block}-settings" style='display: none;'
= form_tag(_update_layout_setting_dashboard_path(@project, @dashboard), remote: true) do
.box
p
label
= l :field_title
' :
= text_field_tag "settings[#{block}][title]", title
p
= text_area_tag "settings[#{block}][text]", text, rows: addtionals_textarea_cols(text), class: 'wiki-edit'
= wikitoolbar_for "settings_#{block}_text"
p
= submit_tag l(:button_save)
'
= link_to_function l(:button_cancel), "$('##{block}-settings').toggle();"
= render partial: 'dashboards/blocks/text_async_settings', locals: { block: block, settings: settings }
.wiki
= textilizable text
= textilizable settings[:text]

View file

@ -0,0 +1,10 @@
- cache render_async_cache_key(_dashboard_async_blocks_path(@project,
dashboard.async_params(block, async, settings))),
expires_in: async[:cache_expires_in] || DashboardContent::RENDER_ASYNC_CACHE_EXPIRES_IN,
skip_digest: true do
- if settings[:title].present?
h3 = settings[:title]
.wiki
= textilizable settings[:text]

View file

@ -0,0 +1,15 @@
div id="#{block}-settings" style='display: none;'
= form_tag(_update_layout_setting_dashboard_path(@project, @dashboard), remote: true) do
.box
p
label
= l :field_title
' :
= text_field_tag "settings[#{block}][title]", (settings[:title] || l(:label_text_sync))
p
= text_area_tag "settings[#{block}][text]", settings[:text], rows: addtionals_textarea_cols(settings[:text]), class: 'wiki-edit'
= wikitoolbar_for "settings_#{block}_text"
p
= submit_tag l :button_save
'
= link_to_function l(:button_cancel), "$('##{block}-settings').toggle();"

View file

@ -1,7 +1,6 @@
h2 = l(:button_dashboard_edit)
= labelled_form_for :dashboard,
@dashboard,
url: { action: 'update', id: @dashboard.id },
html: { multipart: true, method: :put, id: 'dashboard-form' } do |f|
html: { multipart: true, id: 'dashboard-form' } do |f|
= render partial: 'form', locals: { f: f }
= submit_tag l(:button_save)

View file

@ -0,0 +1 @@
= call_hook :view_wiki_form_bottom, content: @content, page: @page

View file

@ -0,0 +1 @@
= call_hook :view_wiki_show_bottom, content: @content, page: @page

View file

@ -1,5 +1,9 @@
- if User.current.logged? && @issue.editable? && Additionals.setting?(:issue_assign_to_me) && \
@issue.assigned_to_id != User.current.id && @project.assignable_users.detect { |u| u.id == User.current.id }
- if User.current.logged? && \
Additionals.setting?(:issue_assign_to_me) && \
@issue.editable? && \
@issue.safe_attribute?('assigned_to_id') && \
@issue.assigned_to_id != User.current.id && \
@project.assignable_users.detect { |u| u.id == User.current.id }
= link_to font_awesome_icon('far_user-circle', post_text: l(:button_assign_to_me)),
issue_assign_to_me_path(@issue), method: :put,
class: 'assign-to-me'

View file

@ -1,19 +1,19 @@
- if Additionals.setting?(:issue_change_status_in_sidebar) && \
@issue && \
User.current.allowed_to?(:edit_issues, @project) && \
(!@issue.closed? || User.current.allowed_to?(:edit_closed_issues, @project))
- statuses = @issue.sidbar_change_status_allowed_to(User.current)
@issue && \
User.current.allowed_to?(:edit_issues, @project) && \
(!@issue.closed? || User.current.allowed_to?(:edit_closed_issues, @project))
- statuses = @issue.sidbar_change_status_allowed_to User.current
- if statuses.present?
h3 = l(:label_issue_change_status)
h3 = l :label_issue_change_status
ul.issue-status-change-sidebar
- statuses.each do |s|
- if s != @issue.status
- unless s == @issue.status
li
- if s.is_closed?
= link_to(font_awesome_icon('fas_caret-square-left', post_text: s.name),
= link_to font_awesome_icon('fas_caret-square-left', post_text: s.name),
issue_change_status_path(@issue, new_status_id: s.id),
method: :put, class: "status-switch status-#{s.id}")
method: :put, class: "status-switch status-#{s.id}"
- else
= link_to(font_awesome_icon('far_caret-square-left', post_text: s.name),
= link_to font_awesome_icon('far_caret-square-left', post_text: s.name),
issue_change_status_path(@issue, new_status_id: s.id),
method: :put, class: "status-switch status-#{s.id}")
method: :put, class: "status-switch status-#{s.id}"

View file

@ -6,8 +6,7 @@
edit_project_dashboard_path(@project, @dashboard),
class: 'icon icon-edit'
- unless Redmine::Plugin.installed? 'redmine_reporting'
= bookmark_link @project
= bookmark_link @project unless Redmine::Plugin.installed? 'redmine_reporting'
= call_hook :view_project_contextual_links, project: @project
- if @dashboard&.editable?
@ -49,9 +48,7 @@
class: 'icon icon-del'
= sidebar_action_toggle @dashboard_sidebar, @dashboard, @project
- unless @dashboard_sidebar
= render_dashboard_actionlist @dashboard, @project
= render_dashboard_actionlist @dashboard, @project unless @dashboard_sidebar
= call_hook :view_project_actions_dropdown, project: @project
- if User.current.allowed_to?(:edit_project, @project)

View file

@ -1,4 +1,3 @@
- if Additionals.setting?(:new_issue_on_profile) && @memberships.present?
- project_url = memberships_new_issue_project_url(user, @memberships)
- if project_url.present?
= link_to(l(:label_issue_new), project_url, class: 'user-new-issue icon icon-add')
- project_url = memberships_new_issue_project_url user, @memberships
= link_to l(:label_issue_new), project_url, class: 'user-new-issue icon icon-add' if project_url.present?

View file

@ -21,8 +21,7 @@
= delete_dashboard_link dashboard_path(@dashboard),
class: 'icon icon-del'
= sidebar_action_toggle @dashboard_sidebar, @dashboard
- unless @dashboard_sidebar
= render_dashboard_actionlist @dashboard
= render_dashboard_actionlist @dashboard unless @dashboard_sidebar
= call_hook :view_welcome_show_actions_dropdown

View file

@ -7,6 +7,7 @@
= avatar(user, size: 50)
.user.line[style="font-weight: bold;"]
= link_to_user user
- if !user_roles.nil? && user_roles[user.id]
.user.line
= l :field_role
@ -16,6 +17,7 @@
= l :field_login
' :
= link_to user.login, "/users/#{user.id}"
- unless user.pref.hide_mail
.user.line
= l :field_mail