Nuevo plugin Additionals 2.0.20

This commit is contained in:
Manuel Cillero 2019-06-16 12:53:09 +02:00
parent a2a901b71b
commit 93e1e28683
354 changed files with 40514 additions and 0 deletions

View file

@ -0,0 +1,131 @@
module Additionals
MAX_CUSTOM_MENU_ITEMS = 5
SELECT2_INIT_ENTRIES = 20
GOTO_LIST = " \xc2\xbb".freeze
LIST_SEPARATOR = GOTO_LIST + ' '
class << self
def setup
incompatible_plugins(%w[redmine_tweaks
redmine_issue_control_panel
redmine_editauthor
redmine_changeauthor
redmine_auto_watch])
patch(%w[AccountController
Issue
IssuePriority
TimeEntry
Project
Wiki
WikiController
Principal
QueryFilter
Role
User
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)
when 'textile'
Redmine::WikiFormatting::Textile::Formatter.send(:include, Patches::FormatterTextilePatch)
Redmine::WikiFormatting::Textile::Helper.send(:include, Patches::FormattingHelperPatch)
end
end
# Static class patches
IssuesController.send(:helper, AdditionalsIssuesHelper)
WikiController.send(:helper, AdditionalsWikiPdfHelper)
Redmine::AccessControl.send(:include, Additionals::Patches::AccessControlPatch)
# Global helpers
ActionView::Base.send :include, Additionals::Helpers
ActionView::Base.send :include, AdditionalsFontawesomeHelper
ActionView::Base.send :include, AdditionalsMenuHelper
# Hooks
require_dependency 'additionals/hooks'
# Macros
load_macros(%w[calendar cryptocompare date fa gist gmap 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)
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
else
# Rails 5 uses ActiveSupport::HashWithIndifferentAccess
Setting[plugin_name]
end
end
def setting?(value)
true?(settings[value])
end
def true?(value)
return true if value.to_i == 1 || value.to_s.casecmp('true').zero?
false
end
def now_with_user_time_zone(user = User.current)
if user.time_zone.nil?
Time.zone.now
else
user.time_zone.now
end
end
def incompatible_plugins(plugins = [], title = 'additionals')
plugins.each do |plugin|
raise "\n\033[31m#{title} plugin cannot be used with #{plugin} plugin'.\033[0m" if Redmine::Plugin.installed?(plugin)
end
end
def patch(patches = [], plugin_id = 'additionals')
patches.each do |name|
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)
end
end
def load_macros(macros = [], plugin_id = 'additionals')
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
end
end
end

View file

@ -0,0 +1,78 @@
# Formater
module Additionals
module Formatter
SMILEYS = { 'smiley' => ':-?\)', # :)
'smiley2' => '=-?\)', # =)
'laughing' => ':-?D', # :D
'laughing2' => '[=]-?D', # =D
'crying' => '[=:][\'*]\(', # :'(
'sad' => '[=:]-?\(', # :(
'wink' => ';-?[)D]', # ;)
'cheeky' => '[=:]-?[Ppb]', # :P
'shock' => '[=:]-?[Oo0]', # :O
'annoyed' => '[=:]-?[\\/]', # :/
'confuse' => '[=:]-?S', # :S
'straight' => '[=:]-?[\|\]]', # :|
'embarrassed' => '[=:]-?[Xx]', # :X
'kiss' => '[=:]-?\*', # :*
'angel' => '[Oo][=:]-?\)', # O:)
'evil' => '>[=:;]-?[)(]', # >:)
'rock' => 'B-?\)', # B)
'rose' => '@[)\}][-\\/\',;()>\}]*', # @}->-
'exclamation' => '[\[(]![\])]', # (!)
'question' => '[\[(]\?[\])]', # (?)
'check' => '[\[(]\\/[\])]', # (/)
'success' => '[\[(]v[\])]', # (v)
'failure' => '[\[(]x[\])]' }.freeze # (x)
def render_inline_smileys(text)
return text if text.blank?
inline_smileys(text)
text
end
def inline_smileys(text)
SMILEYS.each do |name, regexp|
text.gsub!(/(\s|^|>|\))(!)?(#{regexp})(?=\W|$|<)/m) do
leading = Regexp.last_match(1)
esc = Regexp.last_match(2)
smiley = Regexp.last_match(3)
if esc.nil?
leading + content_tag(:span,
'',
class: "additionals smiley smiley-#{name}",
title: smiley)
else
leading + smiley
end
end
end
end
def inline_emojify(text)
text.gsub!(/:([\w+-]+):/) do |match|
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')
else
match
end
end
text
end
def inline_emojify_image_path(image_filename)
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
end
end
end

View file

@ -0,0 +1,326 @@
# Global helper functions
module Additionals
module Helpers
def additionals_list_title(options)
title = []
if options[:issue]
title << link_to(h("#{options[:issue].subject} ##{options[:issue].id}"),
issue_path(options[:issue]),
class: options[:issue].css_classes)
elsif options[:user]
title << 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)
end
def additionals_title_for_locale(title, lang)
"#{title}_#{lang}"
end
def additionals_titles_for_locale(title)
languages = [title.to_sym]
valid_languages.each do |lang|
languages << additionals_title_for_locale(title, lang).to_sym if lang.to_s.exclude? '-'
end
languages
end
def additionals_i18n_title(options, title)
i18n_title = "#{title}_#{::I18n.locale}".to_sym
if options.key?(i18n_title)
options[i18n_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)
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)
if comment.blank?
comment = 'N/A'
comment_link = comment_id
else
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')
end
end
def memberships_new_issue_project_url(user, memberships, permission = :edit_issues)
return if memberships.blank?
project_count = 0
project_id = nil
memberships.each do |m|
project = m.is_a?(Project) ? m : Project.find_by(id: m.project_id)
next unless User.current.allowed_to?(permission, project) && user.issues_assignable?(project)
project_count += 1
break if project_count > 1
project_id = project.identifier
end
return if project_id.nil?
# if more than one projects available, we do not use project url for a new issue
if project_count > 1
if permission == :edit_issues
new_issue_path('issue[assigned_to_id]' => user.id, 'issue[project_id]' => project_id)
else
new_issue_path('issue[project_id]' => project_id)
end
elsif permission == :edit_issues
new_project_issue_path(project_id, 'issue[assigned_to_id]' => user.id)
else
new_project_issue_path(project_id)
end
end
def parse_issue_url(url, comment_id = nil)
rc = { issue_id: nil, comment_id: nil }
return rc if url == '' || url.is_a?(Integer) && url.zero?
unless url.to_i.zero?
rc[:issue_id] = url
return rc
end
uri = URI.parse(url)
# support issue_id plugin
# see https://www.redmine.org/plugins/issue_id
issue_id_parts = url.split('-')
if uri.scheme.nil? && uri.path[0] != '/' && issue_id_parts.count == 2
rc[:issue_id] = url
else
if request.nil?
# this is used by mailer
return rc if url.exclude?(Setting.host_name)
elsif uri.host != URI.parse(request.original_url).host
return rc
end
s_pos = uri.path.rindex('/issues/')
id_string = uri.path[s_pos + 8..-1]
e_pos = id_string.index('/')
rc[:issue_id] = e_pos.nil? ? id_string : id_string[0..e_pos - 1]
# check for comment_id
rc[:comment_id] = uri.fragment[5..-1].to_i if comment_id.nil? && uri.fragment.present? && uri.fragment[0..4] == 'note-'
end
rc
end
def additionals_library_load(module_name)
method = "additionals_load_#{module_name}"
send(method)
end
def system_uptime
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 > 0
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
days = `uptime | awk '{print $3}'`.to_i.round
"#{days} #{l(:days, count: days)}"
end
end
def system_info
if windows_platform?
'unknown'
else
`uname -a`
end
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
end
def autocomplete_select_entries(name, type, option_tags, options = {})
unless option_tags.is_a?(String) || option_tags.blank?
# 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?
s = []
s << hidden_field_tag("#{name}[]", '') if options[:multiple]
s << select_tag(name,
option_tags,
include_blank: options[:include_blank],
multiple: options[:multiple],
disabled: options[:disabled])
s << render(layout: false,
partial: 'additionals/select2_ajax_call.js',
formats: [:js],
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)
end
private
def additionals_already_loaded(scope, js_name)
locked = "#{js_name}.#{scope}"
@alreaded_loaded = [] if @alreaded_loaded.nil?
return true if @alreaded_loaded.include?(locked)
@alreaded_loaded << locked
false
end
def additionals_include_js(js_name)
if additionals_already_loaded('js', js_name)
''
else
javascript_include_tag(js_name, plugin: 'additionals') + "\n"
end
end
def additionals_include_css(css)
if additionals_already_loaded('css', css)
''
else
stylesheet_link_tag(css, plugin: 'additionals') + "\n"
end
end
def additionals_load_select2
additionals_include_js('additionals_to_select2')
end
def additionals_load_observe_field
additionals_include_js('additionals_observe_field')
end
def additionals_load_font_awesome
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')
end
def additionals_load_mermaid
additionals_include_js('mermaid.min') +
additionals_include_js('mermaid_load')
end
def additionals_load_d3
additionals_include_js('d3.min')
end
def additionals_load_d3plus
additionals_include_js('d3plus.full.min')
end
def additionals_load_zeroclipboard
additionals_include_js('zeroclipboard_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)
end
def options_for_menu_select(active)
options_for_select({ l(:button_hide) => '',
l(:label_top_menu) => 'top',
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
end
end

View file

@ -0,0 +1,33 @@
# 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_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')
def helper_issues_show_detail_after_setting(context = {})
d = context[:detail]
return unless d.prop_key == 'author_id'
d[:value] = find_name_by_reflection('author', d.value)
d[:old_value] = find_name_by_reflection('author', d.old_value)
end
end
end

View file

@ -0,0 +1,18 @@
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
def self.available_project_modules_all
@permissions.collect(&:project_module).uniq.compact
end
end
end
end
end
end

View file

@ -0,0 +1,11 @@
module Additionals
module Patches
module AccountControllerPatch
def self.included(base)
base.class_eval do
invisible_captcha only: [:register] if Additionals.setting?(:invisible_captcha)
end
end
end
end
end

View file

@ -0,0 +1,20 @@
module Additionals
module Patches
module FormatterMarkdownPatch
def self.included(base)
base.class_eval do
base.send(: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
end
end
end
end

View file

@ -0,0 +1,16 @@
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
end
end
end
end

View file

@ -0,0 +1,32 @@
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
end
module InstanceMethods
def heads_for_wiki_formatter_with_additionals
heads_for_wiki_formatter_without_additionals
return if @additionals_macro_list
@additionals_macro_list = AdditionalsMacro.all(filtered: Additionals.settings[:hidden_macros_in_toolbar].to_a,
only_names: true,
controller_only: controller_name)
return if @additionals_macro_list.count.zero?
content_for :header_tags do
javascript_include_tag('additionals_macro_button', plugin: 'additionals') +
javascript_tag("jsToolBar.prototype.macroList = #{@additionals_macro_list.to_json};")
end
end
end
end
end
end

View file

@ -0,0 +1,189 @@
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
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
end
module InstanceMethods
def sidbar_change_status_allowed_to(user, new_status_id = nil)
statuses = new_statuses_allowed_to(user)
if new_status_id.present?
statuses.detect { |s| new_status_id == s.id && !timelog_required?(s.id) }
else
statuses.reject { |s| timelog_required?(s.id) }
end
end
def add_autowatcher(watcher)
return if watcher.nil? ||
!watcher.is_a?(User) ||
watcher.anonymous? ||
!watcher.active? ||
watched_by?(watcher)
add_watcher(watcher)
end
def autowatch_involved
return unless Additionals.setting?(:issue_autowatch_involved) &&
User.current.pref.autowatch_involved_issue
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
end
true
end
def log_time_allowed?(user = User.current)
!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 true unless closed?
return true unless Additionals.setting?(:issue_freezed_with_close)
user.allowed_to?(:edit_closed_issues, project)
end
end
def autoassign_get_group_list
return unless Setting.issue_group_assignment?
project.memberships
.active
.where("#{Principal.table_name}.type='Group'")
.includes(:user, :roles)
.each_with_object({}) do |m, h|
m.roles.each do |r|
h[r] ||= []
h[r] << m.principal
end
h
end
end
def new_ticket_message
@new_ticket_message = ''
message = Additionals.settings[:new_ticket_message]
@new_ticket_message << message if message.present?
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?
if Additionals.settings[:issue_assign_to_x].include?(new_status_id.to_s)
true
else
false
end
end
private
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? ||
assigned_to_id.present?
return unless Additionals.settings[: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])
groups = autoassign_get_group_list
return groups[manager_role].first.id unless groups.nil? || groups[manager_role].blank?
users_list = project.users_by_role
return users_list[manager_role].first.id if users_list[manager_role].present?
end
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) ||
!usr.allowed_to?(:log_time, project) ||
usr.allowed_to?(:issue_timelog_never_required, project) ||
time_entries.present?
true
end
def validate_timelog_required
return true unless timelog_required?(status_id)
errors.add :base, :issue_requires_timelog
end
def validate_change_on_closed
return true if !closed? ||
new_record? ||
!Additionals.setting?(:issue_freezed_with_close) ||
!status_was.is_closed ||
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) &&
(assigned_to_id.blank? || assigned_to_id != User.current.id)
errors.add :base, :issue_current_user_status
else
true
end
end
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?
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
self.assigned_to = author
end
end
end
end
end

View file

@ -0,0 +1,41 @@
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
end
module InstanceMethods
def css_classes_with_additionals
classes = [css_classes_without_additionals, css_name_based_class]
classes.join(' ')
end
# css class based on priority name
def css_name_based_class
css_name_based_classes.each do |name_class|
return name_class[:name] if name_class[:words].any? { |s| s.casecmp(name).zero? }
end
'prio-name-other'
end
def css_name_based_classes
@css_name_based_classes ||= [{ name: 'prio-name-low',
words: [l(:default_priority_low), 'Low', 'Trivial', 'Niedrig', 'Gering'] },
{ name: 'prio-name-normal',
words: [l(:default_priority_normal), 'Normal', 'Minor', 'Unwesentlich', 'Default'] },
{ name: 'prio-name-high',
words: [l(:default_priority_high), 'High', 'Major', 'Important', 'Schwer', 'Hoch', 'Wichtig'] },
{ name: 'prio-name-urgent',
words: [l(:default_priority_urgent), 'Urgent', 'Critical', 'Kritisch', 'Dringend'] },
{ name: 'prio-name-immediate',
words: [l(:default_priority_immediate), 'Immediate', 'Blocker', 'Very high', 'Jetzt'] }]
end
end
end
end
end

View file

@ -0,0 +1,46 @@
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
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
# 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
end
}
end
end
end
end
end

