Actualizar plugin Additionals a 3.0.0

This commit is contained in:
Manuel Cillero 2020-11-22 21:30:25 +01:00
parent 3d976f1b3b
commit a26f5567af
399 changed files with 70374 additions and 4093 deletions

View file

@ -1,88 +1,99 @@
module Additionals
MAX_CUSTOM_MENU_ITEMS = 5
SELECT2_INIT_ENTRIES = 20
DEFAULT_MODAL_WIDTH = '350px'.freeze
GOTO_LIST = " \xc2\xbb".freeze
LIST_SEPARATOR = GOTO_LIST + ' '
LIST_SEPARATOR = "#{GOTO_LIST} ".freeze
RenderAsync.configuration.jquery = true
class << self
def setup
incompatible_plugins(%w[redmine_tweaks
redmine_issue_control_panel
incompatible_plugins %w[redmine_issue_control_panel
redmine_editauthor
redmine_changeauthor
redmine_auto_watch])
patch(%w[AccountController
redmine_auto_watch]
patch %w[AccountController
ApplicationController
AutoCompletesController
Issue
IssuePriority
TimeEntry
Project
Wiki
WikiController
ProjectsController
WelcomeController
ReportsController
Principal
QueryFilter
Role
User
UserPreference])
UserPreference]
Rails.configuration.assets.paths << Emoji.images_path
Redmine::WikiFormatting.format_names.each do |format|
case format
when 'markdown'
Redmine::WikiFormatting::Markdown::HTML.send(:include, Patches::FormatterMarkdownPatch)
Redmine::WikiFormatting::Markdown::Helper.send(:include, Patches::FormattingHelperPatch)
Redmine::WikiFormatting::Markdown::HTML.include Patches::FormatterMarkdownPatch
Redmine::WikiFormatting::Markdown::Helper.include Patches::FormattingHelperPatch
when 'textile'
Redmine::WikiFormatting::Textile::Formatter.send(:include, Patches::FormatterTextilePatch)
Redmine::WikiFormatting::Textile::Helper.send(:include, Patches::FormattingHelperPatch)
Redmine::WikiFormatting::Textile::Formatter.include Patches::FormatterTextilePatch
Redmine::WikiFormatting::Textile::Helper.include Patches::FormattingHelperPatch
end
end
IssuesController.send :helper, AdditionalsIssuesHelper
SettingsController.send :helper, AdditionalsSettingsHelper
WikiController.send :helper, AdditionalsWikiPdfHelper
CustomFieldsController.send :helper, AdditionalsCustomFieldsHelper
# Static class patches
IssuesController.send(:helper, AdditionalsIssuesHelper)
WikiController.send(:helper, AdditionalsWikiPdfHelper)
Redmine::AccessControl.send(:include, Additionals::Patches::AccessControlPatch)
Redmine::AccessControl.include Additionals::Patches::AccessControlPatch
# Global helpers
ActionView::Base.send :include, Additionals::Helpers
ActionView::Base.send :include, AdditionalsFontawesomeHelper
ActionView::Base.send :include, AdditionalsMenuHelper
ActionView::Base.include Additionals::Helpers
ActionView::Base.include AdditionalsFontawesomeHelper
ActionView::Base.include AdditionalsMenuHelper
ActionView::Base.include Additionals::AdditionalsSelect2Helper
# Hooks
require_dependency 'additionals/hooks'
# Macros
load_macros(%w[calendar cryptocompare date fa gist gmap group_users iframe
load_macros %w[cryptocompare date fa gist gmap google_docs group_users iframe
issue redmine_issue redmine_wiki
last_updated_at last_updated_by meteoblue member new_issue project
recently_updated reddit slideshare tradingview twitter user vimeo youtube])
end
def settings
settings_compatible(:plugin_additionals)
recently_updated reddit slideshare tradingview twitter user vimeo youtube asciinema]
end
def settings_compatible(plugin_name)
if Setting[plugin_name].class == Hash
if Rails.version >= '5.2'
# convert Rails 4 data (this runs only once)
new_settings = ActiveSupport::HashWithIndifferentAccess.new(Setting[plugin_name])
Setting.send("#{plugin_name}=", new_settings)
new_settings
else
ActionController::Parameters.new(Setting[plugin_name])
end
# convert Rails 4 data (this runs only once)
new_settings = ActiveSupport::HashWithIndifferentAccess.new(Setting[plugin_name])
Setting.send("#{plugin_name}=", new_settings)
new_settings
else
# Rails 5 uses ActiveSupport::HashWithIndifferentAccess
Setting[plugin_name]
end
end
# support with default setting as fall back
def setting(value)
if settings.key? value
settings[value]
else
load_settings[value]
end
end
def setting?(value)
true?(settings[value])
true? setting(value)
end
def true?(value)
return true if value.to_i == 1 || value.to_s.casecmp('true').zero?
return false if value.is_a? FalseClass
return true if value.is_a?(TrueClass) || value.to_i == 1 || value.to_s.casecmp('true').zero?
false
end
@ -103,29 +114,49 @@ module Additionals
def patch(patches = [], plugin_id = 'additionals')
patches.each do |name|
patch_dir = Rails.root.join('plugins', plugin_id, 'lib', plugin_id, 'patches')
patch_dir = Rails.root.join("plugins/#{plugin_id}/lib/#{plugin_id}/patches")
require "#{patch_dir}/#{name.underscore}_patch"
target = name.constantize
patch = "#{plugin_id.camelize}::Patches::#{name}Patch".constantize
target.send(:include, patch) unless target.included_modules.include?(patch)
target.include(patch) unless target.included_modules.include?(patch)
end
end
def load_macros(macros = [], plugin_id = 'additionals')
macro_dir = Rails.root.join('plugins', plugin_id, 'lib', plugin_id, 'wiki_macros')
macro_dir = Rails.root.join("plugins/#{plugin_id}/lib/#{plugin_id}/wiki_macros")
macros.each do |macro|
require_dependency "#{macro_dir}/#{macro.underscore}_macro"
end
end
def load_settings(plugin_id = 'additionals')
data = YAML.safe_load(ERB.new(IO.read(Rails.root.join('plugins',
plugin_id,
'config',
'settings.yml'))).result) || {}
data.symbolize_keys
cached_settings_name = "@load_settings_#{plugin_id}"
cached_settings = instance_variable_get cached_settings_name
if cached_settings.nil?
data = YAML.safe_load(ERB.new(IO.read(Rails.root.join("plugins/#{plugin_id}/config/settings.yml"))).result) || {}
instance_variable_set cached_settings_name, data.symbolize_keys
else
cached_settings
end
end
def hash_remove_with_default(field, options, default = nil)
value = nil
if options.key? field
value = options[field]
options.delete field
elsif !default.nil?
value = default
end
[value, options]
end
private
def settings
settings_compatible :plugin_additionals
end
end
end

View file

@ -0,0 +1,16 @@
module Additionals
module EntityMethods
def assignable_users(prj = nil)
prj = project if project.present?
users = prj.assignable_users_and_groups.to_a
users << author if author&.active?
if assigned_to_id_was.present?
assignee = Principal.find_by(id: assigned_to_id_was)
users << assignee if assignee
end
users.uniq!
users.sort
end
end
end

View file

@ -39,10 +39,8 @@ module Additionals
esc = Regexp.last_match(2)
smiley = Regexp.last_match(3)
if esc.nil?
leading + content_tag(:span,
'',
class: "additionals smiley smiley-#{name}",
title: smiley)
leading + tag.span(class: "additionals smiley smiley-#{name}",
title: smiley)
else
leading + smiley
end
@ -55,12 +53,11 @@ module Additionals
emoji_code = Regexp.last_match(1)
emoji = Emoji.find_by_alias(emoji_code) # rubocop:disable Rails/DynamicFindBy
if emoji.present?
tag(:img,
src: inline_emojify_image_path(emoji.image_filename),
title: ":#{emoji_code}:",
style: 'vertical-align: middle',
width: '20',
height: '20')
tag.img src: inline_emojify_image_path(emoji.image_filename),
title: ":#{emoji_code}:",
style: 'vertical-align: middle',
width: '20',
height: '20'
else
match
end
@ -69,7 +66,7 @@ module Additionals
end
def inline_emojify_image_path(image_filename)
path = Setting.protocol + '://' + Setting.host_name
path = "#{Setting.protocol}://#{Setting.host_name}"
# TODO: use relative path, if not for mailer
# path = '/' + Rails.public_path.relative_path_from Rails.root.join('public')
"#{path}/images/emoji/" + image_filename

View file