View file

@ -0,0 +1,23 @@
module Additionals
module Patches
module ProjectPatch
def self.included(base)
base.send(:prepend, InstancOverwriteMethods)
end
module InstancOverwriteMethods
def users_by_role
roles_with_users = super
roles_with_users.each do |role_with_users|
role = role_with_users.first
next unless role.hide
roles_with_users.delete(role) unless User.current.allowed_to?(:show_hidden_roles_in_memberbox, project)
end
roles_with_users
end
end
end
end
end

View file

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

View file

@ -0,0 +1,11 @@
module Additionals
module Patches
module RolePatch
def self.included(base)
base.class_eval do
safe_attributes 'hide'
end
end
end
end
end

View file

@ -0,0 +1,30 @@
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
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
errors.add(:issue_id, :issue_log_time_not_allowed) unless issue.log_time_allowed?
end
def editable_by_with_additionals?(usr)
return false unless editable_by_without_additionals?(usr)
return true unless issue_id && issue
issue.log_time_allowed?
end
end
end
end
end

View file

@ -0,0 +1,18 @@
module Additionals
module Patches
module UserPatch
def self.included(base)
base.send(:include, InstanceMethods)
end
module InstanceMethods
def issues_assignable?(project = nil)
scope = Principal.joins(members: :roles)
.where(users: { id: id }, roles: { assignable: true })
scope = scope.where(members: { project_id: project.id }) if project
scope.exists?
end
end
end
end
end