@ -1,4 +1,3 @@
# Global helper functions
module Additionals
module Helpers
def additionals_list_title(options)
@ -8,11 +7,11 @@ module Additionals
issue_path(options[:issue]),
class: options[:issue].css_classes)
elsif options[:user]
title << avatar(options[:user], size: 50) + ' ' + options[:user].name
title << safe_join([avatar(options[:user], size: 50), options[:user].name], ' ')
end
title << options[:name] if options[:name]
title << h(options[:query].name) if options[:query] && !options[:query].new_record?
safe_join(title, Additionals::LIST_SEPARATOR)
safe_join title, Additionals::LIST_SEPARATOR
end
def additionals_title_for_locale(title, lang)
@ -29,44 +28,34 @@ module Additionals
def additionals_i18n_title(options, title)
i18n_title = "#{title}_#{::I18n.locale}".to_sym
if options.key?(i18n_title)
if options.key? i18n_title
options[i18n_title]
elsif options.key?(title)
elsif options.key? title
options[title]
end
end
def additionals_settings_tabs
tabs = [{ name: 'general', partial: 'additionals/settings/general', label: :label_general },
{ name: 'content', partial: 'additionals/settings/overview', label: :label_overview_page },
{ 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: 'projects', partial: 'additionals/settings/projects', label: :label_project_plural },
{ name: 'users', partial: 'additionals/settings/users', label: :label_user_plural },
{ name: 'web', partial: 'additionals/settings/web_apis', label: :label_web_apis }]
if User.current.try(:hrm_user_type_id).nil?
tabs << { name: 'menu', partial: 'additionals/settings/menu', label: :label_settings_menu }
end
tabs
end
def render_issue_macro_link(issue, text, comment_id = nil)
only_path = controller_path.split('_').last != 'mailer'
content = link_to(text, issue_url(issue, only_path: only_path), class: issue.css_classes)
if comment_id.nil?
content
else
render_issue_with_comment(issue, content, comment_id, only_path)
render_issue_with_comment issue, content, comment_id, only_path: only_path
end
end
def render_issue_with_comment(issue, content, comment_id, only_path = false)
comment = issue.journals
.where(private_notes: false)
.offset(comment_id - 1).limit(1).first.try(:notes)
def render_issue_with_comment(issue, content, comment_id, only_path: false)
journal = issue.journals.select(:notes, :private_notes, :user_id).offset(comment_id - 1).limit(1).first
comment = if journal
user = User.current
if user.allowed_to?(:view_private_notes, issue.project) ||
!journal.private_notes? ||
journal.user == user
journal.notes
end
end
if comment.blank?
comment = 'N/A'
comment_link = comment_id
@ -74,9 +63,9 @@ module Additionals
comment_link = link_to(comment_id, issue_url(issue, only_path: only_path, anchor: "note-#{comment_id}"))
end
content_tag :div, class: 'issue-macro box' do
content_tag(:div, safe_join([content, '-', l(:label_comment), comment_link], ' '), class: 'issue-macro-subject') +
content_tag(:div, textilizable(comment), class: 'issue-macro-comment journal has-notes')
tag.div class: 'issue-macro box' do
tag.div(safe_join([content, '-', l(:label_comment), comment_link], ' '), class: 'issue-macro-subject') +
tag.div(textilizable(comment), class: 'issue-macro-comment journal has-notes')
end
end
@ -145,9 +134,12 @@ module Additionals
rc
end
def additionals_library_load(module_name)
method = "additionals_load_#{module_name}"
send(method)
def additionals_library_load(module_names)
s = []
Array(module_names).each do |module_name|
s << send("additionals_load_#{module_name}")
end
safe_join s
end
def system_uptime
@ -158,7 +150,7 @@ module Additionals
min = 0
hours = 0
days = 0
if secs > 0
if secs.positive?
min = (secs / 60).round
hours = (secs / 3_600).round
days = (secs / 86_400).round
@ -171,8 +163,15 @@ module Additionals
"#{min} #{l(:minutes, count: min)}"
end
else
days = `uptime | awk '{print $3}'`.to_i.round
"#{days} #{l(:days, count: days)}"
# this should be mac os
seconds = `sysctl -n kern.boottime | awk '{print $4}'`.tr(',', '')
so = DateTime.strptime(seconds.strip, '%s')
if so.present?
time_tag(so)
else
days = `uptime | awk '{print $3}'`.to_i.round
"#{days} #{l(:days, count: days)}"
end
end
end
@ -185,16 +184,7 @@ module Additionals
end
def windows_platform?
true if /cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM
end
def bootstrap_datepicker_locale
s = ''
locale = User.current.language.presence || ::I18n.locale
locale = 'es' if locale == 'es-PA'
locale = 'sr-latin' if locale == 'sr-YU'
s = javascript_include_tag("locales/bootstrap-datepicker.#{locale.downcase}.min", plugin: 'additionals') unless locale == 'en'
s
true if /cygwin|mswin|mingw|bccwin|wince|emx/.match?(RUBY_PLATFORM)
end
def autocomplete_select_entries(name, type, option_tags, options = {})
@ -202,7 +192,7 @@ module Additionals
# if option_tags is not an array, it should be an object
option_tags = options_for_select([[option_tags.try(:name), option_tags.try(:id)]], option_tags.try(:id))
end
options[:project] = @project if @project.present? && options[:project].blank?
options[:project] = @project if @project && options[:project].blank?
s = []
s << hidden_field_tag("#{name}[]", '') if options[:multiple]
@ -217,7 +207,21 @@ module Additionals
locals: { field_id: sanitize_to_id(name),
ajax_url: send("#{type}_path", project_id: options[:project], user_id: options[:user_id]),
options: options })
safe_join(s)
safe_join s
end
def project_list_css_classes(project, level)
classes = [cycle('odd', 'even')]
classes += project.css_classes.split(' ')
if level.positive?
classes << 'idnt'
classes << "idnt-#{level}"
end
classes.join(' ')
end
def addtionals_textarea_cols(text, options = {})
[[(options[:min].presence || 8), text.to_s.length / 50].max, (options[:max].presence || 20)].min
end
private
@ -232,37 +236,50 @@ module Additionals
end
def additionals_include_js(js_name)
if additionals_already_loaded('js', js_name)
if additionals_already_loaded 'js', js_name
''
else
javascript_include_tag(js_name, plugin: 'additionals') + "\n"
javascript_include_tag js_name, plugin: 'additionals'
end
end
def additionals_include_css(css)
if additionals_already_loaded('css', css)
if additionals_already_loaded 'css', css
''
else
stylesheet_link_tag(css, plugin: 'additionals') + "\n"
stylesheet_link_tag css, plugin: 'additionals'
end
end
def additionals_load_select2
additionals_include_js('additionals_to_select2')
additionals_include_css('select2') +
additionals_include_js('select2.min') +
additionals_include_js('select2_helper')
end
def additionals_load_clipboardjs
additionals_include_js 'clipboard.min'
end
def additionals_load_observe_field
additionals_include_js('additionals_observe_field')
additionals_include_js 'additionals_observe_field'
end
def additionals_load_font_awesome
additionals_include_css('fontawesome-all.min')
additionals_include_css 'fontawesome-all.min'
end
def additionals_load_nvd3
additionals_include_css('nv.d3.min') +
additionals_include_js('d3.min') +
additionals_include_js('nv.d3.min')
def additionals_load_chartjs
additionals_include_css('Chart.min') +
additionals_include_js('Chart.bundle.min')
end
def additionals_load_chartjs_datalabels
additionals_include_js 'chartjs-plugin-datalabels.min'
end
def additionals_load_chartjs_colorschemes
additionals_include_js 'chartjs-plugin-colorschemes.min'
end
def additionals_load_mermaid
@ -271,30 +288,48 @@ module Additionals
end
def additionals_load_d3
additionals_include_js('d3.min')
additionals_include_js 'd3.min'
end
def additionals_load_d3plus
additionals_include_js('d3plus.full.min')
additionals_include_js 'd3plus.full.min'
end
def additionals_load_zeroclipboard
additionals_include_js('zeroclipboard_min')
def additionals_load_d3plus_old
additionals_include_js 'd3plus-old.full.min'
end
def additionals_load_d3plus_hierarchy
additionals_include_js 'd3plus-hierarchy.full'
end
def additionals_load_d3plus_network
additionals_include_js 'd3plus-network.full.min'
end
def user_with_avatar(user, options = {})
return if user.nil?
options[:size] = 14 if options[:size].nil?
options[:class] = 'additionals-avatar' if options[:class].nil?
s = []
s << avatar(user, options)
s << if options[:no_link]
user.name
else
link_to_user(user)
end
safe_join(s)
if user.type == 'Group'
if options[:no_link]
user.name
elsif Redmine::Plugin.installed? 'redmine_hrm'
link_to_hrm_group user
else
user.name
end
else
options[:size] = 14 if options[:size].nil?
options[:class] = 'additionals-avatar' if options[:class].nil?
s = []
s << avatar(user, options)
s << if options[:no_link]
user.name
else
link_to_user user
end
safe_join s
end
end
def options_for_menu_select(active)
@ -303,24 +338,25 @@ module Additionals
l(:label_app_menu) => 'app' }, active)
end
def options_for_overview_select(active)
options_for_select({ l(:button_hide) => '',
l(:show_on_redmine_home) => 'home',
l(:show_on_project_overview) => 'project',
l(:show_always) => 'always' }, active)
end
def options_for_welcome_select(active)
options_for_select({ l(:button_hide) => '',
l(:show_welcome_left) => 'left',
l(:show_welcome_right) => 'right' }, active)
end
def human_float_number(value, options = {})
ActionController::Base.helpers.number_with_precision(value,
precision: options[:precision].presence || 2,
separator: options[:separator].presence || '.',
strip_insignificant_zeros: true)
end
def query_list_back_url_tag(project = nil, params = nil)
url = if controller_name == 'dashboard_async_blocks' && request.query_parameters.key?('dashboard_id')
dashboard_link_path project,
Dashboard.find_by(id: request.query_parameters['dashboard_id']),
refresh: 1
elsif params.nil?
url_for params: request.query_parameters
else
url_for params: params
end
hidden_field_tag 'back_url', url, id: nil
end
end
end