View file

@ -0,0 +1,11 @@
module Additionals
module Patches
module UserPreferencePatch
def self.included(base)
base.class_eval do
safe_attributes 'autowatch_involved_issue'
end
end
end
end
end

View file

@ -0,0 +1,65 @@
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

@ -0,0 +1,30 @@
require_dependency 'wiki'
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
# 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)
end
end
end
end
end

View file

@ -0,0 +1,70 @@
# 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

@ -0,0 +1,97 @@
# CryptoCompare wiki macros
# see https://www.cryptocompare.com/dev/widget/wizard/
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc <<-DESCRIPTION
Create CryptoCompare information.
{{cryptocompare(options)}}
see https://additionals.readthedocs.io/en/latest/macros/#cryptocompare
DESCRIPTION
macro :cryptocompare do |_obj, args|
raise 'The correct usage is {{cryptocompare(options)}}' if args.empty?
_args, options = extract_macro_options(args, :fsym, :fsyms, :tsym, :tsyms, :period, :type)
options[:fsym] = 'BTC' if options[:fsym].blank?
options[:tsym] = 'EUR' if options[:tsym].blank?
if options[:type].blank?
widget_type = 'chart'
else
widget_type = options[:type]
options.delete(:type)
end
base_url = 'https://widgets.cryptocompare.com/'
case widget_type
when 'chart'
url = base_url + 'serve/v2/coin/chart'
when 'news'
options[:feedType] = 'CoinTelegraph' if options[:feedType].blank?
url = base_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'
when 'titles'
options[:tsyms] = Additionals.crypto_default(options, :tsyms, 'EUR,USD')
options.delete(:tsym)
url = base_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'
when 'header', 'header_v1'
options[:tsyms] = Additionals.crypto_default(options, :tsyms, 'EUR,USD')
options.delete(:tsym)
url = base_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'
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'
when 'summary'
options[:tsyms] = Additionals.crypto_default(options, :tsyms, 'EUR,USD')
options.delete(:tsym)
url = base_url + 'serve/v1/coin/summary'
when 'historical'
url = base_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'
when 'advanced'
options[:tsyms] = Additionals.crypto_default(options, :tsyms, 'EUR,USD')
options.delete(:tsym)
url = base_url + 'serve/v3/coin/chart'
else
raise 'type is not supported'
end
render partial: 'wiki/cryptocompare',
formats: [:html],
locals: { url: url + '?' + options.map { |k, v| "#{k}=#{v}" }.join('&') }
end
end
end
def self.crypto_default(options, name, defaults)
if options[name].blank?
defaults
else
options[name].tr(';', ',')
end
end
end