View file

@ -1,33 +1,45 @@
# Redmine hooks
module Additionals
class AdditionalsHookListener < Redmine::Hook::ViewListener
include IssuesHelper
include AdditionalsIssuesHelper
render_on(:view_layouts_base_html_head, partial: 'additionals/html_head')
render_on(:view_layouts_base_content, partial: 'additionals/content')
render_on(:view_layouts_base_body_bottom, partial: 'additionals/body_bottom')
render_on :view_layouts_base_html_head, partial: 'additionals/html_head'
render_on :view_layouts_base_body_top, partial: 'additionals/body_top'
render_on :view_layouts_base_body_bottom, partial: 'additionals/body_bottom'
render_on(:view_account_login_bottom, partial: 'login_text')
render_on(:view_issues_context_menu_start, partial: 'additionals_closed_issues')
render_on(:view_issues_bulk_edit_details_bottom, partial: 'change_author_bulk')
render_on(:view_issues_form_details_bottom, partial: 'change_author')
render_on(:view_issues_new_top, partial: 'new_ticket_message')
render_on(:view_issues_sidebar_issues_bottom, partial: 'issues/additionals_sidebar')
render_on(:view_issues_sidebar_queries_bottom, partial: 'additionals/global_sidebar')
render_on(:view_projects_show_right, partial: 'project_overview')
render_on(:view_projects_show_sidebar_bottom, partial: 'additionals/global_sidebar')
render_on(:view_welcome_index_right, partial: 'overview_right')
render_on(:view_my_account_preferences, partial: 'users/autowatch_involved_issue')
render_on(:view_users_form_preferences, partial: 'users/autowatch_involved_issue')
render_on(:view_users_show_contextual, partial: 'users/additionals_contextual')
render_on :view_account_login_bottom, partial: 'login_text'
render_on :view_issues_context_menu_start, partial: 'additionals_closed_issues'
render_on :view_issues_bulk_edit_details_bottom, partial: 'change_author_bulk'
render_on :view_issues_form_details_bottom, partial: 'change_author'
render_on :view_issues_new_top, partial: 'new_ticket_message'
render_on :view_issues_sidebar_issues_bottom, partial: 'issues/additionals_sidebar_issues'
render_on :view_issues_sidebar_queries_bottom, partial: 'issues/additionals_sidebar_queries'
render_on :view_my_account_preferences, partial: 'users/autowatch_involved_issue'
render_on :view_users_form_preferences, partial: 'users/autowatch_involved_issue'
render_on :view_users_show_contextual, partial: 'users/additionals_contextual'
render_on :view_wiki_show_sidebar_bottom, partial: 'additionals_sidebar'
def helper_issues_show_detail_after_setting(context = {})
d = context[:detail]
return unless d.prop_key == 'author_id'
detail = context[:detail]
return unless detail.prop_key == 'author_id'
d[:value] = find_name_by_reflection('author', d.value)
d[:old_value] = find_name_by_reflection('author', d.old_value)
detail[:value] = find_name_by_reflection('author', detail.value) || detail.value
detail[:old_value] = find_name_by_reflection('author', detail.old_value) || detail.old_value
end
def view_layouts_base_content(context = {})
controller = context[:controller]
return if controller.nil?
controller_name = context[:controller].params[:controller]
action_name = context[:controller].params[:action]
return if controller_name == 'account' && action_name == 'login' ||
controller_name == 'my' ||
controller_name == 'account' && action_name == 'lost_password' ||
!Additionals.setting?(:add_go_to_top)
link_to l(:label_go_to_top), '#gototop', class: 'gototop'
end
end
end

View file

@ -1,16 +1,16 @@
module Additionals
module Patches
module AccessControlPatch
def self.included(base)
base.class_eval do
def self.available_project_modules
@available_project_modules = available_project_modules_all
.reject { |m| Additionals.settings[:disabled_modules].to_a.include?(m.to_s) }
end
extend ActiveSupport::Concern
def self.available_project_modules_all
@permissions.collect(&:project_module).uniq.compact
end
included do
def self.available_project_modules
@available_project_modules = available_project_modules_all
.reject { |m| Additionals.setting(:disabled_modules).to_a.include?(m.to_s) }
end
def self.available_project_modules_all
@permissions.collect(&:project_module).compact!.uniq
end
end
end

View file

@ -1,9 +1,25 @@
module Additionals
module Patches
module AccountControllerPatch
def self.included(base)
base.class_eval do
invisible_captcha only: [:register] if Additionals.setting?(:invisible_captcha)
extend ActiveSupport::Concern
included do
include InstanceMethods
invisible_captcha(only: [:register],
on_timestamp_spam: :timestamp_spam_check,
if: -> { Additionals.setting?(:invisible_captcha) })
end
module InstanceMethods
def timestamp_spam_check
# redmine uses same action for _GET and _POST
return unless request.post?
if respond_to?(:redirect_back)
redirect_back(fallback_location: home_url, flash: { error: InvisibleCaptcha.timestamp_error_message })
else
redirect_to :back, flash: { error: InvisibleCaptcha.timestamp_error_message }
end
end
end
end

View file

@ -0,0 +1,21 @@
module Additionals
module Patches
module ApplicationControllerPatch
extend ActiveSupport::Concern
included do
include InstanceMethods
before_action :enable_smileys
end
module InstanceMethods
def enable_smileys
return if Redmine::WikiFormatting::Textile::Formatter::RULES.include?(:inline_smileys) ||
!Additionals.setting?(:legacy_smiley_support)
Redmine::WikiFormatting::Textile::Formatter::RULES << :inline_smileys
end
end
end
end
end

View file

@ -0,0 +1,28 @@
module Additionals
module Patches
module AutoCompletesControllerPatch
def fontawesome
icons = AdditionalsFontAwesome.search_for_select(params[:q].to_s.strip,
params[:selected].to_s.strip)
icons.sort! { |x, y| x[:text] <=> y[:text] }
respond_to do |format|
format.js { render json: icons }
format.html { render json: icons }
end
end
def issue_assignee
assignee_classes = ['User']
assignee_classes << 'Group' if Setting.issue_group_assignment?
scope = Principal.where(type: assignee_classes).limit(100)
scope = scope.member_of(project) if @project.present?
scope = scope.distinct
@assignee = scope.active.visible.sorted.like(params[:q]).to_a
@assignee = @assignee.sort! { |x, y| x.name <=> y.name }
render layout: false, partial: 'issue_assignee'
end
end
end
end

View file

@ -1,17 +1,17 @@
module Additionals
module Patches
module FormatterMarkdownPatch
def self.included(base)
base.class_eval do
base.send(:include, Additionals::Formatter)
extend ActiveSupport::Concern
# Add a postprocess hook to redcarpet's html formatter
def postprocess(text)
if Additionals.setting?(:legacy_smiley_support)
render_inline_smileys(inline_emojify(text))
else
text
end
included do
include Additionals::Formatter
# Add a postprocess hook to redcarpet's html formatter
def postprocess(text)
if Additionals.setting?(:legacy_smiley_support)
render_inline_smileys(inline_emojify(text))
else
text
end
end
end

View file

@ -1,15 +1,13 @@
module Additionals
module Patches
module FormatterTextilePatch
def self.included(base)
base.class_eval do
base.send(:include, Additionals::Formatter)
# Add :inline_emojify to list of textile functions
if Additionals.setting?(:legacy_smiley_support)
Redmine::WikiFormatting::Textile::Formatter::RULES << :inline_emojify
Redmine::WikiFormatting::Textile::Formatter::RULES << :inline_smileys
end
end
extend ActiveSupport::Concern
included do
include Additionals::Formatter
# emojify are always enabled
Redmine::WikiFormatting::Textile::Formatter::RULES << :inline_emojify
end
end
end

View file

@ -1,21 +1,18 @@
module Additionals
module Patches
module FormattingHelperPatch
def self.included(base)
base.send(:include, InstanceMethods)
base.class_eval do
alias_method :heads_for_wiki_formatter_without_additionals, :heads_for_wiki_formatter
alias_method :heads_for_wiki_formatter, :heads_for_wiki_formatter_with_additionals
end
extend ActiveSupport::Concern
included do
prepend InstanceOverwriteMethods
end
module InstanceMethods
def heads_for_wiki_formatter_with_additionals
heads_for_wiki_formatter_without_additionals
module InstanceOverwriteMethods
def heads_for_wiki_formatter
super
return if @additionals_macro_list
@additionals_macro_list = AdditionalsMacro.all(filtered: Additionals.settings[:hidden_macros_in_toolbar].to_a,
@additionals_macro_list = AdditionalsMacro.all(filtered: Additionals.setting(:hidden_macros_in_toolbar).to_a,
only_names: true,
controller_only: controller_name)

View file

@ -1,24 +1,39 @@
module Additionals
module Patches
module IssuePatch
def self.included(base)
base.send(:include, InstanceMethods)
base.class_eval do
alias_method :editable_without_additionals?, :editable?
alias_method :editable?, :editable_with_additionals?
validate :validate_change_on_closed
validate :validate_timelog_required
validate :validate_open_sub_issues
validate :validate_current_user_status
before_validation :auto_assigned_to
before_save :change_status_with_assigned_to_change,
:autowatch_involved
extend ActiveSupport::Concern
safe_attributes 'author_id',
if: proc { |issue, user|
issue.new_record? && user.allowed_to?(:change_new_issue_author, issue.project) ||
issue.persisted? && user.allowed_to?(:edit_issue_author, issue.project)
}
included do
include InstanceMethods
alias_method :editable_without_additionals?, :editable?
alias_method :editable?, :editable_with_additionals?
validate :validate_change_on_closed
validate :validate_timelog_required
validate :validate_current_user_status
before_validation :auto_assigned_to
before_save :change_status_with_assigned_to_change,
:autowatch_involved
safe_attributes 'author_id',
if: proc { |issue, user|
issue.new_record? && user.allowed_to?(:change_new_issue_author, issue.project) ||
issue.persisted? && user.allowed_to?(:edit_issue_author, issue.project)
}
end
class_methods do
def join_issue_status(options = {})
sql = "JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{table_name}.status_id"
return sql unless options.key?(:is_closed)
sql << " AND #{IssueStatus.table_name}.is_closed ="
sql << if options[:is_closed]
" #{connection.quoted_true}"
else
" #{connection.quoted_false}"
end
sql
end
end
@ -48,23 +63,24 @@ module Additionals
add_autowatcher(User.current)
add_autowatcher(author) if (new_record? || author_id != author_id_was) && author != User.current
unless assigned_to_id.nil? || assigned_to_id == User.current.id
add_autowatcher(assigned_to) if new_record? || assigned_to_id != assigned_to_id_was
if !assigned_to_id.nil? && assigned_to_id != User.current.id && (new_record? || assigned_to_id != assigned_to_id_was)
add_autowatcher(assigned_to)
end
true
end
def log_time_allowed?(user = User.current)
!closed? || user.allowed_to?(:log_time_on_closed_issues, project)
!status_was.is_closed || user.allowed_to?(:log_time_on_closed_issues, project)
end
def editable_with_additionals?(user = User.current)
return false unless editable_without_additionals?(user)
return false unless editable_without_additionals? user
return true unless closed?
return true unless Additionals.setting?(:issue_freezed_with_close)
return true unless Additionals.setting? :issue_freezed_with_close
user.allowed_to?(:edit_closed_issues, project)
user.allowed_to? :edit_closed_issues, project
end
end
@ -85,16 +101,14 @@ module Additionals
end
def new_ticket_message
@new_ticket_message = ''
message = Additionals.settings[:new_ticket_message]
@new_ticket_message << message if message.present?
@new_ticket_message ||= Additionals.setting(:new_ticket_message).presence || ''
end
def status_x_affected?(new_status_id)
return false unless Additionals.setting?(:issue_current_user_status)
return false if Additionals.settings[:issue_assign_to_x].blank?
return false if Additionals.setting(:issue_assign_to_x).blank?
if Additionals.settings[:issue_assign_to_x].include?(new_status_id.to_s)
if Additionals.setting(:issue_assign_to_x).include?(new_status_id.to_s)
true
else
false
@ -105,18 +119,18 @@ module Additionals
def auto_assigned_to
return if !Additionals.setting?(:issue_auto_assign) ||
Additionals.settings[:issue_auto_assign_status].blank? ||
Additionals.settings[:issue_auto_assign_role].blank? ||
Additionals.setting(:issue_auto_assign_status).blank? ||
Additionals.setting(:issue_auto_assign_role).blank? ||
assigned_to_id.present?
return unless Additionals.settings[:issue_auto_assign_status].include?(status_id.to_s)
return unless Additionals.setting(:issue_auto_assign_status).include?(status_id.to_s)
self.assigned_to_id = auto_assigned_to_user
true
end
def auto_assigned_to_user
manager_role = Role.builtin.find_by(id: Additionals.settings[:issue_auto_assign_role])
manager_role = Role.builtin.find_by(id: Additionals.setting(:issue_auto_assign_role))
groups = autoassign_get_group_list
return groups[manager_role].first.id unless groups.nil? || groups[manager_role].blank?
@ -127,10 +141,10 @@ module Additionals
def timelog_required?(check_status_id)
usr = User.current
return false if !Additionals.setting?(:issue_timelog_required) ||
Additionals.settings[:issue_timelog_required_tracker].blank? ||
Additionals.settings[:issue_timelog_required_tracker].exclude?(tracker_id.to_s) ||
Additionals.settings[:issue_timelog_required_status].blank? ||
Additionals.settings[:issue_timelog_required_status].exclude?(check_status_id.to_s) ||
Additionals.setting(:issue_timelog_required_tracker).blank? ||
Additionals.setting(:issue_timelog_required_tracker).exclude?(tracker_id.to_s) ||
Additionals.setting(:issue_timelog_required_status).blank? ||
Additionals.setting(:issue_timelog_required_status).exclude?(check_status_id.to_s) ||
!usr.allowed_to?(:log_time, project) ||
usr.allowed_to?(:issue_timelog_never_required, project) ||
time_entries.present?
@ -145,23 +159,15 @@ module Additionals
end
def validate_change_on_closed
return true if !closed? ||
new_record? ||
!Additionals.setting?(:issue_freezed_with_close) ||
return true if new_record? ||
!status_was.is_closed ||
!changed? ||
!Additionals.setting?(:issue_freezed_with_close) ||
User.current.allowed_to?(:edit_closed_issues, project)
errors.add :base, :issue_changes_not_allowed
end
def validate_open_sub_issues
return true unless Additionals.setting?(:issue_close_with_open_children)
errors.add :base, :issue_cannot_close_with_open_children if subject.present? &&
closing? &&
descendants.find { |d| !d.closed? }
end
def validate_current_user_status
if (assigned_to_id_changed? || status_id_changed?) &&
status_x_affected?(status_id) &&
@ -174,13 +180,13 @@ module Additionals
def change_status_with_assigned_to_change
return true if !Additionals.setting?(:issue_status_change) ||
Additionals.settings[:issue_status_x].blank? ||
Additionals.settings[:issue_status_y].blank?
Additionals.setting(:issue_status_x).blank? ||
Additionals.setting(:issue_status_y).blank?
if !assigned_to_id_changed? &&
status_id_changed? &&
(Additionals.settings[:issue_status_x].include? status_id_was.to_s) &&
Additionals.settings[:issue_status_y].to_i == status_id
(Additionals.setting(:issue_status_x).include? status_id_was.to_s) &&
Additionals.setting(:issue_status_y).to_i == status_id
self.assigned_to = author
end
end

View file

@ -1,12 +1,13 @@
module Additionals
module Patches
module IssuePriorityPatch
def self.included(base)
base.send(:include, InstanceMethods)
base.class_eval do
alias_method :css_classes_without_additionals, :css_classes
alias_method :css_classes, :css_classes_with_additionals
end
extend ActiveSupport::Concern
included do
include InstanceMethods
alias_method :css_classes_without_additionals, :css_classes
alias_method :css_classes, :css_classes_with_additionals
end
module InstanceMethods

View file

@ -1,45 +1,45 @@
module Additionals
module Patches
module PrincipalPatch
def self.included(base)
base.class_eval do
# TODO: find better solution, which not requires overwrite visible
# to filter out hide role members
scope :visible, lambda { |*args|
user = args.first || User.current
extend ActiveSupport::Concern
if user.admin?
all
included do
# TODO: find better solution, which not requires overwrite visible
# to filter out hide role members
scope :visible, lambda { |*args|
user = args.first || User.current
if user.admin?
all
else
view_all_active = if user.memberships.to_a.any?
user.memberships.any? { |m| m.roles.any? { |r| r.users_visibility == 'all' } }
else
user.builtin_role.users_visibility == 'all'
end
if view_all_active
active
else
view_all_active = if user.memberships.to_a.any?
user.memberships.any? { |m| m.roles.any? { |r| r.users_visibility == 'all' } }
else
user.builtin_role.users_visibility == 'all'
end
# self and members of visible projects
scope = if user.allowed_to?(:show_hidden_roles_in_memberbox, nil, global: true)
active.where("#{table_name}.id = ? OR #{table_name}.id IN (SELECT user_id " \
"FROM #{Member.table_name} WHERE project_id IN (?))",
user.id, user.visible_project_ids)
else
active.where("#{table_name}.id = ? OR #{table_name}.id IN (SELECT user_id " \
"FROM #{Member.table_name} JOIN #{MemberRole.table_name} " \
" ON #{Member.table_name}.id = #{MemberRole.table_name}.member_id" \
" JOIN #{Role.table_name} " \
" ON #{Role.table_name}.id = #{MemberRole.table_name}.role_id" \
" WHERE project_id IN (?) AND #{Role.table_name}.hide = ?)",
user.id, user.visible_project_ids, false)
end
if view_all_active
active
else
# self and members of visible projects
scope = if user.allowed_to?(:show_hidden_roles_in_memberbox, nil, global: true)
active.where("#{table_name}.id = ? OR #{table_name}.id IN (SELECT user_id " \
"FROM #{Member.table_name} WHERE project_id IN (?))",
user.id, user.visible_project_ids)
else
active.where("#{table_name}.id = ? OR #{table_name}.id IN (SELECT user_id " \
"FROM #{Member.table_name} JOIN #{MemberRole.table_name} " \
" ON #{Member.table_name}.id = #{MemberRole.table_name}.member_id" \
" JOIN #{Role.table_name} " \
" ON #{Role.table_name}.id = #{MemberRole.table_name}.role_id" \
" WHERE project_id IN (?) AND #{Role.table_name}.hide = ?)",
user.id, user.visible_project_ids, false)
end
scope
end
scope
end
}
end
end
}
end
end
end

View file

@ -1,13 +1,50 @@
module Additionals
module Patches
module ProjectPatch
def self.included(base)
base.send(:prepend, InstancOverwriteMethods)
extend ActiveSupport::Concern
included do
prepend InstanceOverwriteMethods
include InstanceMethods
has_many :dashboards, dependent: :destroy
end
module InstancOverwriteMethods
module InstanceOverwriteMethods
# this change take care of hidden roles and performance issues (includes for hrm, if installed)
def users_by_role
roles_with_users = super
if Redmine::VERSION.to_s >= '4.2'
includes = Redmine::Plugin.installed?('redmine_hrm') ? [:roles, { principal: :hrm_user_type }] : %i[roles principal]
memberships.includes(includes).each_with_object({}) do |m, h|
m.roles.each do |r|
next if r.hide && !User.current.allowed_to?(:show_hidden_roles_in_memberbox, project)
h[r] ||= []
h[r] << m.principal
end
h
end
else
includes = Redmine::Plugin.installed?('redmine_hrm') ? [:roles, { user: :hrm_user_type }] : %i[roles user]
members.includes(includes).each_with_object({}) do |m, h|
m.roles.each do |r|
next if r.hide && !User.current.allowed_to?(:show_hidden_roles_in_memberbox, project)
h[r] ||= []
h[r] << m.user
end
h
end
end
end
def users_by_role_old
roles_with_users = if Redmine::VERSION.to_s >= '4.2'
principals_by_role
else
super
end
roles_with_users.each do |role_with_users|
role = role_with_users.first
next unless role.hide
@ -18,6 +55,30 @@ module Additionals
roles_with_users
end
end
module InstanceMethods
def visible_principals
query = ::Query.new(project: self, name: '_')
query&.principals
end
def visible_users
query = ::Query.new(project: self, name: '_')
query&.users
end
# assignable_users result depends on Setting.issue_group_assignment?
# this result is not depending on issue settings
def assignable_users_and_groups
Principal.active
.joins(members: :roles)
.where(type: %w[User Group],
members: { project_id: id },
roles: { assignable: true })
.distinct
.sorted
end
end
end
end
end

View file

@ -0,0 +1,47 @@
require_dependency 'projects_controller'
module Additionals
module Patches
module ProjectsControllerPatch
extend ActiveSupport::Concern
included do
include InstanceMethods
before_action :find_dashboard, only: %i[show]
helper :additionals_routes
helper :issues
helper :queries
helper :additionals_queries
helper :additionals_projects
helper :dashboards
include DashboardsHelper
end
module InstanceMethods
private
def find_dashboard
if params[:dashboard_id].present?
begin
@dashboard = Dashboard.project_only.find(params[:dashboard_id])
raise ::Unauthorized unless @dashboard.visible?
raise ::Unauthorized unless @dashboard.project.nil? || @dashboard.project == @project
rescue ActiveRecord::RecordNotFound
return render_404
end
else
@dashboard = Dashboard.default DashboardContentProject::TYPE_NAME, @project
end
@dashboard.content_project = @project
resently_used_dashboard_save @dashboard, @project
@can_edit = @dashboard&.editable?
@dashboard_sidebar = dashboard_sidebar? @dashboard, params
end
end
end
end
end

View file

@ -3,17 +3,11 @@ require_dependency 'query'
module Additionals
module Patches
module QueryFilterPatch
def self.included(base)
base.send(:include, InstanceMethods)
end
unless method_defined? :[]=
def []=(key, value)
return unless key == :values
module InstanceMethods
unless method_defined? :[]=
def []=(key, value)
return unless key == :values
@value = @options[:values] = value
end
@value = @options[:values] = value
end
end
end

View file

@ -0,0 +1,24 @@
module Additionals
module Patches
module ReportsControllerPatch
extend ActiveSupport::Concern
included do
prepend InstanceOverwriteMethods
end
module InstanceOverwriteMethods
def issue_report_details
super
return if @rows.nil?
if Setting.issue_group_assignment? && params[:detail] == 'assigned_to'
@rows = @project.visible_principals
elsif %w[assigned_to author].include? params[:detail]
@rows = @project.visible_users
end
end
end
end
end
end

View file

@ -1,10 +1,10 @@
module Additionals
module Patches
module RolePatch
def self.included(base)
base.class_eval do
safe_attributes 'hide'
end
extend ActiveSupport::Concern
included do
safe_attributes 'hide'
end
end
end

View file

@ -1,19 +1,21 @@
module Additionals
module Patches
module TimeEntryPatch
def self.included(base)
base.send(:include, InstanceMethods)
base.class_eval do
alias_method :editable_by_without_additionals?, :editable_by?
alias_method :editable_by?, :editable_by_with_additionals?
validate :validate_issue_allowed
end
extend ActiveSupport::Concern
included do
include InstanceMethods
alias_method :editable_by_without_additionals?, :editable_by?
alias_method :editable_by?, :editable_by_with_additionals?
validate :validate_issue_allowed
end
module InstanceMethods
def validate_issue_allowed
return unless issue_id && issue
return if Setting.commit_logtime_enabled? && (issue.updated_on + 3.seconds) > Additionals.now_with_user_time_zone
# NOTE: do not use user time zone here, because issue do not use it
return if Setting.commit_logtime_enabled? && (issue.updated_on + 5.seconds) > Time.zone.now
errors.add(:issue_id, :issue_log_time_not_allowed) unless issue.log_time_allowed?
end

View file

@ -1,14 +1,56 @@
module Additionals
module Patches
module UserPatch
def self.included(base)
base.send(:include, InstanceMethods)
extend ActiveSupport::Concern
included do
include InstanceMethods
end
class_methods do
def admin_column_field
Redmine::Plugin.installed?('redmine_sudo') ? 'sudoer' : 'admin'
end
# NOTE: this is a better (performance related) solution as:
# authors = users.to_a.select { |u| u.allowed_to? permission, project, global: project.nil? }
def with_permission(permission, project = nil)
# Clear cache for debuging performance issue
# ActiveRecord::Base.connection.clear_query_cache
role_ids = Role.builtin(false).select { |p| p.permissions.include? permission }
role_ids.map!(&:id)
admin_ids = User.visible.active.where(admin: true).ids
member_scope = Member.joins(:member_roles, :project)
.where(projects: { status: Project::STATUS_ACTIVE },
user_id: User.all,
member_roles: { role_id: role_ids })
.select(:user_id)
.distinct
if project.nil?
# user_ids = member_scope.pluck(:user_id) | admin_ids
# where(id: user_ids)
where(id: member_scope).or(where(id: admin_ids))
else
# user_ids = member_scope.where(project_id: project).pluck(:user_id)
# where(id: user_ids).or(where(id: admin_ids))
where(id: member_scope.where(project_id: project)).or(where(id: admin_ids))
end
end
end
module InstanceMethods
def can_be_admin?
@can_be_admin ||= Redmine::Plugin.installed?('redmine_sudo') ? (admin || sudoer) : admin
end
def issues_assignable?(project = nil)
scope = Principal.joins(members: :roles)
.where(users: { id: id }, roles: { assignable: true })
.where(users: { id: id },
roles: { assignable: true })
scope = scope.where(members: { project_id: project.id }) if project
scope.exists?
end

View file

@ -1,9 +1,33 @@
module Additionals
module Patches
module UserPreferencePatch
def self.included(base)
base.class_eval do
safe_attributes 'autowatch_involved_issue'
extend ActiveSupport::Concern
included do
include InstanceMethods
safe_attributes 'autowatch_involved_issue', 'recently_used_dashboards'
end
module InstanceMethods
def recently_used_dashboards
self[:recently_used_dashboards]
end
def recently_used_dashboard(dashboard_type, project = nil)
r = self[:recently_used_dashboards] ||= {}
r = {} unless r.is_a? Hash
return unless r.is_a?(Hash) && r.key?(dashboard_type)
if dashboard_type == DashboardContentProject::TYPE_NAME
r[dashboard_type][project.id]
else
r[dashboard_type]
end
end
def recently_used_dashboards=(value)
self[:recently_used_dashboards] = value
end
end
end

View file

@ -0,0 +1,44 @@
require_dependency 'welcome_controller'
module Additionals
module Patches
module WelcomeControllerPatch
extend ActiveSupport::Concern
included do
include InstanceMethods
before_action :find_dashboard, only: %i[index]
helper :additionals_routes
helper :issues
helper :queries
helper :additionals_queries
helper :dashboards
include DashboardsHelper
end
module InstanceMethods
private
def find_dashboard
if params[:dashboard_id].present?
begin
@dashboard = Dashboard.welcome_only.find(params[:dashboard_id])
raise ::Unauthorized unless @dashboard.visible?
rescue ActiveRecord::RecordNotFound
return render_404
end
else
@dashboard = Dashboard.default DashboardContentWelcome::TYPE_NAME
end
resently_used_dashboard_save @dashboard
@can_edit = @dashboard&.editable?
@dashboard_sidebar = dashboard_sidebar? @dashboard, params
end
end
end
end
end

View file

@ -1,65 +0,0 @@
require_dependency 'wiki_controller'
module Additionals
module Patches
module WikiControllerPatch
def self.included(base)
base.send(:include, InstanceMethods)
base.class_eval do
alias_method :respond_to_without_additionals, :respond_to
alias_method :respond_to, :respond_to_with_additionals
end
end
end
module InstanceMethods
def respond_to_with_additionals(&block)
if @project && @content
if @_action_name == 'show'
additionals_include_header
additionals_include_footer
end
end
respond_to_without_additionals(&block)
end
private
def additionals_include_header
wiki_header = '' + Additionals.settings[:global_wiki_header].to_s
return if wiki_header.empty?
if Object.const_defined?('WikiExtensionsUtil') && WikiExtensionsUtil.is_enabled?(@project)
header = @wiki.find_page('Header')
return if header
end
text = "\n"
text << '<div id="wiki_extentions_header">'
text << "\n\n"
text << wiki_header
text << "\n\n</div>"
text << "\n\n"
text << @content.text
@content.text = text
end
def additionals_include_footer
wiki_footer = '' + Additionals.settings[:global_wiki_footer].to_s
return if wiki_footer.empty?
if Object.const_defined?('WikiExtensionsUtil') && WikiExtensionsUtil.is_enabled?(@project)
footer = @wiki.find_page('Footer')
return if footer
end
text = @content.text
text << "\n\n"
text << '<div id="wiki_extentions_footer">'
text << "\n\n"
text << wiki_footer
text << "\n\n</div>"
end
end
end
end

View file

@ -4,25 +4,25 @@ module Additionals
module Patches
# Patch wiki to include sidebar
module WikiPatch
def self.included(base)
base.send(:include, InstanceMethodsForAdditionalsWiki)
base.class_eval do
alias_method :sidebar_without_additionals, :sidebar
alias_method :sidebar, :sidebar_with_additionals
end
end
end
extend ActiveSupport::Concern
# Instance methodes for Wiki
module InstanceMethodsForAdditionalsWiki
def sidebar_with_additionals
@sidebar ||= find_page('Sidebar', with_redirect: false)
if @sidebar && @sidebar.content
sidebar_without_additionals
else
wiki_sidebar = Additionals.settings[:global_wiki_sidebar].to_s
@sidebar ||= find_page(project.wiki.start_page, with_redirect: false)
@sidebar.content.text = wiki_sidebar if wiki_sidebar != '' && @sidebar.try(:content)
included do
include InstanceMethods
alias_method :sidebar_without_additionals, :sidebar
alias_method :sidebar, :sidebar_with_additionals
end
module InstanceMethods
def sidebar_with_additionals
@sidebar ||= find_page('Sidebar', with_redirect: false)
if @sidebar&.content
sidebar_without_additionals
else
wiki_sidebar = Additionals.setting(:global_wiki_sidebar).to_s
@sidebar ||= find_page(project.wiki.start_page, with_redirect: false)
@sidebar.content.text = wiki_sidebar if wiki_sidebar != '' && @sidebar.try(:content)
end
end
end
end

View file

@ -0,0 +1,14 @@
# asciinema wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc 'asciinema embed'
macro :asciinema do |_obj, args|
raise 'The correct usage is {{asciinema(<cast_id>)}}' if args.empty?
javascript_tag(nil, id: "asciicast-#{args[0]}", src: "//asciinema.org/a/#{args[0]}.js", async: true)
end
end
end
end

View file

@ -1,70 +0,0 @@
# Calendar wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc <<-DESCRIPTION
Display calendar (only works on wiki pages)
Examples:
{{calendar}} show calendar for current date
{{calendar(year=2014,month=6)}} show calendar for Juni in year 2014
{{calendar(show_weeks=true)}} show calendar with week numbers
{{calendar(select=2015-07-12 2015-07-31, show_weeks=true)}} preselect dates and show week numbers
{{calendar(select=2016-03-13:2016-03-27)}} preselect dates between 2016/3/13 and 2016/3/27
DESCRIPTION
macro :calendar do |_obj, args|
raise 'Only works on wiki page' unless controller_name == 'wiki' && %w[show preview].include?(action_name)
_args, options = extract_macro_options(args, :show_weeks, :year, :month, :select)
options[:show_weeks] = 'false' if options[:show_weeks].blank?
options[:year] = Additionals.now_with_user_time_zone.year.to_s if options[:year].blank?
options[:month] = Additionals.now_with_user_time_zone.month.to_s if options[:month].blank?
options[:month] = options[:month].to_i - 1
selected = ''
selected = Additionals.convert_string2date(options[:select]) if options[:select].present?
locale = User.current.language.presence || ::I18n.locale
# not more then 30 calendars per page are expected
id = (0..30).to_a.sort { rand - 0.5 } [1]
render partial: 'wiki/calendar_macros',
formats: [:html],
locals: { options: options, locale: locale, id: id, selected: selected }
end
end
end
def self.convert_string2date(string)
selected = if string.include? ':'
convert_string2period(string)
else
convert_string2dates(string)
end
selected.join(', ')
end
def self.convert_string2period(string)
s = string.split ':'
raise 'missing date' if s[0].blank? || s[1].blank?
tstart = Date.strptime(s[0], '%Y-%m-%d')
raise 'invalid start date' if tstart.nil?
tend = Date.strptime(s[1], '%Y-%m-%d')
raise 'invalid start date' if tend.nil?
(tstart..tend).map { |date| "new Date(#{date.year},#{date.month - 1},#{date.mday})" }
end
def self.convert_string2dates(string)
selected = []
s = string.split
s.each do |d|
con = Date.strptime(d, '%Y-%m-%d')
selected << "new Date(#{con.year},#{con.month - 1},#{con.mday})" unless con.nil?
end
selected
end
end

View file

@ -21,68 +21,67 @@ module Additionals
widget_type = 'chart'
else
widget_type = options[:type]
options.delete(:type)
options.delete :type
end
base_url = 'https://widgets.cryptocompare.com/'
case widget_type
when 'chart'
url = base_url + 'serve/v2/coin/chart'
url = 'serve/v2/coin/chart'
when 'news'
options[:feedType] = 'CoinTelegraph' if options[:feedType].blank?
url = base_url + 'serve/v1/coin/feed'
url = 'serve/v1/coin/feed'
when 'list'
options[:tsyms] = Additionals.crypto_default(options, :tsyms, 'EUR,USD')
options.delete(:tsym)
url = base_url + 'serve/v1/coin/list'
options[:tsyms] = Additionals.crypto_default options, :tsyms, 'EUR,USD'
options.delete :tsym
url = 'serve/v1/coin/list'
when 'titles'
options[:tsyms] = Additionals.crypto_default(options, :tsyms, 'EUR,USD')
options.delete(:tsym)
url = base_url + 'serve/v1/coin/tiles'
options[:tsyms] = Additionals.crypto_default options, :tsyms, 'EUR,USD'
options.delete :tsym
url = 'serve/v1/coin/tiles'
when 'tabbed'
options[:fsyms] = Additionals.crypto_default(options, :fsyms, 'BTC,ETH,LTC')
options[:tsyms] = Additionals.crypto_default(options, :tsyms, 'EUR,USD')
options.delete(:fsym)
options.delete(:tsym)
url = base_url + 'serve/v1/coin/multi'
options[:fsyms] = Additionals.crypto_default options, :fsyms, 'BTC,ETH,LTC'
options[:tsyms] = Additionals.crypto_default options, :tsyms, 'EUR,USD'
options.delete :fsym
options.delete :tsym
url = 'serve/v1/coin/multi'
when 'header', 'header_v1'
options[:tsyms] = Additionals.crypto_default(options, :tsyms, 'EUR,USD')
options.delete(:tsym)
url = base_url + 'serve/v1/coin/header'
options[:tsyms] = Additionals.crypto_default options, :tsyms, 'EUR,USD'
options.delete :tsym
url = 'serve/v1/coin/header'
when 'header_v2'
options[:fsyms] = Additionals.crypto_default(options, :fsyms, 'BTC,ETH,LTC')
options[:tsyms] = Additionals.crypto_default(options, :tsyms, 'EUR,USD')
options.delete(:fsym)
options.delete(:tsym)
url = base_url + 'serve/v2/coin/header'
options[:fsyms] = Additionals.crypto_default options, :fsyms, 'BTC,ETH,LTC'
options[:tsyms] = Additionals.crypto_default options, :tsyms, 'EUR,USD'
options.delete :fsym
options.delete :tsym
url = 'serve/v2/coin/header'
when 'header_v3'
options[:fsyms] = Additionals.crypto_default(options, :fsyms, 'BTC,ETH,LTC')
options[:tsyms] = Additionals.crypto_default(options, :tsyms, 'EUR')
options.delete(:fsym)
options.delete(:tsym)
url = base_url + 'serve/v3/coin/header'
options[:fsyms] = Additionals.crypto_default options, :fsyms, 'BTC,ETH,LTC'
options[:tsyms] = Additionals.crypto_default options, :tsyms, 'EUR'
options.delete :fsym
options.delete :tsym
url = 'serve/v3/coin/header'
when 'summary'
options[:tsyms] = Additionals.crypto_default(options, :tsyms, 'EUR,USD')
options.delete(:tsym)
url = base_url + 'serve/v1/coin/summary'
options[:tsyms] = Additionals.crypto_default options, :tsyms, 'EUR,USD'
options.delete :tsym
url = 'serve/v1/coin/summary'
when 'historical'
url = base_url + 'serve/v1/coin/histo_week'
url = 'serve/v1/coin/histo_week'
when 'converter'
options[:tsyms] = Additionals.crypto_default(options, :tsyms, 'EUR,USD')
options.delete(:tsym)
url = base_url + 'serve/v1/coin/converter'
options[:tsyms] = Additionals.crypto_default options, :tsyms, 'EUR,USD'
options.delete :tsym
url = 'serve/v1/coin/converter'
when 'advanced'
options[:tsyms] = Additionals.crypto_default(options, :tsyms, 'EUR,USD')
options.delete(:tsym)
url = base_url + 'serve/v3/coin/chart'
options[:tsyms] = Additionals.crypto_default options, :tsyms, 'EUR,USD'
options.delete :tsym
url = 'serve/v3/coin/chart'
else
raise 'type is not supported'
end
params = options.map { |k, v| "#{k}=#{v}" }.join('&')
render partial: 'wiki/cryptocompare',
formats: [:html],
locals: { url: url + '?' + options.map { |k, v| "#{k}=#{v}" }.join('&') }
locals: { url: "https://widgets.cryptocompare.com/#{url}?#{params}" }
end
end
end

View file

@ -63,7 +63,7 @@ module Additionals
format_date(type.to_date)
end
content_tag(:span, date_result, class: 'current-date')
tag.span date_result, class: 'current-date'
end
end
end

View file

@ -37,7 +37,7 @@ module Additionals
args, options = extract_macro_options(args, :class, :title, :text, :size, :color, :link)
raise 'The correct usage is {{fa(<ICON>, class=CLASS, title=TITLE, text=TEXT, size=SIZE, color=COLOR)}}' if args.empty?
values = args[0].split('_')
values = args[0].split '_'
classes = []
if values.count == 2
@ -55,14 +55,14 @@ module Additionals
content_options[:title] = options[:title] if options[:title].present?
content_options[:style] = "color: #{options[:color]}" if options[:color].present?
text = options[:text].present? ? ' ' + options[:text] : ''
text = options[:text].present? ? " #{options[:text]}" : ''
if options[:link].present?
content_tag(:a, href: options[:link]) do
content_tag(:i, text, content_options)
tag.a href: options[:link] do
tag.i text, content_options
end
else
content_tag(:i, text, content_options)
tag.i text, content_options
end
end
end

View file

@ -60,7 +60,7 @@ module Additionals
:waypoints,
:zoom)
raise 'Missing Google Maps Embed API Key. See documentation for more info.' if Additionals.settings[:google_maps_api_key].blank?
raise 'Missing Google Maps Embed API Key. See documentation for more info.' if Additionals.setting(:google_maps_api_key).blank?
width = options[:width].presence || 620
height = options[:height].presence || 350
@ -70,11 +70,11 @@ module Additionals
raise 'The correct usage is {{gmap([q=QUERY, mode=MODE, widths=x, height=y])}}'
end
src = "https://www.google.com/maps/embed/v1/#{mode}?key=" + Additionals.settings[:google_maps_api_key]
src = "https://www.google.com/maps/embed/v1/#{mode}?key=" + Additionals.setting(:google_maps_api_key)
if options[:q].present?
src << '&q=' + ERB::Util.url_encode(options[:q])
src << "&q=#{ERB::Util.url_encode(options[:q])}"
elsif mode == 'search'
src << '&q=' + ERB::Util.url_encode(args[0])
src << "&q=#{ERB::Util.url_encode(args[0])}"
end
src_options.each do |key|
@ -82,7 +82,7 @@ module Additionals
end
src << "&#{mode}=" + ERB::Util.url_encode(options[:way_mode]) if options[:way_mode].present?
content_tag(:iframe, '', width: width, height: height, src: src, frameborder: 0, allowfullscreen: 'true')
tag.iframe width: width, height: height, src: src, frameborder: 0, allowfullscreen: 'true'
end
end
end