View file

@ -0,0 +1,70 @@
# Date wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc <<-DESCRIPTION
Show date.
Syntax:
{{date([TYPE])}}
TYPE
- current_date current date (default)
- current_date_with_time current date with time
- current_year current year
- current_month current month
- current_day current day
- current_hour current hour
- current_minute current minute
- current_weekday current weekday
- current_weeknumber current week number (1 - 52) The week starts with Monday
- YYYY-MM-DD e.g. 2018-12-24, which will formated with Redmine date format
Examples:
{{date}}
...show current date
{{date(current_year)}}
...show current year
{{date(current_month)}}
...show current month
{{date(current_weeknumber)}}
...show current week number
DESCRIPTION
macro :date do |_obj, args|
type = if args.present?
args[0]
else
'current_date'
end
d = Additionals.now_with_user_time_zone
date_result = case type
when 'current_date'
format_date(User.current.today)
when 'current_date_with_time'
format_time(d, true)
when 'current_year'
d.year
when 'current_month'
d.month
when 'current_day'
d.day
when 'current_hour'
d.hour
when 'current_minute'
d.min
when 'current_weekday'
day_name(d.wday)
when 'current_weeknumber'
User.current.today.cweek
else
format_date(type.to_date)
end
content_tag(:span, date_result, class: 'current-date')
end
end
end
end

View file

@ -0,0 +1,70 @@
# Font Awesome wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc <<-DESCRIPTION
Show Font Awesome icon.
Syntax:
{{fa(ICON [, class=CLASS, title=TITLE, text=TEXT size=SIZE, color=COLOR)}}
ICON of fontawesome icon, eg. fa-adjust
CLASS = additional css classes
TITLE = mouseover title
TEXT = Text to show
LINK = Link icon and text (if specified) to this URL
COLOR = css color code
Examples:
{{fa(adjust)}}
...show fontawesome icon "fas fa-adjust"
{{fa(adjust, class=fa-inverse)}}
...show fontawesome icon "fas fa-stack" and inverse
{{fa(adjust, size=4x)}}
...show fontawesome icon "fas fa-adjust" with size 4x
{{fa(fas_adjust, title=Show icon)}}
...show fontawesome icon "fas fa-adjust" with title "Show icon"
{{fa(fab_angellist)}}
...Show fontawesome icon "fab fa-angellist"
{{fa(adjust, link=https=//www.redmine.org))}}
...Show fontawesome icon "fas fa-adjust" and link it to https://www.redmine.org
{{fa(adjust, link=https=//www.redmine.de, name=Go to Redmine.org))}}
...Show fontawesome icon "fas fa-adjust" with name "Go to Redmine.org" and link it to https://www.redmine.org
DESCRIPTION
macro :fa do |_obj, args|
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('_')
classes = []
if values.count == 2
classes << values[0]
classes << "fa-#{values[1]}"
else
classes << 'fas'
classes << "fa-#{values[0]}"
end
classes += options[:class].split(' ') if options[:class].present?
classes << "fa-#{options[:size]}" if options[:size].present?
content_options = { class: classes.uniq.join(' ') }
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] : ''
if options[:link].present?
content_tag(:a, href: options[:link]) do
content_tag(:i, text, content_options)
end
else
content_tag(:i, text, content_options)
end
end
end
end
end

View file

@ -0,0 +1,14 @@
# Gist wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc 'gist embed'
macro :gist do |_obj, args|
raise 'The correct usage is {{gist(<gist_id>)}}' if args.empty?
javascript_tag(nil, src: "https://gist.github.com/#{args[0]}.js")
end
end
end
end

View file

@ -0,0 +1,97 @@
# Gist wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc <<-DESCRIPTION
Display a google map. Examples:
Syntax:
{{gmap([q=QUERY, mode=MODE, width=216, height=368])}}
Examples:
{{gmap(Munich)}} Google maps with Munich
{{gmap(mode=directions, origin=Munich+Rosenheimerstr, destination=Arco)}} Direction from Munich to Arco
DESCRIPTION
macro :gmap do |_obj, args|
src_options = %i[attribution_ios_deep_link_id
attribution_source
attribution_web_url
avoid
center
destination
fov
heading
language
location
maptype
origin
pano
pitch
region
units
waypoints
zoom]
args, options = extract_macro_options(args,
:mode,
:width,
:height,
:attribution_ios_deep_link_id,
:attribution_source,
:attribution_web_url,
:avoid,
:center,
:destination,
:fov,
:heading,
:language,
:location,
:maptype,
:origin,
:pano,
:pitch,
:region,
:units,
:way_mode,
:waypoints,
:zoom)
raise 'Missing Google Maps Embed API Key. See documentation for more info.' if Additionals.settings[:google_maps_api_key].blank?
width = options[:width].presence || 620
height = options[:height].presence || 350
mode = options[:mode].presence || 'search'
if mode == 'search' && options[:q].blank? && args.empty?
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]
if options[:q].present?
src << '&q=' + ERB::Util.url_encode(options[:q])
elsif mode == 'search'
src << '&q=' + ERB::Util.url_encode(args[0])
end
src_options.each do |key|
src << Additionals.gmap_flags(options, key)
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')
end
end
end
def self.gmap_flags(options, key)
if options[key].present?
"&#{key}=" + ERB::Util.url_encode(options[key])
else
''
end
end
end

View file

@ -0,0 +1,34 @@
# Group wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc <<-DESCRIPTION
Display users of group.
Syntax:
{{group_users(GROUP_NAME}}
Examples:
{{group_users(Team)}}
...List all users in user group "Team" (with the current user permission)
DESCRIPTION
macro :group_users do |_obj, args|
raise 'The correct usage is {{group_users(<group_name>)}}' if args.empty?
group_name = args[0].strip
group = Group.named(group_name).first
raise unless group
users = Principal.visible.where(id: group.users).order(User.name_formatter[:order])
render partial: 'wiki/user_macros',
formats: [:html],
locals: { users: users,
user_roles: nil,
list_title: group_name }
end
end
end
end

View file

@ -0,0 +1,61 @@
# Slideshare wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc <<-DESCRIPTION
Include iframe
Syntax:
{{iframe(<url> [, width=100%, height=485)}}
Examples:
show iframe of URL https://www.google.com/
{{iframe(https://www.google.com/)}}
show iframe of URL https://www.google.com/ and show link to it
{{iframe(https://www.google.com/, with_link: true)}}
DESCRIPTION
macro :iframe do |_obj, args|
args, options = extract_macro_options(args, :width, :height, :slide, :with_link)
width = options[:width].presence || '100%'
height = options[:height].presence || 485
raise 'The correct usage is {{iframe(<url>[, width=x, height=y, with_link=bool])}}' if args.empty?
src = args[0]
if Additionals.valid_iframe_url?(src)
s = [content_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)
elsif Setting.protocol == 'https'
raise 'Invalid url provided to iframe (only full URLs with protocol HTTPS are accepted)'
else
raise 'Invalid url provided to iframe (only full URLs are accepted)'
end
end
end
end
def self.valid_iframe_url?(url)
uri = URI.parse(url)
if Setting.protocol == 'https'
uri.is_a?(URI::HTTPS) && !uri.host.nil?
else
!uri.host.nil?
end
rescue URI::InvalidURIError
false
end
end

View file

@ -0,0 +1,66 @@
# Issue wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc <<-DESCRIPTION
Create a link to issue with the subject of this issue.
Syntax:
{{issue(URL [, format=USER_FORMAT, id=ID, note_id=NOTE_ID)}}
URL is URL to issue
USER_FORMATS
- text
- short
- link (default)
- full
ID is issue
NOTE_ID is note id, if you want to display it
Examples:
{{issue(1)}}
...Link to issue with id and subject
{{issue(http://myredmine.url/issues/1)}}
...Link to issue with id and subject
{{issue(http://myredmine.url/issues/1#note-3)}}
...Link to issue with id and subject and display comment 3
{{issue(1, format=short)}}
...Link to issue with subject (without id)
{{issue(1, format=text)}}
...Display subject name
{{issue(1, format=full)}}
...Link to issue with track, issue id and subject
DESCRIPTION
macro :issue do |_obj, args|
args, options = extract_macro_options(args, :id, :note_id, :format)
raise 'The correct usage is {{issue(<url>, format=FORMAT, id=INT, note_id=INT)}}' if args.empty? && options[:id].blank?
comment_id = options[:note_id].to_i if options[:note_id].present?
issue_id = options[:id].presence ||
(info = parse_issue_url(args[0], comment_id)
comment_id = info[:comment_id] if comment_id.nil?
info[:issue_id])
issue = Issue.find_by(id: issue_id)
return if issue.nil? || !issue.visible?
text = case options[:format]
when 'full'
"#{issue.tracker.name} ##{issue_id} #{issue.subject}"
when 'text', 'short'
issue.subject
else
"##{issue_id} #{issue.subject}"
end
if options[:format].blank? || options[:format] != 'text'
render_issue_macro_link(issue, text, comment_id)
else
text
end
end
end
end
end

View file

@ -0,0 +1,40 @@
# Last_updated_at wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc <<-DESCRIPTION
Displays a date that updated the page.
{{last_updated_at}}
{{last_updated_at(project_name, wiki_page)}}
{{last_updated_at(project_identifier, wiki_page)}}
DESCRIPTION
macro :last_updated_at do |obj, args|
return '' unless @project
if args.empty?
page = obj
else
raise '{{last_updated_at(project_identifier, wiki_page)}}' if args.length < 2
project_name = args[0].strip
page_name = args[1].strip
project = Project.find_by(name: project_name)
project ||= Project.find_by(identifier: project_name)
return unless project
wiki = Wiki.find_by(project_id: project.id)
return unless wiki
page = wiki.find_page(page_name)
end
return unless page
content_tag(:span,
l(:label_updated_time, time_tag(page.updated_on)).html_safe,
class: 'last-updated-at')
end
end
end
end

View file

@ -0,0 +1,19 @@
# Last_updated_by wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc <<-DESCRIPTION
Displays a user who updated the page.
{{last_updated_by}}
DESCRIPTION
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')
end
end
end
end

View file

@ -0,0 +1,77 @@
# Member wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc <<-DESCRIPTION
Display members.
Syntax:
{{members([PROJECT_NAME, title=My members list, role=ROLE)]}}
PROJECT_NAME can be project identifier, project name or project id
Examples:
{{members}}
...List all members for all projects (with the current user permission)
{{members(the-identifier)}}
...A box showing all members for the project with the identifier of 'the-identifier'
{{members(the-identifier, role=Manager)}}
...A box showing all members for the project with the identifier of 'the-identifier', which
have the role "Manager"
{{members(the-identifier, title=My user list)}}
...A box showing all members for the project with the identifier of 'the-identifier' and with
box title "My user list"
DESCRIPTION
macro :members do |_obj, args|
args, options = extract_macro_options(args, :role, :title)
project_id = args[0]
user_roles = []
if project_id.present?
project_id.strip!
project = Project.visible.find_by(id: project_id)
project ||= Project.visible.find_by(identifier: project_id)
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?
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])
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])
.sorted
end
render partial: 'wiki/user_macros', locals: { users: users,
user_roles: user_roles,
list_title: options[:title] }
end
end
end
def self.check_role_matches(roles, filters)
filters.tr('|', ',').split(',').each do |filter|
roles.each { |role| return true if filter.to_s == role.to_s }
end
false
end
end

View file

@ -0,0 +1,85 @@
# meteoblue wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc <<-DESCRIPTION
Display current weather from meteoblue service. Examples:
Syntax:
{{meteoblue(<location> [, days=INT, width=216, height=368, color=BOOL])}}
Examples:
{{meteoblue(münchen_deutschland_2867714)}} weather for Munich
{{meteoblue(münchen_deutschland_2867714, days=6, color=false)}} weather for Munich of the next 6 days without color
DESCRIPTION
macro :meteoblue do |_obj, args|
args, options = extract_macro_options(args,
:days,
:width,
:height,
:color,
:pictoicon,
:maxtemperature,
:mintemperature,
:windspeed,
:windgust,
:winddirection,
:uv,
:humidity,
:precipitation,
:precipitationprobability,
:spot)
raise 'The correct usage is {{meteoblue(<location>[, days=x, color=BOOL])}}' if args.empty?
options[:days] = 4 if options[:days].blank?
options[:coloured] = if options[:color].present? && !Additionals.true?(options[:color])
'monochrome'
else
'coloured'
end
width = options[:width].presence || 216
height = options[:height].presence || 368
src = if User.current.language.blank? ? ::I18n.locale : User.current.language == 'de'
'https://www.meteoblue.com/de/wetter/widget/daily/'
else
'https://www.meteoblue.com/en/weather/widget/daily/'
end
src << ERB::Util.url_encode(args[0])
src << "?geoloc=fixed&days=#{options[:days]}&tempunit=CELSIUS&windunit=KILOMETER_PER_HOUR"
src << "&precipunit=MILLIMETER&coloured=#{options[:coloured]}"
src << Additionals.meteoblue_flag(options, :pictoicon, true)
src << Additionals.meteoblue_flag(options, :maxtemperature, true)
src << Additionals.meteoblue_flag(options, :mintemperature, true)
src << Additionals.meteoblue_flag(options, :windspeed, false)
src << Additionals.meteoblue_flag(options, :windgust, false)
src << Additionals.meteoblue_flag(options, :winddirection, false)
src << Additionals.meteoblue_flag(options, :uv, false)
src << Additionals.meteoblue_flag(options, :humidity, false)
src << Additionals.meteoblue_flag(options, :precipitation, true)
src << Additionals.meteoblue_flag(options, :precipitationprobability, true)
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)
end
end
end
def self.meteoblue_flag(options, name, default = tue)
flag = "&#{name}="
flag << if Additionals.true?(options[name]) || default
'1'
else
'0'
end
end
end

View file

@ -0,0 +1,61 @@
# Issue wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc <<-DESCRIPTION
Create a link for "New issue" for the current user.
Syntax:
{{new_issue([PROJECT_NAME, name=Custom name]}}
PROJECT_NAME can be project identifier, project name or project id.
If no PROJECT_NAME is specified, first project is used, which the current user
has permission to create an issue.
Examples:
{{new_issue}}
...Link to create new issue in first available project
{{new_issue(the-identifier)}}
...Link to create new issue in project with the identifier of 'the-identifier'
{{new_issue(the-identifier, title=New issue for broken displays)}}
...Link to create new issue in project with the identifier of 'the-identifier'
and the name 'New issue for broken displays'
DESCRIPTION
macro :new_issue do |_obj, args|
if args.any?
args, options = extract_macro_options(args, *additionals_titles_for_locale(:name))
i18n_name = additionals_i18n_title(options, :name)
project_id = args[0]
end
i18n_name = l(:label_issue_new) if i18n_name.blank?
if project_id.present?
project_id.strip!
project = Project.visible.find_by(id: project_id)
project ||= Project.visible.find_by(identifier: project_id)
project ||= Project.visible.find_by(name: project_id)
return '' if project.nil? || !User.current.allowed_to?(:add_issues, project)
return link_to(i18n_name, new_project_issue_path(project), class: 'macro-new-issue icon icon-add')
else
@memberships = User.current
.memberships
.preload(:roles, :project)
.where(Project.visible_condition(User.current))
.to_a
if @memberships.present?
project_url = memberships_new_issue_project_url(User.current, @memberships, :add_issues)
return link_to(i18n_name, project_url, class: 'macro-new-issue icon icon-add') if project_url.present?
end
end
''
end
end
end
end

View file

@ -0,0 +1,47 @@
# Project wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc <<-DESCRIPTION
Display projects.
Syntax:
{{projects([title=My project list, with_create_issue=BOOL])}}
Examples:
{{projects}}
...List all project, which I am member of
{{projects(title=My project list)}}
...List all project with title "My project list", which I am member of
{{projects(with_create_issue=true)}}
...List all project with link to create new issue, which I am member of
DESCRIPTION
macro :projects do |_obj, args|
_args, options = extract_macro_options(args, :title, :with_create_issue)
@projects = Additionals.load_projects
return if @projects.nil?
@html_options = { class: 'external' }
render partial: 'wiki/project_macros',
formats: [:html],
locals: { projects: @projects,
list_title: options[:title],
with_create_issue: options[:with_create_issue] }
end
end
end
def self.load_projects
all_projects = Project.active.visible.sorted
my_projects = []
all_projects.each do |p|
my_projects << p if User.current.member_of?(p)
end
my_projects
end
end

View file

@ -0,0 +1,53 @@
# Recently updated wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc <<-DESCRIPTION
Displays a list of pages that were updated recently.
{{recently_updated}}
{{recently_updated([days])}}
Examples:
{{recently_updated}}
...List last updated pages (of the last 5 days)
{{recently_updated(15)}}
...List last updated pages of the last 15 days
DESCRIPTION
macro :recently_updated do |obj, args|
page = obj.page
return unless page
project = page.project
return unless project
days = 5
days = args[0].strip.to_i unless args.empty?
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 = ''
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/>'
end
o << link_to(content.page.pretty_title,
controller: 'wiki', action: 'show', project_id: content.page.project, id: content.page.title)
o << '<br/>'
end
content_tag('div', o.html_safe, class: 'recently-updated')
end
end
end
end

View file

@ -0,0 +1,36 @@
# Reddit wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc <<-DESCRIPTION
Creates link to reddit.
{{reddit(name)}}
DESCRIPTION
macro :reddit do |_obj, args|
raise 'The correct usage is {{reddit(<name>)}}' if args.empty?
name = args[0].strip
case name[0..1]
when 'r/'
link_to(font_awesome_icon('fab_reddit', post_text: name),
"https://www.reddit.com/#{name}",
class: 'external reddit',
title: l(:label_reddit_subject))
when 'u/'
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))
else
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))
end
end
end
end
end

View file

@ -0,0 +1,42 @@
# Redmine.org issue wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc <<-DESCRIPTION
Creates link to redmine.org issue.
{{redmine_issue(1448)}}
DESCRIPTION
macro :redmine_issue do |_obj, args|
raise 'The correct usage is {{redmine_issue(<id>)}}' if args.empty?
args, options = extract_macro_options(args, :title)
raw_link = args[0].to_s.strip
if !/\A\d+\z/.match(raw_link[0])
# https://www.redmine.org/issues/12066#note-7
if raw_link =~ %r{redmine.org/issues/([0-9].+?)#(.*)} ||
raw_link =~ %r{redmine.org/issues/([0-9].+)}
link_name = Regexp.last_match(1)
link = raw_link.gsub('http://', 'https://')
else
raise 'The correct usage is {{redmine_issue(<id>)}}'
end
elsif raw_link =~ /([0-9].+?)\D/
# ID with parameters
link_name = Regexp.last_match(1)
link = "https://www.redmine.org/issues/#{raw_link}"
else
# just ID
link_name = raw_link
link = "https://www.redmine.org/issues/#{raw_link}"
end
link_options = { class: 'external redmine-link' }
link_options[:title] = options[:title].presence || l(:label_redmine_org_issue)
link_to("##{link_name}", link, link_options)
end
end
end
end

View file

@ -0,0 +1,37 @@
# Redmine.org issue wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc <<-DESCRIPTION
Creates link to redmine.org wiki page.
{{redmine_wiki(Installing Redmine)}}
DESCRIPTION
macro :redmine_wiki do |_obj, args|
raise 'The correct usage is {{redmine_wiki(<page>)}}' if args.empty?
args, options = extract_macro_options(args, :title, :name)
raw_link = args[0].to_s.strip
if raw_link[0..3] == 'http'
start_pos = raw_link.index('redmine.org/projects/redmine/wiki/')
raise 'The correct usage is {{redmine_wiki(<page>)}}' if start_pos.nil? || start_pos.zero?
options[:name] = raw_link[(start_pos + 34)..-1] if options[:name].blank?
link = raw_link.gsub('http://', 'https://')
elsif raw_link[0] =~ /\w/
options[:name] = raw_link if options[:name].blank?
link = "https://www.redmine.org/projects/redmine/wiki/#{Wiki.titleize(raw_link)}"
else
raise 'The correct usage is {{redmine_wiki(<page>)}}'
end
link_options = { class: 'external redmine-link' }
link_options[:title] = options[:title].presence || l(:label_redmine_org_wiki)
link_to(options[:name], link, link_options)
end
end
end
end

View file

@ -0,0 +1,38 @@
# Slideshare wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc <<-DESCRIPTION
Slideshare macro to include Slideshare slide.
Syntax:
{{slideshare(<key> [, width=595, height=485, slide=SLIDE])}}
Examples:
{{slideshare(57941706)}} show slideshare slide with default size 595x485
{{slideshare(57941706, width=514, height=422)}} show slide with user defined size
{{slideshare(57941706, slide=5)}} start with slide (page) 5
DESCRIPTION
macro :slideshare do |_obj, args|
args, options = extract_macro_options(args, :width, :height, :slide)
width = options[:width].presence || 595
height = options[:height].presence || 485
slide = options[:slide].present? ? options[:slide].to_i : 0
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')
end
end
end
end

View file

@ -0,0 +1,38 @@
# Tradingview wiki macros
# see https://www.tradingview.com/widget/
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc <<-DESCRIPTION
Creates Tradingview chart
{{tradingview(options)}}
see https://additionals.readthedocs.io/en/latest/macros/#tradingview
DESCRIPTION
macro :tradingview do |_obj, args|
raise 'The correct usage is {{tradingview(options)}}' if args.empty?
_args, options = extract_macro_options(args, :width, :height, :symbol, :interval, :timezone,
:theme, :style, :locale, :toolbar_bg, :enable_publishing,
:allow_symbol_change, :hideideasbutton)
options[:width] = 640 if options[:width].blank?
options[:height] = 480 if options[:height].blank?
options[:symbol] = 'NASDAQ:AAPL' if options[:symbol].blank?
options[:interval] = 'W' if options[:interval].blank?
options[:timezone] = 'Europe/Berlin' if options[:timezone].blank?
options[:theme] = 'White' if options[:theme].blank?
options[:style] = 2 if options[:style].blank?
options[:locale] = 'de' if options[:locale].blank?
options[:toolbar_bg] = '#f1f3f6' if options[:toolbar_bg].blank?
options[:enable_publishing] = false if options[:enable_publishing].blank?
options[:allow_symbol_change] = true if options[:allow_symbol_change].blank?
options[:hideideasbutton] = true if options[:hideideasbutton].blank?
render partial: 'wiki/tradingview',
formats: [:html],
locals: { options: options }
end
end
end
end

View file

@ -0,0 +1,34 @@
# Twitter wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc <<-DESCRIPTION
Creates link to twitter account page or topic.
{{twitter(name)}}
DESCRIPTION
macro :twitter do |_obj, args|
raise 'The correct usage is {{twitter(<name>)}}' if args.empty?
name = args[0].strip
case name[0]
when '@'
link_to(font_awesome_icon('fab_twitter', post_text: name),
"https://twitter.com/#{name[1..-1]}",
class: 'external twitter',
title: l(:label_twitter_account))
when '#'
link_to(font_awesome_icon('fab_twitter-square', post_text: name),
"https://twitter.com/hashtag/#{name[1..-1]}",
class: 'external twitter',
title: l(:label_twitter_hashtag))
else
link_to(font_awesome_icon('fab_twitter', post_text: " @#{name}"),
"https://twitter.com/#{name}",
class: 'external twitter',
title: l(:label_twitter_account))
end
end
end
end
end

View file

@ -0,0 +1,50 @@
# User wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc "Display link to user profile\n\n" \
"Syntax:\n\n" \
"{{user(USER_NAME [, format=USER_FORMAT, avatar=BOOL])}}\n\n" \
"USER_NAME can be user id or user name (login name)\n" \
"USER_FORMATS\n" \
"- system (use system settings) (default)\n- " +
User::USER_FORMATS.keys.join("\n- ") + "\n\n" \
"Examples:\n\n" \
"{{user(1)}}\n" \
"...Link to user with user id 1\n\n" \
"{{user(1, avatar=true)}}\n" \
"...Link to user with user id 1 with avatar\n\n" \
"{{user(admin)}}\n" \
"...Link to user with username 'admin'\n\n" \
"{{user(admin, format=firstname)}}\n" \
"...Link to user with username 'admin' and show firstname as link text"
macro :user do |_obj, args|
args, options = extract_macro_options(args, :format, :avatar)
raise 'The correct usage is {{user(<user_id or username>, format=USER_FORMAT)}}' if args.empty?
user_id = args[0]
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)
end
s = []
s << avatar(user, size: 14) + ' ' if options[:avatar].present? && options[:avatar]
s << if user.active?
link_to(h(name), user_url(user, only_path: controller_path != 'mailer'), class: user.css_classes)
else
h(name)
end
safe_join(s)
end
end
end
end

View file

@ -0,0 +1,43 @@
# Vimeo wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc <<-DESCRIPTION
Vimeo macro to include vimeo video.
Syntax:
{{vimeo(<video key> [, width=640, height=360, autoplay=BOOL])}}
Examples:
{{vimeo(142849533)}} show video with default size 640x360
{{vimeo(142849533, width=853, height=480)}} show video with user defined size
{{vimeo(142849533, autoplay=true)}} autoplay video
DESCRIPTION
macro :vimeo do |_obj, args|
args, options = extract_macro_options(args, :width, :height, :autoplay)
width = options[:width].presence || 640
height = options[:height].presence || 360
autoplay = if !options[:autoplay].nil? && Additionals.true?(options[:autoplay])
true
else
false
end
raise 'The correct usage is {{vimeo(<video key>[, width=x, height=y])}}' if args.empty?
v = args[0]
src = if autoplay
'//player.vimeo.com/video/' + v + '?autoplay=1'
else
'//player.vimeo.com/video/' + v
end
content_tag(:iframe, '', width: width, height: height, src: src, frameborder: 0, allowfullscreen: 'true')
end
end
end
end

View file

@ -0,0 +1,43 @@
# Youtube wiki macros
module Additionals
module WikiMacros
Redmine::WikiFormatting::Macros.register do
desc <<-DESCRIPTION
Youtube macro to include youtube video.
Syntax:
{{youtube(<video key> [, width=640, height=360, autoplay=BOOL])}}
Examples:
{{youtube(KMU0tzLwhbE)}} show video with default size 640x360
{{youtube(KMU0tzLwhbE, width=853, height=480)}} show video with user defined size
{{youtube(KMU0tzLwhbE, autoplay=true)}} autoplay video
DESCRIPTION
macro :youtube do |_obj, args|
args, options = extract_macro_options(args, :width, :height, :autoplay)
width = options[:width].presence || 640
height = options[:height].presence || 360
autoplay = if !options[:autoplay].nil? && Additionals.true?(options[:autoplay])
true
else
false
end
raise 'The correct usage is {{youtube(<video key>[, width=x, height=y])}}' if args.empty?
v = args[0]
src = if autoplay
'//www.youtube.com/embed/' + v + '?autoplay=1'
else
'//www.youtube-nocookie.com/embed/' + v
end
content_tag(:iframe, '', width: width, height: height, src: src, frameborder: 0, allowfullscreen: 'true')
end
end
end
end

View file

@ -0,0 +1,82 @@
namespace :redmine do
namespace :additionals do
desc <<-DESCRIPTION
Drop plugin settings.
Example:
bundle exec rake redmine:additionals:drop_settings RAILS_ENV=production plugin="redmine_plugin_example"
DESCRIPTION
task drop_settings: :environment do
plugin = ENV['plugin']
if plugin.blank?
puts 'Parameter plugin is required.'
exit 2
end
Setting.where(name: "plugin_#{plugin}".to_sym).destroy_all
Setting.clear_cache
puts "Setting for plugin #{plugin} has been dropped."
end
desc <<-DESCRIPTION
Set settings.
Example for value:
bundle exec rake redmine:additionals:setting_set RAILS_ENV=production name="additionals" setting="external_urls" value="2"
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
task setting_set: :environment do
name = ENV['name'] ||= 'redmine'
setting = ENV['setting']
value = if ENV['values'].present?
ENV['values'].split(',')
else
ENV['value']
end
if name.blank? || setting.blank? || value.blank?
puts 'Parameters setting and value are required.'
exit 2
end
if name == 'redmine'
Setting[setting.to_sym] = value
else
plugin_name = "plugin_#{name}".to_sym
plugin_settings = Setting[plugin_name]
plugin_settings[setting] = value
Setting[plugin_name] = plugin_settings
end
end
desc <<-DESCRIPTION
Get settings.
Example for plugin setting:
bundle exec rake redmine:additionals:setting_get RAILS_ENV=production name="additionals" setting="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:
bundle exec rake redmine:additionals:setting_get RAILS_ENV=production setting="app_title"
DESCRIPTION
task setting_get: :environment do
name = ENV['name'] ||= 'redmine'
setting = ENV['setting']
if setting.blank?
puts 'Parameters setting is required'
exit 2
end
if name == 'redmine'
puts Setting.send(setting)
else
plugin_name = "plugin_#{name}".to_sym
plugin_settings = Setting[plugin_name]
puts plugin_settings[setting]
end
end
end
end