View file

@ -0,0 +1,54 @@
# Google docs wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc <<-DESCRIPTION
Google docs macro to include Google documents.
Syntax:
{{google_docs(<link> [, width=100%, height=485, edit_link=LINK)}}
Examples:
{{google_docs(https://docs.google.com/spreadsheets/d/e/2PACX-1vQL__Vgu0Y0f-P__GJ9kpUmQ0S-HG56ni_b-x4WpWxzGIGXh3X6A587SeqvJDpH42rDmWVZoUN07VGE/pubhtml)}
{{google_docs(https://docs.google.com/spreadsheets/d/e/2PACX-1vQL__Vgu0Y0f-P__GJ9kpUmQ0S-HG56ni_b-x4WpWxzGIGXh3X6A587SeqvJDpH42rDmWVZoUN07VGE/pubhtml, width=514, height=422)}
DESCRIPTION
macro :google_docs do |_obj, args|
args, options = extract_macro_options(args, :width, :height, :edit_link)
width = options[:width].presence || '100%'
height = options[:height].presence || 485
raise 'The correct usage is {{google_docs(<link>[, width=x, height=y, edit_link=LINK])}}' if args.empty?
v = args[0]
raise '<link> is not a Google document.' unless v.start_with? 'https://docs.google.com/'
src = v.dup
unless src.include? '?'
src << if src.include?('edit')
'?rm=minimal'
else
'?widget=true&headers=false'
end
end
s = []
s << tag.iframe(width: width, height: height, src: src, frameborder: 0, allowfullscreen: 'true')
if options[:edit_link].present?
raise '<edit_link> is not a Google document.' unless options[:edit_link].start_with? 'https://docs.google.com/'
s << tag.br
s << link_to(font_awesome_icon('fab_google-drive', post_text: :label_open_in_google_docs),
options[:edit_link],
class: 'external')
end
safe_join s
end
end
end
end

View file

@ -28,17 +28,15 @@ module Additionals
src = args[0]
if Additionals.valid_iframe_url?(src)
s = [content_tag(:iframe,
'',
width: width,
height: height,
src: src,
frameborder: 0,
allowfullscreen: 'true')]
s = [tag.iframe(width: width,
height: height,
src: src,
frameborder: 0,
allowfullscreen: 'true')]
if !options[:with_link].nil? && Additionals.true?(options[:with_link])
s << link_to(l(:label_open_in_new_windows), src, class: 'external')
end
safe_join(s)
safe_join s
elsif Setting.protocol == 'https'
raise 'Invalid url provided to iframe (only full URLs with protocol HTTPS are accepted)'
else

View file

@ -31,9 +31,9 @@ module Additionals
return unless page
content_tag(:span,
l(:label_updated_time, time_tag(page.updated_on)).html_safe,
class: 'last-updated-at')
# TODO: find solution for time_tag without to use html_safe
tag.span(l(:label_updated_time, time_tag(page.updated_on)).html_safe, # rubocop:disable Rails/OutputSafety
class: 'last-updated-at')
end
end
end

View file

@ -10,9 +10,8 @@ module Additionals
macro :last_updated_by do |obj, args|
raise 'The correct usage is {{last_updated_by}}' unless args.empty?
content_tag(:span,
safe_join([avatar(obj.author, size: 14), ' ', link_to_user(obj.author)]),
class: 'last-updated-by')
tag.span safe_join([avatar(obj.author, size: 14), ' ', link_to_user(obj.author)]),
class: 'last-updated-by'
end
end
end

View file

@ -7,7 +7,7 @@ module Additionals
Syntax:
{{members([PROJECT_NAME, title=My members list, role=ROLE)]}}
{{members([PROJECT_NAME, title=My members list, role=ROLE, with_sum=BOOL)]}}
PROJECT_NAME can be project identifier, project name or project id
@ -16,6 +16,9 @@ module Additionals
{{members}}
...List all members for all projects (with the current user permission)
{{members(with_sum=true)}}
...List all members for all projects and show title with amount of members
{{members(the-identifier)}}
...A box showing all members for the project with the identifier of 'the-identifier'
@ -29,7 +32,7 @@ module Additionals
DESCRIPTION
macro :members do |_obj, args|
args, options = extract_macro_options(args, :role, :title)
args, options = extract_macro_options(args, :role, :title, :with_sum)
project_id = args[0]
user_roles = []
@ -42,28 +45,33 @@ module Additionals
project ||= Project.visible.find_by(name: project_id)
return if project.nil?
raw_users = User.active
.where(["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id=(?))", project.id])
.sorted
return if raw_users.nil?
principals = project.visible_users
return if principals.nil?
users = []
raw_users.each do |user|
user_roles[user.id] = user.roles_for_project(project)
users << user if options[:role].blank? || Additionals.check_role_matches(user_roles[user.id], options[:role])
principals.each do |principal|
next unless principal.type == 'User'
user_roles[principal.id] = principal.roles_for_project(project)
users << principal if options[:role].blank? || Additionals.check_role_matches(user_roles[principal.id], options[:role])
end
else
project_ids = Project.visible.collect(&:id)
return unless project_ids.any?
# members of the user's projects
users = User.active
.where(["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", project_ids])
users = User.visible
.where(type: 'User')
.active
.sorted
end
list_title = if options[:with_sum]
list_title = options[:title].presence || l(:label_member_plural)
list_title + " (#{users.count})"
else
options[:title]
end
render partial: 'wiki/user_macros', locals: { users: users,
user_roles: user_roles,
list_title: options[:title] }
list_title: list_title }
end
end
end

View file

@ -69,7 +69,7 @@ module Additionals
src << Additionals.meteoblue_flag(options, :spot, true)
src << Additionals.meteoblue_flag(options, :pressure, false)
content_tag(:iframe, '', width: width, height: height, src: src, frameborder: 0)
tag.iframe width: width, height: height, src: src, frameborder: 0
end
end
end

View file

@ -28,25 +28,26 @@ module Additionals
return if days < 1
pages = WikiPage
.includes(:content)
.where(["#{WikiPage.table_name}.wiki_id = ? AND #{WikiContent.table_name}.updated_on > ?",
page.wiki_id, User.current.today - days])
.order("#{WikiContent.table_name}.updated_on desc")
o = ''
pages = WikiPage.joins(:content)
.where(wiki_id: page.wiki_id)
.where("#{WikiContent.table_name}.updated_on > ?", User.current.today - days)
.order("#{WikiContent.table_name}.updated_on desc")
s = []
date = nil
pages.each do |page_raw|
content = page_raw.content
updated_on = Date.new(content.updated_on.year, content.updated_on.month, content.updated_on.day)
if date != updated_on
date = updated_on
o << '<b>' + format_date(date) + '</b><br/>'
s << tag.strong(format_date(date))
s << tag.br
end
o << link_to(content.page.pretty_title,
s << link_to(content.page.pretty_title,
controller: 'wiki', action: 'show', project_id: content.page.project, id: content.page.title)
o << '<br/>'
s << tag.br
end
content_tag('div', o.html_safe, class: 'recently-updated')
tag.div safe_join(s), class: 'recently-updated'
end
end
end

View file

@ -14,21 +14,21 @@ module Additionals
case name[0..1]
when 'r/'
link_to(font_awesome_icon('fab_reddit', post_text: name),
link_to font_awesome_icon('fab_reddit', post_text: name),
"https://www.reddit.com/#{name}",
class: 'external reddit',
title: l(:label_reddit_subject))
title: l(:label_reddit_subject)
when 'u/'
link_to(font_awesome_icon('fab_reddit-square', post_text: name),
link_to font_awesome_icon('fab_reddit-square', post_text: name),
"https://www.reddit.com/username/#{name[2..-1]}",
class: 'external reddit',
title: l(:label_reddit_user_account))
title: l(:label_reddit_user_account)
else
name = 'r/' + name
link_to(font_awesome_icon('fab_reddit', post_text: name),
name = "r/#{name}"
link_to font_awesome_icon('fab_reddit', post_text: name),
"https://www.reddit.com/#{name}",
class: 'external reddit',
title: l(:label_reddit_subject))
title: l(:label_reddit_subject)
end
end
end

View file

@ -20,7 +20,7 @@ module Additionals
options[:name] = raw_link[(start_pos + 34)..-1] if options[:name].blank?
link = raw_link.gsub('http://', 'https://')
elsif raw_link[0] =~ /\w/
elsif /\w/.match?(raw_link[0])
options[:name] = raw_link if options[:name].blank?
link = "https://www.redmine.org/projects/redmine/wiki/#{Wiki.titleize(raw_link)}"
else

View file

@ -26,12 +26,10 @@ module Additionals
raise 'The correct usage is {{slideshare(<key>[, width=x, height=y, slide=number])}}' if args.empty?
v = args[0]
src = if slide > 0
'//www.slideshare.net/slideshow/embed_code/' + v + '?startSlide=' + slide.to_s
else
'//www.slideshare.net/slideshow/embed_code/' + v
end
content_tag(:iframe, '', width: width, height: height, src: src, frameborder: 0, allowfullscreen: 'true')
src = "//www.slideshare.net/slideshow/embed_code/#{v}"
src += "?startSlide=#{slide}" if slide.positive?
tag.iframe width: width, height: height, src: src, frameborder: 0, allowfullscreen: 'true'
end
end
end

View file

@ -25,25 +25,28 @@ module Additionals
user_id = args[0]
user = User.find_by(id: user_id)
user = User.find_by id: user_id
user ||= User.find_by(login: user_id)
return if user.nil?
name = if options[:format].blank?
user.name
else
user.name(options[:format].to_sym)
user.name options[:format].to_sym
end
s = []
s << avatar(user, size: 14) + ' ' if options[:avatar].present? && options[:avatar]
if options[:avatar].present? && options[:avatar]
s << avatar(user, size: 14)
s << ' '
end
s << if user.active?
link_to(h(name), user_url(user, only_path: controller_path != 'mailer'), class: user.css_classes)
link_to h(name), user_url(user, only_path: controller_path != 'mailer'), class: user.css_classes
else
h(name)
h name
end
safe_join(s)
safe_join s
end
end
end

View file

@ -32,11 +32,11 @@ module Additionals
v = args[0]
src = if autoplay
'//player.vimeo.com/video/' + v + '?autoplay=1'
"//player.vimeo.com/video/#{v}?autoplay=1"
else
'//player.vimeo.com/video/' + v
"//player.vimeo.com/video/#{v}"
end
content_tag(:iframe, '', width: width, height: height, src: src, frameborder: 0, allowfullscreen: 'true')
tag.iframe width: width, height: height, src: src, frameborder: 0, allowfullscreen: 'true'
end
end
end

View file

@ -32,11 +32,11 @@ module Additionals
v = args[0]
src = if autoplay
'//www.youtube.com/embed/' + v + '?autoplay=1'
"//www.youtube.com/embed/#{v}?autoplay=1"
else
'//www.youtube-nocookie.com/embed/' + v
"//www.youtube-nocookie.com/embed/#{v}"
end
content_tag(:iframe, '', width: width, height: height, src: src, frameborder: 0, allowfullscreen: 'true')
tag.iframe width: width, height: height, src: src, frameborder: 0, allowfullscreen: 'true'
end
end
end

View file

@ -23,7 +23,7 @@ namespace :redmine do
Set settings.
Example for value:
bundle exec rake redmine:additionals:setting_set RAILS_ENV=production name="additionals" setting="external_urls" value="2"
bundle exec rake redmine:additionals:setting_set RAILS_ENV=production name="additionals" setting="open_external_urls" value="1"
Example for list of value:
bundle exec rake redmine:additionals:setting_set RAILS_ENV=production setting="default_projects_modules" value="issue_tracking,time_tracking,wiki"
DESCRIPTION
@ -55,7 +55,7 @@ namespace :redmine do
Get settings.
Example for plugin setting:
bundle exec rake redmine:additionals:setting_get RAILS_ENV=production name="additionals" setting="external_urls"
bundle exec rake redmine:additionals:setting_get RAILS_ENV=production name="additionals" setting="open_external_urls"
Example for redmine setting:
bundle exec rake redmine:additionals:setting_get RAILS_ENV=production name="redmine" setting="app_title"
Example for redmine setting: