Redmine 4.1.1

This commit is contained in:
Manuel Cillero 2020-11-22 21:20:06 +01:00
parent 33e7b881a5
commit 3d976f1b3b
1593 changed files with 36180 additions and 19489 deletions

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -21,9 +21,9 @@ module ActivitiesHelper
def sort_activity_events(events)
events_by_group = events.group_by(&:event_group)
sorted_events = []
events.sort {|x, y| y.event_datetime <=> x.event_datetime}.each do |event|
events.sort_by(&:event_datetime).reverse_each do |event|
if group_events = events_by_group.delete(event.event_group)
group_events.sort {|x, y| y.event_datetime <=> x.event_datetime}.each_with_index do |e, i|
group_events.sort_by(&:event_datetime).reverse.each_with_index do |e, i|
sorted_events << [e, i > 0]
end
end

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -23,7 +23,6 @@ require 'cgi'
module ApplicationHelper
include Redmine::WikiFormatting::Macros::Definitions
include Redmine::I18n
include GravatarHelper::PublicMethods
include Redmine::Pagination::Helper
include Redmine::SudoMode::Helper
include Redmine::Themes::Helper
@ -53,7 +52,8 @@ module ApplicationHelper
if user.is_a?(User)
name = h(user.name(options[:format]))
if user.active? || (User.current.admin? && user.logged?)
link_to name, user_path(user), :class => user.css_classes
only_path = options[:only_path].nil? ? true : options[:only_path]
link_to name, user_url(user, :only_path => only_path), :class => user.css_classes
else
name
end
@ -62,6 +62,20 @@ module ApplicationHelper
end
end
# Displays a link to edit group page if current user is admin
# Otherwise display only the group name
def link_to_group(group, options={})
if group.is_a?(Group)
name = h(group.name)
if User.current.admin?
only_path = options[:only_path].nil? ? true : options[:only_path]
link_to name, edit_group_path(group, :only_path => only_path)
else
name
end
end
end
# Displays a link to +issue+ with its subject.
# Examples:
#
@ -97,10 +111,17 @@ module ApplicationHelper
# * :download - Force download (default: false)
def link_to_attachment(attachment, options={})
text = options.delete(:text) || attachment.filename
route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
html_options = options.slice!(:only_path)
if options.delete(:download)
route_method = :download_named_attachment_url
options[:filename] = attachment.filename
else
route_method = :attachment_url
# make sure we don't have an extraneous :filename in the options
options.delete(:filename)
end
html_options = options.slice!(:only_path, :filename)
options[:only_path] = true unless options.key?(:only_path)
url = send(route_method, attachment, attachment.filename, options)
url = send(route_method, attachment, options)
link_to text, url, html_options
end
@ -146,8 +167,8 @@ module ApplicationHelper
h(project.name)
else
link_to project.name,
project_url(project, {:only_path => true}.merge(options)),
html_options
project_url(project, {:only_path => true}.merge(options)),
html_options
end
end
@ -169,6 +190,39 @@ module ApplicationHelper
link_to_if version.visible?, format_version_name(version), version_path(version), options
end
RECORD_LINK = {
'CustomValue' => -> (custom_value) { link_to_record(custom_value.customized) },
'Document' => -> (document) { link_to(document.title, document_path(document)) },
'Group' => -> (group) { link_to(group.name, group_path(group)) },
'Issue' => -> (issue) { link_to_issue(issue, :subject => false) },
'Message' => -> (message) { link_to_message(message) },
'News' => -> (news) { link_to(news.title, news_path(news)) },
'Project' => -> (project) { link_to_project(project) },
'User' => -> (user) { link_to_user(user) },
'Version' => -> (version) { link_to_version(version) },
'WikiPage' => -> (wiki_page) { link_to(wiki_page.pretty_title, project_wiki_page_path(wiki_page.project, wiki_page.title)) }
}
def link_to_record(record)
if link = RECORD_LINK[record.class.name]
self.instance_exec(record, &link)
end
end
ATTACHMENT_CONTAINER_LINK = {
# Custom list, since project/version attachments are listed in the files
# view and not in the project/milestone view
'Project' => -> (project) { link_to(l(:project_module_files), project_files_path(project)) },
'Version' => -> (version) { link_to(l(:project_module_files), project_files_path(version.project)) },
}
def link_to_attachment_container(attachment_container)
if link = ATTACHMENT_CONTAINER_LINK[attachment_container.class.name] ||
RECORD_LINK[attachment_container.class.name]
self.instance_exec(attachment_container, &link)
end
end
# Helper that formats object for html or text rendering
def format_object(object, html=true, &block)
if block_given?
@ -228,22 +282,57 @@ module ApplicationHelper
:srcset => "#{thumbnail_path(attachment, :size => thumbnail_size * 2)} 2x",
:style => "max-width: #{thumbnail_size}px; max-height: #{thumbnail_size}px;"
),
named_attachment_path(
attachment,
attachment.filename
attachment_path(
attachment
),
:title => attachment.filename
)
end
def toggle_link(name, id, options={})
onclick = "$('##{id}').toggle(); "
onclick = +"$('##{id}').toggle(); "
onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
onclick << "$(window).scrollTop($('##{options[:focus]}').position().top); " if options[:scroll]
onclick << "return false;"
link_to(name, "#", :onclick => onclick)
end
def link_to_previous_month(year, month, options={})
target_year, target_month = if month == 1
[year - 1, 12]
else
[year, month - 1]
end
name = if target_month == 12
"#{month_name(target_month)} #{target_year}"
else
month_name(target_month)
end
link_to_month(("« " + name), target_year, target_month, options)
end
def link_to_next_month(year, month, options={})
target_year, target_month = if month == 12
[year + 1, 1]
else
[year, month + 1]
end
name = if target_month == 1
"#{month_name(target_month)} #{target_year}"
else
month_name(target_month)
end
link_to_month((name + " »"), target_year, target_month, options)
end
def link_to_month(link_name, year, month, options={})
link_to(link_name, {:params => request.query_parameters.merge(:year => year, :month => month)}, options)
end
# Used to format item titles on the activity view
def format_activity_title(text)
text
@ -281,19 +370,19 @@ module ApplicationHelper
# The given collection may be a subset of the whole project tree
# (eg. some intermediate nodes are private and can not be seen)
def render_project_nested_lists(projects, &block)
s = ''
s = +''
if projects.any?
ancestors = []
original_project = @project
projects.sort_by(&:lft).each do |project|
# set the project environment to please macros.
@project = project
if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
if ancestors.empty? || project.is_descendant_of?(ancestors.last)
s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
else
ancestors.pop
s << "</li>"
while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
while ancestors.any? && !project.is_descendant_of?(ancestors.last)
ancestors.pop
s << "</ul></li>\n"
end
@ -311,12 +400,17 @@ module ApplicationHelper
end
def render_page_hierarchy(pages, node=nil, options={})
content = ''
content = +''
if pages[node]
content << "<ul class=\"pages-hierarchy\">\n"
pages[node].each do |page|
content << "<li>"
content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
if controller.controller_name == 'wiki' && controller.action_name == 'export'
href = "##{page.title}"
else
href = {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil}
end
content << link_to(h(page.pretty_title), href,
:title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
content << "</li>\n"
@ -328,7 +422,7 @@ module ApplicationHelper
# Renders flash messages
def render_flash_messages
s = ''
s = +''
flash.each do |k,v|
s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
end
@ -348,6 +442,17 @@ module ApplicationHelper
end
end
# Returns the tab action depending on the tab properties
def get_tab_action(tab)
if tab[:onclick]
return tab[:onclick]
elsif tab[:partial]
return "showTab('#{tab[:name]}', this.href)"
else
return nil
end
end
# Returns the default scope for the quick search form
# Could be 'all', 'my_projects', 'subprojects' or nil (current project)
def default_search_project_scope
@ -366,12 +471,35 @@ module ApplicationHelper
end
def render_projects_for_jump_box(projects, selected=nil)
jump_box = Redmine::ProjectJumpBox.new User.current
query = params[:q] if request.format.js?
bookmarked = jump_box.bookmarked_projects(query)
recents = jump_box.recently_used_projects(query)
projects = projects - (recents + bookmarked)
projects_label = (bookmarked.any? || recents.any?) ? :label_optgroup_others : :label_project_plural
jump = params[:jump].presence || current_menu_item
s = ''.html_safe
project_tree(projects) do |project, level|
s = (+'').html_safe
build_project_link = ->(project, level = 0){
padding = level * 16
text = content_tag('span', project.name, :style => "padding-left:#{padding}px;")
s << link_to(text, project_path(project, :jump => jump), :title => project.name, :class => (project == selected ? 'selected' : nil))
s << link_to(text, project_path(project, :jump => jump),
:title => project.name,
:class => (project == selected ? 'selected' : nil))
}
[
[bookmarked, :label_optgroup_bookmarks, true],
[recents, :label_optgroup_recents, false],
[projects, projects_label, true]
].each do |projects, label, is_tree|
next if projects.blank?
s << content_tag(:strong, l(label))
if is_tree
project_tree(projects, &build_project_link)
else
# we do not want to render recently used projects as a tree, but in the
# order they were used (most recent first)
projects.each(&build_project_link)
end
end
s
end
@ -384,17 +512,21 @@ module ApplicationHelper
end
text ||= l(:label_jump_to_a_project)
url = autocomplete_projects_path(:format => 'js', :jump => current_menu_item)
trigger = content_tag('span', text, :class => 'drdn-trigger')
q = text_field_tag('q', '', :id => 'projects-quick-search', :class => 'autocomplete', :data => {:automcomplete_url => url}, :autocomplete => 'off')
all = link_to(l(:label_project_all), projects_path(:jump => current_menu_item), :class => (@project.nil? && controller.class.main_menu ? 'selected' : nil))
content = content_tag('div',
content_tag('div', q, :class => 'quick-search') +
content_tag('div', render_projects_for_jump_box(projects, @project), :class => 'drdn-items projects selection') +
content_tag('div', all, :class => 'drdn-items all-projects selection'),
:class => 'drdn-content'
q = text_field_tag('q', '', :id => 'projects-quick-search',
:class => 'autocomplete',
:data => {:automcomplete_url => url},
:autocomplete => 'off')
all = link_to(l(:label_project_all), projects_path(:jump => current_menu_item),
:class => (@project.nil? && controller.class.main_menu ? 'selected' : nil))
content =
content_tag('div',
content_tag('div', q, :class => 'quick-search') +
content_tag('div', render_projects_for_jump_box(projects, @project),
:class => 'drdn-items projects selection') +
content_tag('div', all, :class => 'drdn-items all-projects selection'),
:class => 'drdn-content'
)
content_tag('div', trigger + content, :id => "project-jump", :class => "drdn")
end
@ -428,20 +560,20 @@ module ApplicationHelper
end
def principals_check_box_tags(name, principals)
s = ''
s = +''
principals.each do |principal|
s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } <span class='name icon icon-#{principal.class.name.downcase}'></span>#{h principal}</label>\n"
end
s.html_safe
end
# Returns a string for users/groups option tags
def principals_options_for_select(collection, selected=nil)
s = ''
s = +''
if collection.include?(User.current)
s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
end
groups = ''
groups = +''
collection.sort.each do |element|
selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
(element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
@ -471,7 +603,7 @@ module ApplicationHelper
end
def anchor(text)
text.to_s.gsub(' ', '_')
text.to_s.tr(' ', '_')
end
def html_hours(text)
@ -504,33 +636,15 @@ module ApplicationHelper
str.blank? ? nil : str
end
def reorder_links(name, url, method = :post)
# TODO: remove associated styles from application.css too
ActiveSupport::Deprecation.warn "Application#reorder_links will be removed in Redmine 4."
link_to(l(:label_sort_highest),
url.merge({"#{name}[move_to]" => 'highest'}), :method => method,
:title => l(:label_sort_highest), :class => 'icon-only icon-move-top') +
link_to(l(:label_sort_higher),
url.merge({"#{name}[move_to]" => 'higher'}), :method => method,
:title => l(:label_sort_higher), :class => 'icon-only icon-move-up') +
link_to(l(:label_sort_lower),
url.merge({"#{name}[move_to]" => 'lower'}), :method => method,
:title => l(:label_sort_lower), :class => 'icon-only icon-move-down') +
link_to(l(:label_sort_lowest),
url.merge({"#{name}[move_to]" => 'lowest'}), :method => method,
:title => l(:label_sort_lowest), :class => 'icon-only icon-move-bottom')
end
def reorder_handle(object, options={})
data = {
:reorder_url => options[:url] || url_for(object),
:reorder_param => options[:param] || object.class.name.underscore
}
content_tag('span', '',
:class => "sort-handle",
:data => data,
:title => l(:button_sort))
:class => "icon-only icon-sort-handle sort-handle",
:data => data,
:title => l(:button_sort))
end
def breadcrumb(*args)
@ -600,6 +714,17 @@ module ApplicationHelper
end
end
def actions_dropdown(&block)
content = capture(&block)
if content.present?
trigger = content_tag('span', l(:button_actions), :class => 'icon-only icon-actions', :title => l(:button_actions))
trigger = content_tag('span', trigger, :class => 'drdn-trigger')
content = content_tag('div', content, :class => 'drdn-items')
content = content_tag('div', content, :class => 'drdn-content')
content_tag('span', trigger + content, :class => 'drdn')
end
end
# Returns the theme, controller name, and action as css classes for the
# HTML body.
def body_css_classes
@ -609,8 +734,10 @@ module ApplicationHelper
end
css << 'project-' + @project.identifier if @project && @project.identifier.present?
css << 'has-main-menu' if display_main_menu?(@project)
css << 'controller-' + controller_name
css << 'action-' + action_name
css << 'avatars-' + (Setting.gravatar_enabled? ? 'on' : 'off')
if UserPreference::TEXTAREA_FONT_OPTIONS.include?(User.current.pref.textarea_font)
css << "textarea-#{User.current.pref.textarea_font}"
end
@ -661,7 +788,7 @@ module ApplicationHelper
@current_section = 0 if options[:edit_section_links]
parse_sections(text, project, obj, attr, only_path, options)
text = parse_non_pre_blocks(text, obj, macros) do |text|
text = parse_non_pre_blocks(text, obj, macros, options) do |text|
[:parse_inline_attachments, :parse_hires_images, :parse_wiki_links, :parse_redmine_links].each do |method_name|
send method_name, text, project, obj, attr, only_path, options
end
@ -675,18 +802,18 @@ module ApplicationHelper
text.html_safe
end
def parse_non_pre_blocks(text, obj, macros)
def parse_non_pre_blocks(text, obj, macros, options={})
s = StringScanner.new(text)
tags = []
parsed = ''
parsed = +''
while !s.eos?
s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
if tags.empty?
yield text
inject_macros(text, obj, macros) if macros.any?
inject_macros(text, obj, macros, true, options) if macros.any?
else
inject_macros(text, obj, macros, false) if macros.any?
inject_macros(text, obj, macros, false, options) if macros.any?
end
parsed << text
if tag
@ -724,7 +851,7 @@ module ApplicationHelper
attachments += obj.attachments if obj.respond_to?(:attachments)
if attachments.present?
text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
filename, ext, alt, alttext = $1.downcase, $2, $3, $4
filename, ext, alt, alttext = $1, $2, $3, $4
# search for the picture in attachments
if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
@ -751,10 +878,17 @@ module ApplicationHelper
# [[project:mypage]]
# [[project:mypage|mytext]]
def parse_wiki_links(text, project, obj, attr, only_path, options)
text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
text.gsub!(/(!)?(\[\[([^\n\|]+?)(\|([^\n\|]+?))?\]\])/) do |m|
link_project = project
esc, all, page, title = $1, $2, $3, $5
if esc.nil?
page = CGI.unescapeHTML(page)
if page =~ /^\#(.+)$/
anchor = sanitize_anchor_name($1)
url = "##{anchor}"
next link_to(title.present? ? title.html_safe : h(page), url, :class => 'wiki-page')
end
if page =~ /^([^\:]+)\:(.*)$/
identifier, page = $1, $2
link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
@ -770,19 +904,27 @@ module ApplicationHelper
anchor = sanitize_anchor_name(anchor) if anchor.present?
# check if page exists
wiki_page = link_project.wiki.find_page(page)
url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
"##{anchor}"
else
case options[:wiki_links]
when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
url =
if anchor.present? && wiki_page.present? &&
(obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) &&
obj.page == wiki_page
"##{anchor}"
else
wiki_page_id = page.present? ? Wiki.titleize(page) : nil
parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
:id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
case options[:wiki_links]
when :local
"#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
when :anchor
# used for single-file wiki export
"##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '')
else
wiki_page_id = page.present? ? Wiki.titleize(page) : nil
parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
url_for(:only_path => only_path, :controller => 'wiki',
:action => 'show', :project_id => link_project,
:id => wiki_page_id, :version => nil, :anchor => anchor,
:parent => parent)
end
end
end
link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
else
# project or wiki doesn't exist
@ -799,6 +941,7 @@ module ApplicationHelper
# Examples:
# Issues:
# #52 -> Link to issue #52
# ##52 -> Link to issue #52, including the issue's subject
# Changesets:
# r52 -> Link to revision 52
# commit:a85130f -> Link to scmid starting with a85130f
@ -886,17 +1029,28 @@ module ApplicationHelper
:title => truncate_single_line_raw(changeset.comments, 100))
end
end
elsif sep == '#'
elsif sep == '#' || sep == '##'
oid = identifier.to_i
case prefix
when nil
if oid.to_s == identifier &&
issue = Issue.visible.find_by_id(oid)
anchor = comment_id ? "note-#{comment_id}" : nil
link = link_to("##{oid}#{comment_suffix}",
issue_url(issue, :only_path => only_path, :anchor => anchor),
:class => issue.css_classes,
:title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
url = issue_url(issue, :only_path => only_path, :anchor => anchor)
link =
if sep == '##'
link_to("#{issue.tracker.name} ##{oid}#{comment_suffix}: #{issue.subject}",
url,
:class => issue.css_classes,
:title => "#{l(:field_status)}: #{issue.status.name}")
else
link_to("##{oid}#{comment_suffix}",
url,
:class => issue.css_classes,
:title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
end
elsif identifier == 'note'
link = link_to("#note-#{comment_id}", "#note-#{comment_id}")
end
when 'document'
if document = Document.visible.find_by_id(oid)
@ -923,8 +1077,8 @@ module ApplicationHelper
link = link_to_project(p, {:only_path => only_path}, :class => 'project')
end
when 'user'
u = User.visible.where(:id => oid, :type => 'User').first
link = link_to_user(u) if u
u = User.visible.find_by(:id => oid, :type => 'User')
link = link_to_user(u, :only_path => only_path) if u
end
elsif sep == ':'
name = remove_double_quotes(identifier)
@ -956,19 +1110,29 @@ module ApplicationHelper
end
if prefix == 'commit'
if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
:class => 'changeset',
:title => truncate_single_line_raw(changeset.comments, 100)
link = link_to(
h("#{project_prefix}#{repo_prefix}#{name}"),
{:only_path => only_path, :controller => 'repositories',
:action => 'revision', :id => project,
:repository_id => repository.identifier_param,
:rev => changeset.identifier},
:class => 'changeset',
:title => truncate_single_line_raw(changeset.comments, 100))
end
else
if repository && User.current.allowed_to?(:browse_repository, project)
name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
path, rev, anchor = $1, $3, $5
link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
:path => to_path_param(path),
:rev => rev,
:anchor => anchor},
:class => (prefix == 'export' ? 'source download' : 'source')
link =
link_to(
h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"),
{:only_path => only_path, :controller => 'repositories',
:action => (prefix == 'export' ? 'raw' : 'entry'),
:id => project, :repository_id => repository.identifier_param,
:path => to_path_param(path),
:rev => rev,
:anchor => anchor},
:class => (prefix == 'export' ? 'source download' : 'source'))
end
end
repo_prefix = nil
@ -984,13 +1148,13 @@ module ApplicationHelper
link = link_to_project(p, {:only_path => only_path}, :class => 'project')
end
when 'user'
u = User.visible.where(:login => name, :type => 'User').first
link = link_to_user(u) if u
u = User.visible.find_by("LOWER(login) = :s AND type = 'User'", :s => name.downcase)
link = link_to_user(u, :only_path => only_path) if u
end
elsif sep == "@"
name = remove_double_quotes(identifier)
u = User.visible.where(:login => name, :type => 'User').first
link = link_to_user(u) if u
u = User.visible.find_by("LOWER(login) = :s AND type = 'User'", :s => name.downcase)
link = link_to_user(u, :only_path => only_path) if u
end
end
(leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
@ -999,7 +1163,7 @@ module ApplicationHelper
end
LINKS_RE =
%r{
%r{
<a( [^>]+?)?>(?<tag_content>.*?)</a>|
(?<leading>[\s\(,\-\[\>]|^)
(?<esc>!)?
@ -1007,14 +1171,14 @@ module ApplicationHelper
(?<prefix>attachment|document|version|forum|news|message|project|commit|source|export|user)?
(
(
(?<sep1>\#)|
(?<sep1>\#\#?)|
(
(?<repo_prefix>(?<repo_identifier>[a-z0-9\-_]+)\|)?
(?<sep2>r)
)
)
(
(?<identifier1>\d+)
(?<identifier1>((\d)+|(note)))
(?<comment_suffix>
(\#note)?
-(?<comment_id>\d+)
@ -1026,7 +1190,7 @@ module ApplicationHelper
)|
(
(?<sep4>@)
(?<identifier3>[a-z0-9_\-@\.]*)
(?<identifier3>[A-Za-z0-9_\-@\.]*)
)
)
(?=
@ -1036,7 +1200,7 @@ module ApplicationHelper
\]|
<|
$)
}x
}x
HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
def parse_sections(text, project, obj, attr, only_path, options)
@ -1045,9 +1209,13 @@ module ApplicationHelper
heading, level = $1, $2
@current_section += 1
if @current_section > 1
content_tag('div',
link_to(l(:button_edit_section), options[:edit_section_links].merge(:section => @current_section),
:class => 'icon-only icon-edit'),
content_tag(
'div',
link_to(
l(:button_edit_section),
options[:edit_section_links].merge(
:section => @current_section),
:class => 'icon-only icon-edit'),
:class => "contextual heading-#{level}",
:title => l(:button_edit_section),
:id => "section-#{@current_section}") + heading.html_safe
@ -1112,14 +1280,14 @@ module ApplicationHelper
end
# Executes and replaces macros in text
def inject_macros(text, obj, macros, execute=true)
def inject_macros(text, obj, macros, execute=true, options={})
text.gsub!(MACRO_SUB_RE) do
all, index = $1, $2.to_i
orig = macros.delete(index)
if execute && orig && orig =~ MACROS_RE
esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
if esc.nil?
h(exec_macro(macro, obj, args, block) || all)
h(exec_macro(macro, obj, args, block, options) || all)
else
h(all)
end
@ -1142,10 +1310,10 @@ module ApplicationHelper
if headings.empty?
''
else
div_class = 'toc'
div_class = +'toc'
div_class << ' right' if right_align
div_class << ' left' if left_align
out = "<ul class=\"#{div_class}\"><li><strong>#{l :label_table_of_contents}</strong></li><li>"
out = +"<ul class=\"#{div_class}\"><li><strong>#{l :label_table_of_contents}</strong></li><li>"
root = headings.map(&:first).min
current = root
started = false
@ -1197,6 +1365,13 @@ module ApplicationHelper
fields_for(*args, &proc)
end
def form_tag_html(html_options)
# Set a randomized name attribute on all form fields by default
# as a workaround to https://bugzilla.mozilla.org/show_bug.cgi?id=1279253
html_options['name'] ||= "#{html_options['id'] || 'form'}-#{SecureRandom.hex(4)}"
super
end
# Render the error messages for the given objects
def error_messages_for(*objects)
objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
@ -1206,7 +1381,7 @@ module ApplicationHelper
# Renders a list of error messages
def render_error_messages(errors)
html = ""
html = +""
if errors.present?
html << "<div id='errorExplanation'><ul>\n"
errors.each do |error|
@ -1227,39 +1402,27 @@ module ApplicationHelper
link_to l(:button_delete), url, options
end
def preview_link(url, form, target='preview', options={})
content_tag 'a', l(:label_preview), {
:href => "#",
:onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
:accesskey => accesskey(:preview)
}.merge(options)
end
def link_to_function(name, function, html_options={})
content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
end
def link_to_context_menu
link_to l(:button_actions), '#', title: l(:button_actions), class: 'icon-only icon-actions js-contextmenu'
end
# Helper to render JSON in views
def raw_json(arg)
arg.to_json.to_s.gsub('/', '\/').html_safe
end
def back_url
url = params[:back_url]
if url.nil? && referer = request.env['HTTP_REFERER']
url = CGI.unescape(referer.to_s)
# URLs that contains the utf8=[checkmark] parameter added by Rails are
# parsed as invalid by URI.parse so the redirect to the back URL would
# not be accepted (ApplicationController#validate_back_url would return
# false)
url.gsub!(/(\?|&)utf8=\u2713&?/, '\1')
end
url
def back_url_hidden_field_tag
url = validate_back_url(back_url)
hidden_field_tag('back_url', url, :id => nil) unless url.blank?
end
def back_url_hidden_field_tag
url = back_url
hidden_field_tag('back_url', url, :id => nil) unless url.blank?
def cancel_button_tag(fallback_url)
url = validate_back_url(back_url) || fallback_url
link_to l(:button_cancel), url
end
def check_all_links(form_name)
@ -1270,24 +1433,41 @@ module ApplicationHelper
def toggle_checkboxes_link(selector)
link_to_function '',
"toggleCheckboxesBySelector('#{selector}')",
:title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}",
:class => 'icon icon-checked'
"toggleCheckboxesBySelector('#{selector}')",
:title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}",
:class => 'icon icon-checked'
end
def progress_bar(pcts, options={})
pcts = [pcts, pcts] unless pcts.is_a?(Array)
pcts = pcts.collect(&:round)
pcts = pcts.collect(&:floor)
pcts[1] = pcts[1] - pcts[0]
pcts << (100 - pcts[1] - pcts[0])
titles = options[:titles].to_a
titles[0] = "#{pcts[0]}%" if titles[0].blank?
legend = options[:legend] || ''
content_tag('table',
content_tag('tr',
(pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed', :title => titles[0]) : ''.html_safe) +
(pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done', :title => titles[1]) : ''.html_safe) +
(pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo', :title => titles[2]) : ''.html_safe)
content_tag(
'table',
content_tag(
'tr',
(if pcts[0] > 0
content_tag('td', '', :style => "width: #{pcts[0]}%;",
:class => 'closed', :title => titles[0])
else
''.html_safe
end) +
(if pcts[1] > 0
content_tag('td', '', :style => "width: #{pcts[1]}%;",
:class => 'done', :title => titles[1])
else
''.html_safe
end) +
(if pcts[2] > 0
content_tag('td', '', :style => "width: #{pcts[2]}%;",
:class => 'todo', :title => titles[2])
else
''.html_safe
end)
), :class => "progress progress-#{pcts[0]}").html_safe +
content_tag('p', legend, :class => 'percent').html_safe
end
@ -1410,49 +1590,15 @@ module ApplicationHelper
!!ActionMailer::Base.perform_deliveries
end
# Returns the avatar image tag for the given +user+ if avatars are enabled
# +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
def avatar(user, options = { })
if Setting.gravatar_enabled?
options.merge!(:default => Setting.gravatar_default)
email = nil
if user.respond_to?(:mail)
email = user.mail
elsif user.to_s =~ %r{<(.+?)>}
email = $1
end
if email.present?
gravatar(email.to_s.downcase, options) rescue nil
elsif user.is_a?(AnonymousUser)
options[:size] &&= options[:size].to_s
image_tag 'anonymous.png',
GravatarHelper::DEFAULT_OPTIONS
.except(:default, :rating, :ssl).merge(options)
else
nil
end
else
''
end
end
# Returns a link to edit user's avatar if avatars are enabled
def avatar_edit_link(user, options={})
if Setting.gravatar_enabled?
url = "https://gravatar.com"
link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
end
end
def sanitize_anchor_name(anchor)
anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
end
# Returns the javascript tags that are included in the html layout head
def javascript_heads
tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
tags = javascript_include_tag('jquery-2.2.4-ui-1.11.0-ujs-5.2.3', 'tribute-3.7.3.min', 'application', 'responsive')
unless User.current.pref.warn_on_leaving_unsaved == '0'
tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
tags << "\n".html_safe + javascript_tag("$(window).on('load', function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
end
tags
end
@ -1501,9 +1647,31 @@ module ApplicationHelper
end
end
def generate_csv(&block)
decimal_separator = l(:general_csv_decimal_separator)
encoding = l(:general_csv_encoding)
def export_csv_encoding_select_tag
return if l(:general_csv_encoding).casecmp('UTF-8') == 0
options = [l(:general_csv_encoding), 'UTF-8']
content_tag(:p) do
concat(
content_tag(:label) do
concat l(:label_encoding) + ' '
concat select_tag('encoding', options_for_select(options, l(:general_csv_encoding)))
end
)
end
end
# Returns an array of error messages for bulk edited items (issues, time entries)
def bulk_edit_error_messages(items)
messages = {}
items.each do |item|
item.errors.full_messages.each do |message|
messages[message] ||= []
messages[message] << item
end
end
messages.map { |message, items|
"#{message}: " + items.map {|i| "##{i.id}"}.join(', ')
}
end
private

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -33,13 +33,12 @@ module AttachmentsHelper
# :thumbails -- display thumbnails if enabled in settings
def link_to_attachments(container, options = {})
options.assert_valid_keys(:author, :thumbnails)
attachments = if container.attachments.loaded?
container.attachments
else
container.attachments.preload(:author).to_a
end
attachments =
if container.attachments.loaded?
container.attachments
else
container.attachments.preload(:author).to_a
end
if attachments.any?
options = {
:editable => container.attachments_editable?,
@ -56,6 +55,14 @@ module AttachmentsHelper
end
end
def render_pagination
pagination_links_each @paginator do |text, parameters, options|
if att = @attachments[parameters[:page] - 1]
link_to text, named_attachment_path(att, att.filename)
end
end if @paginator
end
def render_api_attachment(attachment, api, options={})
api.attachment do
render_api_attachment_attributes(attachment, api)
@ -78,4 +85,14 @@ module AttachmentsHelper
end
api.created_on attachment.created_on
end
def render_file_content(attachment, content)
if attachment.is_markdown?
render :partial => 'common/markup', :locals => {:markup_text_formatting => 'markdown', :markup_text => content}
elsif attachment.is_textile?
render :partial => 'common/markup', :locals => {:markup_text_formatting => 'textile', :markup_text => content}
else
render :partial => 'common/file', :locals => {:content => content, :filename => attachment.filename}
end
end
end

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -0,0 +1,75 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module AvatarsHelper
include GravatarHelper::PublicMethods
def assignee_avatar(user, options={})
return '' unless user
options.merge!(:title => l(:field_assigned_to) + ": " + user.name)
avatar(user, options).to_s.html_safe
end
def author_avatar(user, options={})
return '' unless user
options.merge!(:title => l(:field_author) + ": " + user.name)
avatar(user, options).to_s.html_safe
end
# Returns the avatar image tag for the given +user+ if avatars are enabled
# +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
def avatar(user, options = { })
if Setting.gravatar_enabled?
options.merge!(:default => Setting.gravatar_default)
options[:class] = GravatarHelper::DEFAULT_OPTIONS[:class] + " " + options[:class] if options[:class]
email = nil
if user.respond_to?(:mail)
email = user.mail
options[:title] = user.name unless options[:title]
elsif user.to_s =~ %r{<(.+?)>}
email = $1
end
if email.present?
gravatar(email.to_s.downcase, options) rescue nil
elsif user.is_a?(AnonymousUser)
anonymous_avatar(options)
else
nil
end
else
''
end
end
# Returns a link to edit user's avatar if avatars are enabled
def avatar_edit_link(user, options={})
if Setting.gravatar_enabled?
url = Redmine::Configuration['avatar_server_url']
link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
end
end
private
def anonymous_avatar(options={})
image_tag 'anonymous.png', GravatarHelper::DEFAULT_OPTIONS.except(:default, :rating, :ssl).merge(options)
end
end

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -18,41 +18,12 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module CalendarsHelper
def link_to_previous_month(year, month, options={})
target_year, target_month = if month == 1
[year - 1, 12]
else
[year, month - 1]
end
include Redmine::Utils::DateCalculation
name = if target_month == 12
"#{month_name(target_month)} #{target_year}"
else
"#{month_name(target_month)}"
end
# \xc2\xab(utf-8) = &#171;
link_to_month(("\xc2\xab " + name), target_year, target_month, options)
end
def link_to_next_month(year, month, options={})
target_year, target_month = if month == 12
[year + 1, 1]
else
[year, month + 1]
end
name = if target_month == 1
"#{month_name(target_month)} #{target_year}"
else
"#{month_name(target_month)}"
end
# \xc2\xbb(utf-8) = &#187;
link_to_month((name + " \xc2\xbb"), target_year, target_month, options)
end
def link_to_month(link_name, year, month, options={})
link_to(link_name, {:params => request.query_parameters.merge(:year => year, :month => month)}, options)
def calendar_day_css_classes(calendar, day)
css = day.month==calendar.month ? +'even' : +'odd'
css << " today" if User.current.today == day
css << " nwday" if non_working_week_days.include?(day.cwday)
css
end
end

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -21,30 +21,38 @@ module ContextMenusHelper
def context_menu_link(name, url, options={})
options[:class] ||= ''
if options.delete(:selected)
options[:class] << ' icon-checked disabled'
options[:class] += ' icon-checked disabled'
options[:disabled] = true
end
if options.delete(:disabled)
options.delete(:method)
options.delete(:data)
options[:onclick] = 'return false;'
options[:class] << ' disabled'
options[:class] += ' disabled'
url = '#'
end
link_to h(name), url, options
end
def bulk_update_custom_field_context_menu_link(field, text, value)
context_menu_link h(text),
bulk_update_issues_path(:ids => @issue_ids, :issue => {'custom_field_values' => {field.id => value}}, :back_url => @back),
context_menu_link(
h(text),
bulk_update_issues_path(:ids => @issue_ids,
:issue => {'custom_field_values' => {field.id => value}},
:back_url => @back),
:method => :post,
:selected => (@issue && @issue.custom_field_value(field) == value)
)
end
def bulk_update_time_entry_custom_field_context_menu_link(field, text, value)
context_menu_link h(text),
bulk_update_time_entries_path(:ids => @time_entries.map(&:id).sort, :time_entry => {'custom_field_values' => {field.id => value}}, :back_url => @back),
context_menu_link(
h(text),
bulk_update_time_entries_path(:ids => @time_entries.map(&:id).sort,
:time_entry => {'custom_field_values' => {field.id => value}},
:back_url => @back),
:method => :post,
:selected => (@time_entry && @time_entry.custom_field_value(field) == value)
)
end
end

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -55,7 +55,7 @@ module CustomFieldsHelper
items = []
items << [l(:label_custom_field_plural), custom_fields_path]
items << [l(custom_field.type_name), custom_fields_path(:tab => custom_field.class.name)] if custom_field
items << (custom_field.nil? || custom_field.new_record? ? l(:label_custom_field_new) : custom_field.name)
items << (custom_field.nil? || custom_field.new_record? ? l(:label_custom_field_new) : custom_field.name)
title(*items)
end
@ -69,7 +69,7 @@ module CustomFieldsHelper
def custom_field_tag_name(prefix, custom_field)
name = "#{prefix}[custom_field_values][#{custom_field.id}]"
name << "[]" if custom_field.multiple?
name += "[]" if custom_field.multiple?
name
end
@ -79,11 +79,22 @@ module CustomFieldsHelper
# Return custom field html tag corresponding to its format
def custom_field_tag(prefix, custom_value)
custom_value.custom_field.format.edit_tag self,
css = "#{custom_value.custom_field.field_format}_cf"
data = nil
if custom_value.custom_field.full_text_formatting?
css += ' wiki-edit'
data = {
:auto_complete => true,
:issues_url => auto_complete_issues_path(:project_id => custom_value.customized.project, :q => '')
} if custom_value.customized&.try(:project)
end
custom_value.custom_field.format.edit_tag(
self,
custom_field_tag_id(prefix, custom_value.custom_field),
custom_field_tag_name(prefix, custom_value.custom_field),
custom_value,
:class => "#{custom_value.custom_field.field_format}_cf"
:class => css,
:data => data)
end
# Return custom field name tag
@ -92,16 +103,16 @@ module CustomFieldsHelper
css = title ? "field-description" : nil
content_tag 'span', custom_field.name, :title => title, :class => css
end
# Return custom field label tag
def custom_field_label_tag(name, custom_value, options={})
required = options[:required] || custom_value.custom_field.is_required?
for_tag_id = options.fetch(:for_tag_id, "#{name}_custom_field_values_#{custom_value.custom_field.id}")
content = custom_field_name_tag custom_value.custom_field
content_tag "label", content +
content_tag(
"label", content +
(required ? " <span class=\"required\">*</span>".html_safe : ""),
:for => for_tag_id
:for => for_tag_id)
end
# Return custom field tag with its label tag
@ -117,13 +128,25 @@ module CustomFieldsHelper
# Returns the custom field tag for when bulk editing objects
def custom_field_tag_for_bulk_edit(prefix, custom_field, objects=nil, value='')
custom_field.format.bulk_edit_tag self,
custom_field.format.bulk_edit_tag(
self,
custom_field_tag_id(prefix, custom_field),
custom_field_tag_name(prefix, custom_field),
custom_field,
objects,
value,
:class => "#{custom_field.field_format}_cf"
:class => "#{custom_field.field_format}_cf")
end
# Returns custom field value tag
def custom_field_value_tag(value)
attr_value = show_value(value)
if !attr_value.blank? && value.custom_field.full_text_formatting?
content_tag('div', attr_value, :class => 'wiki')
else
attr_value
end
end
# Return a string used to display a custom value
@ -180,4 +203,15 @@ module CustomFieldsHelper
end
form.select :edit_tag_style, select_options, :label => :label_display
end
def select_type_radio_buttons(default_type)
if CUSTOM_FIELDS_TABS.none? {|tab| tab[:name] == default_type}
default_type = 'IssueCustomField'
end
custom_field_type_options.map do |name, type|
content_tag(:label, :style => 'display:block;') do
radio_button_tag('type', type, type == default_type) + name
end
end.join("\n").html_safe
end
end

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -22,17 +22,19 @@ module EmailAddressesHelper
# Returns a link to enable or disable notifications for the address
def toggle_email_address_notify_link(address)
if address.notify?
link_to l(:label_disable_notifications),
link_to(
l(:label_disable_notifications),
user_email_address_path(address.user, address, :notify => '0'),
:method => :put, :remote => true,
:title => l(:label_disable_notifications),
:class => 'icon-only icon-email'
:class => 'icon-only icon-email')
else
link_to l(:label_enable_notifications),
link_to(
l(:label_enable_notifications),
user_email_address_path(address.user, address, :notify => '1'),
:method => :put, :remote => true,
:title => l(:label_enable_notifications),
:class => 'icon-only icon-email-disabled'
:class => 'icon-only icon-email-disabled')
end
end
end

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -23,18 +23,20 @@ module GanttHelper
case in_or_out
when :in
if gantt.zoom < 4
link_to l(:text_zoom_in),
link_to(
l(:text_zoom_in),
{:params => request.query_parameters.merge(gantt.params.merge(:zoom => (gantt.zoom + 1)))},
:class => 'icon icon-zoom-in'
:class => 'icon icon-zoom-in')
else
content_tag(:span, l(:text_zoom_in), :class => 'icon icon-zoom-in').html_safe
end
when :out
if gantt.zoom > 1
link_to l(:text_zoom_out),
link_to(
l(:text_zoom_out),
{:params => request.query_parameters.merge(gantt.params.merge(:zoom => (gantt.zoom - 1)))},
:class => 'icon icon-zoom-out'
:class => 'icon icon-zoom-out')
else
content_tag(:span, l(:text_zoom_out), :class => 'icon icon-zoom-out').html_safe
end

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -31,16 +31,14 @@ module GroupsHelper
principal_count = scope.count
principal_pages = Redmine::Pagination::Paginator.new principal_count, limit, params['page']
principals = scope.offset(principal_pages.offset).limit(principal_pages.per_page).to_a
s = content_tag('div',
s = content_tag(
'div',
content_tag('div', principals_check_box_tags('user_ids[]', principals), :id => 'principals'),
:class => 'objects-selection'
)
links = pagination_links_full(principal_pages, principal_count, :per_page_links => false) {|text, parameters, options|
link_to text, autocomplete_for_user_group_path(group, parameters.merge(:q => params[:q], :format => 'js')), :remote => true
}
s + content_tag('span', links, :class => 'pagination')
end
end

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -18,6 +18,14 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module ImportsHelper
def import_title
l(:"label_import_#{import_partial_prefix}")
end
def import_partial_prefix
@import.class.name.sub('Import', '').underscore.pluralize
end
def options_for_mapping_select(import, field, options={})
tags = "".html_safe
blank_text = options[:required] ? "-- #{l(:actionview_instancetag_blank_option)} --" : "&nbsp;".html_safe
@ -25,7 +33,7 @@ module ImportsHelper
tags << options_for_select(import.columns_options, import.mapping[field])
if values = options[:values]
tags << content_tag('option', '--', :disabled => true)
tags << options_for_select(values.map {|text, value| [text, "value:#{value}"]}, import.mapping[field])
tags << options_for_select(values.map {|text, value| [text, "value:#{value}"]}, import.mapping[field] || options[:default_value])
end
tags
end

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -20,6 +20,6 @@
module IssueRelationsHelper
def collection_for_relation_type_select
values = IssueRelation::TYPES
values.keys.sort{|x,y| values[x][:order] <=> values[y][:order]}.collect{|k| [l(values[k][:name]), k]}
values.keys.sort_by{|k| values[k][:order]}.collect{|k| [l(values[k][:name]), k]}
end
end

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -24,7 +24,8 @@ module IssuesHelper
def issue_list(issues, &block)
ancestors = []
issues.each do |issue|
while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
while ancestors.any? &&
!issue.is_descendant_of?(ancestors.last)
ancestors.pop
end
yield issue, ancestors.size
@ -35,7 +36,8 @@ module IssuesHelper
def grouped_issue_list(issues, query, &block)
ancestors = []
grouped_query_results(issues, query) do |issue, group_name, group_count, group_totals|
while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
while ancestors.any? &&
!issue.is_descendant_of?(ancestors.last)
ancestors.pop
end
yield issue, ancestors.size, group_name, group_count, group_totals
@ -62,10 +64,10 @@ module IssuesHelper
link_to_issue(issue) + "<br /><br />".html_safe +
"<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
"<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
"<strong>#{@cached_label_status}</strong>: #{h(issue.status.name) + (" (#{format_date(issue.closed_on)})" if issue.closed?)}<br />".html_safe +
"<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
"<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
"<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
"<strong>#{@cached_label_assigned_to}</strong>: #{avatar(issue.assigned_to, :size => '13', :title => l(:field_assigned_to)) if issue.assigned_to} #{h(issue.assigned_to)}<br />".html_safe +
"<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
end
@ -74,7 +76,7 @@ module IssuesHelper
end
def render_issue_subject_with_tree(issue)
s = ''
s = +''
ancestors = issue.root? ? [] : issue.ancestors.visible.to_a
ancestors.each do |ancestor|
s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
@ -82,7 +84,7 @@ module IssuesHelper
s << '<div>'
subject = h(issue.subject)
if issue.is_private?
subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
subject = subject + ' ' + content_tag('span', l(:field_is_private), :class => 'badge badge-private private')
end
s << content_tag('h3', subject)
s << '</div>' * (ancestors.size + 1)
@ -90,17 +92,52 @@ module IssuesHelper
end
def render_descendants_tree(issue)
s = '<table class="list issues odd-even">'
issue_list(issue.descendants.visible.preload(:status, :priority, :tracker, :assigned_to).sort_by(&:lft)) do |child, level|
css = "issue issue-#{child.id} hascontextmenu #{child.css_classes}"
manage_relations = User.current.allowed_to?(:manage_subtasks, issue.project)
s = +'<table class="list issues odd-even">'
issue_list(
issue.descendants.visible.
preload(:status, :priority, :tracker,
:assigned_to).sort_by(&:lft)) do |child, level|
css = +"issue issue-#{child.id} hascontextmenu #{child.css_classes}"
css << " idnt idnt-#{level}" if level > 0
s << content_tag('tr',
content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
content_tag('td', link_to_issue(child, :project => (issue.project_id != child.project_id)), :class => 'subject', :style => 'width: 50%') +
buttons =
if manage_relations
link_to(l(:label_delete_link_to_subtask),
issue_path(
{:id => child.id, :issue => {:parent_issue_id => ''},
:back_url => issue_path(issue.id), :no_flash => '1'}),
:method => :put,
:data => {:confirm => l(:text_are_you_sure)},
:title => l(:label_delete_link_to_subtask),
:class => 'icon-only icon-link-break'
)
else
"".html_safe
end
buttons << link_to_context_menu
s <<
content_tag(
'tr',
content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil),
:class => 'checkbox') +
content_tag('td',
link_to_issue(
child,
:project => (issue.project_id != child.project_id)),
:class => 'subject') +
content_tag('td', h(child.status), :class => 'status') +
content_tag('td', link_to_user(child.assigned_to), :class => 'assigned_to') +
content_tag('td', child.disabled_core_fields.include?('done_ratio') ? '' : progress_bar(child.done_ratio), :class=> 'done_ratio'),
:class => css)
content_tag('td', format_date(child.start_date), :class => 'start_date') +
content_tag('td', format_date(child.due_date), :class => 'due_date') +
content_tag('td',
(if child.disabled_core_fields.include?('done_ratio')
''
else
progress_bar(child.done_ratio)
end),
:class=> 'done_ratio') +
content_tag('td', buttons, :class => 'buttons'),
:class => css)
end
s << '</table>'
s.html_safe
@ -109,32 +146,55 @@ module IssuesHelper
# Renders the list of related issues on the issue details view
def render_issue_relations(issue, relations)
manage_relations = User.current.allowed_to?(:manage_issue_relations, issue.project)
s = ''.html_safe
relations.each do |relation|
other_issue = relation.other_issue(issue)
css = "issue hascontextmenu #{other_issue.css_classes}"
link = manage_relations ? link_to(l(:label_relation_delete),
relation_path(relation),
:remote => true,
:method => :delete,
:data => {:confirm => l(:text_are_you_sure)},
:title => l(:label_relation_delete),
:class => 'icon-only icon-link-break'
) : nil
s << content_tag('tr',
content_tag('td', check_box_tag("ids[]", other_issue.id, false, :id => nil), :class => 'checkbox') +
content_tag('td', relation.to_s(@issue) {|other| link_to_issue(other, :project => Setting.cross_project_issue_relations?)}.html_safe, :class => 'subject', :style => 'width: 50%') +
buttons =
if manage_relations
link_to(
l(:label_relation_delete),
relation_path(relation),
:remote => true,
:method => :delete,
:data => {:confirm => l(:text_are_you_sure)},
:title => l(:label_relation_delete),
:class => 'icon-only icon-link-break'
)
else
"".html_safe
end
buttons << link_to_context_menu
s <<
content_tag(
'tr',
content_tag('td',
check_box_tag(
"ids[]", other_issue.id,
false, :id => nil),
:class => 'checkbox') +
content_tag('td',
relation.to_s(@issue) {|other|
link_to_issue(
other,
:project => Setting.cross_project_issue_relations?)
}.html_safe,
:class => 'subject') +
content_tag('td', other_issue.status, :class => 'status') +
content_tag('td', link_to_user(other_issue.assigned_to), :class => 'assigned_to') +
content_tag('td', format_date(other_issue.start_date), :class => 'start_date') +
content_tag('td', format_date(other_issue.due_date), :class => 'due_date') +
content_tag('td', other_issue.disabled_core_fields.include?('done_ratio') ? '' : progress_bar(other_issue.done_ratio), :class=> 'done_ratio') +
content_tag('td', link, :class => 'buttons'),
:id => "relation-#{relation.id}",
:class => css)
content_tag('td',
(if other_issue.disabled_core_fields.include?('done_ratio')
''
else
progress_bar(other_issue.done_ratio)
end),
:class=> 'done_ratio') +
content_tag('td', buttons, :class => 'buttons'),
:id => "relation-#{relation.id}",
:class => css)
end
content_tag('table', s, :class => 'list issues odd-even')
end
@ -144,7 +204,7 @@ module IssuesHelper
l_hours_short(issue.estimated_hours)
else
s = issue.estimated_hours.present? ? l_hours_short(issue.estimated_hours) : ""
s << " (#{l(:label_total)}: #{l_hours_short(issue.total_estimated_hours)})"
s += " (#{l(:label_total)}: #{l_hours_short(issue.total_estimated_hours)})"
s.html_safe
end
end
@ -158,25 +218,18 @@ module IssuesHelper
link_to(l_hours_short(issue.spent_hours), path)
else
s = issue.spent_hours > 0 ? l_hours_short(issue.spent_hours) : ""
s << " (#{l(:label_total)}: #{link_to l_hours_short(issue.total_spent_hours), path})"
s += " (#{l(:label_total)}: #{link_to l_hours_short(issue.total_spent_hours), path})"
s.html_safe
end
end
end
# Returns an array of error messages for bulk edited issues
def bulk_edit_error_messages(issues)
messages = {}
issues.each do |issue|
issue.errors.full_messages.each do |message|
messages[message] ||= []
messages[message] << issue
end
end
messages.map { |message, issues|
"#{message}: " + issues.map {|i| "##{i.id}"}.join(', ')
}
end
def issue_due_date_details(issue)
return if issue&.due_date.nil?
s = format_date(issue.due_date)
s += " (#{due_date_distance_in_words(issue.due_date)})" unless issue.closed?
s
end
# Returns a link for adding a new subtask to the given issue
def link_to_new_subtask(issue)
@ -188,13 +241,18 @@ module IssuesHelper
end
def trackers_options_for_select(issue)
trackers = trackers_for_select(issue)
trackers.collect {|t| [t.name, t.id]}
end
def trackers_for_select(issue)
trackers = issue.allowed_target_trackers
if issue.new_record? && issue.parent_issue_id.present?
trackers = trackers.reject do |tracker|
issue.tracker_id != tracker.id && tracker.disabled_core_fields.include?('parent_issue_id')
end
end
trackers.collect {|t| [t.name, t.id]}
trackers
end
class IssueFieldsRows
@ -227,9 +285,11 @@ module IssuesHelper
def cells(label, text, options={})
options[:class] = [options[:class] || "", 'attribute'].join(' ')
content_tag 'div',
content_tag('div', label + ":", :class => 'label') + content_tag('div', text, :class => 'value'),
options
content_tag(
'div',
content_tag('div', label + ":", :class => 'label') +
content_tag('div', text, :class => 'value'),
options)
end
end
@ -245,13 +305,8 @@ module IssuesHelper
half = (values.size / 2.0).ceil
issue_fields_rows do |rows|
values.each_with_index do |value, i|
css = "cf_#{value.custom_field.id}"
attr_value = show_value(value)
if value.custom_field.text_formatting == 'full'
attr_value = content_tag('div', attr_value, class: 'wiki')
end
m = (i < half ? :left : :right)
rows.send m, custom_field_name_tag(value.custom_field), attr_value, :class => css
rows.send m, custom_field_name_tag(value.custom_field), custom_field_value_tag(value), :class => value.custom_field.css_classes
end
end
end
@ -259,21 +314,15 @@ module IssuesHelper
def render_full_width_custom_fields_rows(issue)
values = issue.visible_custom_field_values.select {|value| value.custom_field.full_width_layout?}
return if values.empty?
s = ''.html_safe
values.each_with_index do |value, i|
attr_value = show_value(value)
next if attr_value.blank?
if value.custom_field.text_formatting == 'full'
attr_value = content_tag('div', attr_value, class: 'wiki')
end
attr_value_tag = custom_field_value_tag(value)
next if attr_value_tag.blank?
content =
content_tag('hr') +
content_tag('p', content_tag('strong', custom_field_name_tag(value.custom_field) )) +
content_tag('div', attr_value, class: 'value')
s << content_tag('div', content, class: "cf_#{value.custom_field.id} attribute")
content_tag('hr') +
content_tag('p', content_tag('strong', custom_field_name_tag(value.custom_field) )) +
content_tag('div', attr_value_tag, class: 'value')
s << content_tag('div', content, class: "#{value.custom_field.css_classes} attribute")
end
s
end
@ -323,20 +372,24 @@ module IssuesHelper
def email_issue_attributes(issue, user, html)
items = []
%w(author status priority assigned_to category fixed_version).each do |attribute|
unless issue.disabled_core_fields.include?(attribute+"_id")
%w(author status priority assigned_to category fixed_version start_date due_date).each do |attribute|
if issue.disabled_core_fields.grep(/^#{attribute}(_id)?$/).empty?
attr_value = (issue.send attribute).to_s
next if attr_value.blank?
if html
items << content_tag('strong', "#{l("field_#{attribute}")}: ") + (issue.send attribute)
items << content_tag('strong', "#{l("field_#{attribute}")}: ") + attr_value
else
items << "#{l("field_#{attribute}")}: #{issue.send attribute}"
items << "#{l("field_#{attribute}")}: #{attr_value}"
end
end
end
issue.visible_custom_field_values(user).each do |value|
cf_value = show_value(value, false)
next if cf_value.blank?
if html
items << content_tag('strong', "#{value.custom_field.name}: ") + show_value(value, false)
items << content_tag('strong', "#{value.custom_field.name}: ") + cf_value
else
items << "#{value.custom_field.name}: #{show_value(value, false)}"
items << "#{value.custom_field.name}: #{cf_value}"
end
end
items
@ -351,10 +404,12 @@ module IssuesHelper
end
end
MultipleValuesDetail = Struct.new(:property, :prop_key, :custom_field, :old_value, :value)
# Returns the textual representation of a journal details
# as an array of strings
def details_to_strings(details, no_html=false, options={})
options[:only_path] = (options[:only_path] == false ? false : true)
options[:only_path] = !(options[:only_path] == false)
strings = []
values_by_field = {}
details.each do |detail|
@ -374,15 +429,14 @@ module IssuesHelper
strings << show_detail(detail, no_html, options)
end
if values_by_field.present?
multiple_values_detail = Struct.new(:property, :prop_key, :custom_field, :old_value, :value)
values_by_field.each do |field, changes|
if changes[:added].any?
detail = multiple_values_detail.new('cf', field.id.to_s, field)
detail = MultipleValuesDetail.new('cf', field.id.to_s, field)
detail.value = changes[:added]
strings << show_detail(detail, no_html, options)
end
if changes[:deleted].any?
detail = multiple_values_detail.new('cf', field.id.to_s, field)
detail = MultipleValuesDetail.new('cf', field.id.to_s, field)
detail.old_value = changes[:deleted]
strings << show_detail(detail, no_html, options)
end
@ -446,12 +500,20 @@ module IssuesHelper
when 'relation'
if detail.value && !detail.old_value
rel_issue = Issue.visible.find_by_id(detail.value)
value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.value}" :
(no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
value =
if rel_issue.nil?
"#{l(:label_issue)} ##{detail.value}"
else
(no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
end
elsif detail.old_value && !detail.value
rel_issue = Issue.visible.find_by_id(detail.old_value)
old_value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.old_value}" :
(no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
old_value =
if rel_issue.nil?
"#{l(:label_issue)} ##{detail.old_value}"
else
(no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
end
end
relation_type = IssueRelation::TYPES[detail.prop_key]
label = l(relation_type[:name]) if relation_type
@ -487,10 +549,13 @@ module IssuesHelper
elsif show_diff
s = l(:text_journal_changed_no_detail, :label => label)
unless no_html
diff_link = link_to 'diff',
diff_journal_url(detail.journal_id, :detail_id => detail.id, :only_path => options[:only_path]),
:title => l(:label_view_diff)
s << " (#{ diff_link })"
diff_link =
link_to(
'diff',
diff_journal_url(detail.journal_id, :detail_id => detail.id,
:only_path => options[:only_path]),
:title => l(:label_view_diff))
s << " (#{diff_link})"
end
s.html_safe
elsif detail.value.present?
@ -513,9 +578,7 @@ module IssuesHelper
# Find the name of an associated record stored in the field attribute
def find_name_by_reflection(field, id)
unless id.present?
return nil
end
return nil if id.blank?
@detail_value_name_by_reflection ||= Hash.new do |hash, key|
association = Issue.reflect_on_association(key.first.to_sym)
name = nil
@ -543,4 +606,36 @@ module IssuesHelper
end
end
end
# Issue history tabs
def issue_history_tabs
tabs = []
if @journals.present?
journals_without_notes = @journals.select{|value| value.notes.blank?}
journals_with_notes = @journals.reject{|value| value.notes.blank?}
tabs << {:name => 'history', :label => :label_history, :onclick => 'showIssueHistory("history", this.href)', :partial => 'issues/tabs/history', :locals => {:issue => @issue, :journals => @journals}}
tabs << {:name => 'notes', :label => :label_issue_history_notes, :onclick => 'showIssueHistory("notes", this.href)'} if journals_with_notes.any?
tabs << {:name => 'properties', :label => :label_issue_history_properties, :onclick => 'showIssueHistory("properties", this.href)'} if journals_without_notes.any?
end
tabs << {:name => 'time_entries', :label => :label_time_entry_plural, :remote => true, :onclick => "getRemoteTab('time_entries', '#{tab_issue_path(@issue, :name => 'time_entries')}', '#{issue_path(@issue, :tab => 'time_entries')}')"} if User.current.allowed_to?(:view_time_entries, @project) && @issue.spent_hours > 0
tabs << {:name => 'changesets', :label => :label_associated_revisions, :remote => true, :onclick => "getRemoteTab('changesets', '#{tab_issue_path(@issue, :name => 'changesets')}', '#{issue_path(@issue, :tab => 'changesets')}')"} if @has_changesets
tabs
end
def issue_history_default_tab
# tab params overrides user default tab preference
return params[:tab] if params[:tab].present?
user_default_tab = User.current.pref.history_default_tab
case user_default_tab
when 'last_tab_visited'
cookies['history_last_tab'].presence || 'notes'
when ''
'notes'
else
user_default_tab
end
end
end

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -25,19 +25,20 @@ module JournalsHelper
ids.any? ? Attachment.where(:id => ids).select(&:thumbnailable?) : []
end
def render_notes(issue, journal, options={})
content = ''
css_classes = "wiki"
# Returns the action links for an issue journal
def render_journal_actions(issue, journal, options={})
links = []
if journal.notes.present?
links << link_to(l(:button_quote),
quoted_issue_path(issue, :journal_id => journal),
:remote => true,
:method => 'post',
:title => l(:button_quote),
:class => 'icon-only icon-comment'
) if options[:reply_links]
if options[:reply_links]
indice = journal.indice || @journal.issue.visible_journals_with_index.find{|j| j.id == @journal.id}.indice
links << link_to(l(:button_quote),
quoted_issue_path(issue, :journal_id => journal, :journal_indice => indice),
:remote => true,
:method => 'post',
:title => l(:button_quote),
:class => 'icon-only icon-comment'
)
end
if journal.editable_by?(User.current)
links << link_to(l(:button_edit),
edit_journal_path(journal),
@ -49,21 +50,22 @@ module JournalsHelper
links << link_to(l(:button_delete),
journal_path(journal, :journal => {:notes => ""}),
:remote => true,
:method => 'put', :data => {:confirm => l(:text_are_you_sure)},
:method => 'put', :data => {:confirm => l(:text_are_you_sure)},
:title => l(:button_delete),
:class => 'icon-only icon-del'
)
css_classes << " editable"
end
end
content << content_tag('div', links.join(' ').html_safe, :class => 'contextual') unless links.empty?
content << textilizable(journal, :notes)
content_tag('div', content.html_safe, :id => "journal-#{journal.id}-notes", :class => css_classes)
safe_join(links, ' ')
end
def render_notes(issue, journal, options={})
content_tag('div', textilizable(journal, :notes), :id => "journal-#{journal.id}-notes", :class => "wiki")
end
def render_private_notes_indicator(journal)
content = journal.private_notes? ? l(:field_is_private) : ''
css_classes = journal.private_notes? ? 'private' : ''
css_classes = journal.private_notes? ? 'badge badge-private private' : ''
content_tag('span', content.html_safe, :id => "journal-#{journal.id}-private_notes", :class => css_classes)
end
end

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -23,16 +23,40 @@ module MembersHelper
principal_count = scope.count
principal_pages = Redmine::Pagination::Paginator.new principal_count, limit, params['page']
principals = scope.offset(principal_pages.offset).limit(principal_pages.per_page).to_a
s = content_tag('div',
content_tag('div', principals_check_box_tags('membership[user_ids][]', principals), :id => 'principals'),
:class => 'objects-selection'
s = content_tag(
'div',
content_tag(
'div',
principals_check_box_tags('membership[user_ids][]', principals), :id => 'principals'),
:class => 'objects-selection'
)
links = pagination_links_full(principal_pages, principal_count, :per_page_links => false) {|text, parameters, options|
link_to text, autocomplete_project_memberships_path(project, parameters.merge(:q => params[:q], :format => 'js')), :remote => true
}
links =
pagination_links_full(principal_pages,
principal_count,
:per_page_links => false) {|text, parameters, options|
link_to(
text,
autocomplete_project_memberships_path(
project,
parameters.merge(:q => params[:q], :format => 'js')
),
:remote => true)
}
s + content_tag('span', links, :class => 'pagination')
end
# Returns inheritance information for an inherited member role
def render_role_inheritance(member, role)
content = member.role_inheritance(role).map do |h|
if h.is_a?(Project)
l(:label_inherited_from_parent_project)
elsif h.is_a?(Group)
l(:label_inherited_from_group, :name => h.name.to_s)
end
end.compact.uniq
if content.present?
content_tag('em', content.join(", "), :class => "info")
end
end
end

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -34,7 +34,7 @@ module MyHelper
def render_block(block, user)
content = render_block_content(block, user)
if content.present?
handle = content_tag('span', '', :class => 'sort-handle', :title => l(:button_move))
handle = content_tag('span', '', :class => 'icon-only icon-sort-handle sort-handle', :title => l(:button_move))
close = link_to(l(:button_delete),
{:action => "remove_block", :block => block},
:remote => true, :method => 'post',
@ -78,7 +78,7 @@ module MyHelper
def render_calendar_block(block, settings)
calendar = Redmine::Helpers::Calendar.new(User.current.today, current_language, :week)
calendar.events = Issue.visible.
where(:project_id => User.current.projects.map(&:id)).
where(:project => User.current.projects).
where("(start_date>=? and start_date<=?) or (due_date>=? and due_date<=?)", calendar.startdt, calendar.enddt, calendar.startdt, calendar.enddt).
includes(:project, :tracker, :priority, :assigned_to).
references(:project, :tracker, :priority, :assigned_to).
@ -96,6 +96,7 @@ module MyHelper
def render_issuesassignedtome_block(block, settings)
query = IssueQuery.new(:name => l(:label_assigned_to_me_issues), :user => User.current)
query.add_filter 'assigned_to_id', '=', ['me']
query.add_filter 'project.status', '=', ["#{Project::STATUS_ACTIVE}"]
query.column_names = settings[:columns].presence || ['project', 'tracker', 'status', 'subject']
query.sort_criteria = settings[:sort].presence || [['priority', 'desc'], ['updated_on', 'desc']]
issues = query.issues(:limit => 10)
@ -106,6 +107,18 @@ module MyHelper
def render_issuesreportedbyme_block(block, settings)
query = IssueQuery.new(:name => l(:label_reported_issues), :user => User.current)
query.add_filter 'author_id', '=', ['me']
query.add_filter 'project.status', '=', ["#{Project::STATUS_ACTIVE}"]
query.column_names = settings[:columns].presence || ['project', 'tracker', 'status', 'subject']
query.sort_criteria = settings[:sort].presence || [['updated_on', 'desc']]
issues = query.issues(:limit => 10)
render :partial => 'my/blocks/issues', :locals => {:query => query, :issues => issues, :block => block}
end
def render_issuesupdatedbyme_block(block, settings)
query = IssueQuery.new(:name => l(:label_updated_issues), :user => User.current)
query.add_filter 'updated_by', '=', ['me']
query.add_filter 'project.status', '=', ["#{Project::STATUS_ACTIVE}"]
query.column_names = settings[:columns].presence || ['project', 'tracker', 'status', 'subject']
query.sort_criteria = settings[:sort].presence || [['updated_on', 'desc']]
issues = query.issues(:limit => 10)
@ -116,6 +129,7 @@ module MyHelper
def render_issueswatched_block(block, settings)
query = IssueQuery.new(:name => l(:label_watched_issues), :user => User.current)
query.add_filter 'watcher_id', '=', ['me']
query.add_filter 'project.status', '=', ["#{Project::STATUS_ACTIVE}"]
query.column_names = settings[:columns].presence || ['project', 'tracker', 'status', 'subject']
query.sort_criteria = settings[:sort].presence || [['updated_on', 'desc']]
issues = query.issues(:limit => 10)
@ -139,7 +153,7 @@ module MyHelper
def render_news_block(block, settings)
news = News.visible.
where(:project_id => User.current.projects.map(&:id)).
where(:project => User.current.projects).
limit(10).
includes(:project, :author).
references(:project, :author).
@ -164,4 +178,10 @@ module MyHelper
render :partial => 'my/blocks/timelog', :locals => {:block => block, :entries => entries, :entries_by_day => entries_by_day, :days => days}
end
def render_activity_block(block, settings)
events_by_day = Redmine::Activity::Fetcher.new(User.current, :author => User.current).events(nil, nil, :limit => 10).group_by {|event| User.current.time_to_date(event.event_datetime)}
render :partial => 'my/blocks/activity', :locals => {:events_by_day => events_by_day}
end
end

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -19,18 +19,31 @@
module ProjectsHelper
def project_settings_tabs
tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural},
{:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural},
{:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural},
{:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural,
:url => {:tab => 'versions', :version_status => params[:version_status], :version_name => params[:version_name]}},
{:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
{:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
{:name => 'repositories', :action => :manage_repository, :partial => 'projects/settings/repositories', :label => :label_repository_plural},
{:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural},
{:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :enumeration_activities}
]
tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
tabs =
[
{:name => 'info', :action => :edit_project,
:partial => 'projects/edit', :label => :label_project},
{:name => 'members', :action => :manage_members,
:partial => 'projects/settings/members', :label => :label_member_plural},
{:name => 'issues', :action => :edit_project, :module => :issue_tracking,
:partial => 'projects/settings/issues', :label => :label_issue_tracking},
{:name => 'versions', :action => :manage_versions,
:partial => 'projects/settings/versions', :label => :label_version_plural,
:url => {:tab => 'versions', :version_status => params[:version_status],
:version_name => params[:version_name]}},
{:name => 'categories', :action => :manage_categories,
:partial => 'projects/settings/issue_categories',
:label => :label_issue_category_plural},
{:name => 'repositories', :action => :manage_repository,
:partial => 'projects/settings/repositories', :label => :label_repository_plural},
{:name => 'boards', :action => :manage_boards,
:partial => 'projects/settings/boards', :label => :label_board_plural},
{:name => 'activities', :action => :manage_project_activities,
:partial => 'projects/settings/activities', :label => :label_time_tracking}
]
tabs.
select {|tab| User.current.allowed_to?(tab[:action], @project)}.
select {|tab| tab[:module].nil? || @project.module_enabled?(tab[:module])}
end
def parent_project_select_tag(project)
@ -41,24 +54,27 @@ module ProjectsHelper
selected = (parent_id.blank? ? nil : Project.find(parent_id))
end
options = ''
options = +''
options << "<option value=''>&nbsp;</option>" if project.allowed_parents.include?(nil)
options << project_tree_options_for_select(project.allowed_parents.compact, :selected => selected)
content_tag('select', options.html_safe, :name => 'project[parent_id]', :id => 'project_parent_id')
end
def render_project_action_links
links = "".html_safe
links = (+"").html_safe
if User.current.allowed_to?(:add_project, nil, :global => true)
links << link_to(l(:label_project_new), new_project_path, :class => 'icon icon-add')
end
if User.current.admin?
links << link_to(l(:label_administration), admin_projects_path, :class => 'icon icon-settings')
end
links
end
# Renders the projects index
def render_project_hierarchy(projects)
render_project_nested_lists(projects) do |project|
s = link_to_project(project, {}, :class => "#{project.css_classes} #{User.current.member_of?(project) ? 'icon icon-fav my-project' : nil}")
s = link_to_project(project, {}, :class => "#{project.css_classes} #{User.current.member_of?(project) ? 'icon icon-user my-project' : nil}")
if project.description.present?
s << content_tag('div', textilizable(project.short_description, :project => project), :class => 'wiki description')
end
@ -137,4 +153,33 @@ module ProjectsHelper
end
end if include_in_api_response?('enabled_modules')
end
def bookmark_link(project, user = User.current)
return '' unless user && user.logged?
@jump_box ||= Redmine::ProjectJumpBox.new user
bookmarked = @jump_box.bookmark?(project)
css = +"icon bookmark "
if bookmarked
css << "icon-bookmark"
method = "delete"
text = l(:button_project_bookmark_delete)
else
css << "icon-bookmark-off"
method = "post"
text = l(:button_project_bookmark)
end
url = bookmark_project_path(project)
link_to text, url, remote: true, method: method, class: css
end
def grouped_project_list(projects, query, &block)
ancestors = []
grouped_query_results(projects, query) do |project, group_name, group_count, group_totals|
ancestors.pop while ancestors.any? && !project.is_descendant_of?(ancestors.last)
yield project, ancestors.size, group_name, group_count, group_totals
ancestors << project unless project.leaf?
end
end
end

View file

@ -0,0 +1,62 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module ProjectsQueriesHelper
include ApplicationHelper
def column_value(column, item, value)
if item.is_a?(Project)
case column.name
when :name
link_to_project(item) + (content_tag('span', '', :class => 'icon icon-user my-project', :title => l(:label_my_projects)) if User.current.member_of?(item))
when :short_description
item.description? ? content_tag('div', textilizable(item, :short_description), :class => "wiki") : ''
when :homepage
item.homepage? ? content_tag('div', textilizable(item, :homepage), :class => "wiki") : ''
when :status
get_project_status_label[column.value_object(item)]
when :parent_id
link_to_project(item.parent) unless item.parent.nil?
else
super
end
end
end
def csv_value(column, object, value)
if object.is_a?(Project)
case column.name
when :status
get_project_status_label[column.value_object(object)]
when :parent_id
object.parent.name unless object.parent.nil?
else
super
end
end
end
private
def get_project_status_label
{
Project::STATUS_ACTIVE => l(:project_status_active),
Project::STATUS_CLOSED => l(:project_status_closed)
}
end
end

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -17,6 +17,8 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'redmine/export/csv'
module QueriesHelper
include ApplicationHelper
@ -28,7 +30,7 @@ module QueriesHelper
group = :label_relations
elsif field_options[:type] == :tree
group = query.is_a?(IssueQuery) ? :label_relations : nil
elsif field =~ /^cf_\d+\./
elsif /^cf_\d+\./.match?(field)
group = (field_options[:through] || field_options[:field]).try(:name)
elsif field =~ /^(.+)\./
# association filters
@ -37,6 +39,8 @@ module QueriesHelper
group = :field_assigned_to
elsif field_options[:type] == :date_past || field_options[:type] == :date
group = :label_date
elsif %w(estimated_hours spent_time).include?(field)
group = :label_time_tracking
end
if group
(grouped[group] ||= []) << [field_options[:name], field]
@ -93,12 +97,13 @@ module QueriesHelper
tags
end
def available_totalable_columns_tags(query)
def available_totalable_columns_tags(query, options={})
tag_name = (options[:name] || 't') + '[]'
tags = ''.html_safe
query.available_totalable_columns.each do |column|
tags << content_tag('label', check_box_tag('t[]', column.name.to_s, query.totalable_columns.include?(column), :id => nil) + " #{column.caption}", :class => 'inline')
tags << content_tag('label', check_box_tag(tag_name, column.name.to_s, query.totalable_columns.include?(column), :id => nil) + " #{column.caption}", :class => 'inline')
end
tags << hidden_field_tag('t[]', '')
tags << hidden_field_tag(tag_name, '')
tags
end
@ -115,6 +120,15 @@ module QueriesHelper
render :partial => 'queries/columns', :locals => {:query => query, :tag_name => tag_name}
end
def available_display_types_tags(query)
tags = ''.html_safe
query.available_display_types.each do |t|
tags << radio_button_tag('display_type', t, @query.display_type == t, :id => "display_type_#{t}") +
content_tag('label', l(:"label_display_type_#{t}"), :for => "display_type_#{t}", :class => "inline")
end
tags
end
def grouped_query_results(items, query, &block)
result_count_by_group = query.result_count_by_group
previous_group, first = false, true
@ -125,7 +139,7 @@ module QueriesHelper
items.each do |item|
group_name = group_count = nil
if query.grouped?
group = query.group_by_column.value(item)
group = query.group_by_column.group_value(item)
if first || group != previous_group
if group.blank? && group != false
group_name = "(#{l(:label_blank_value)})"
@ -152,11 +166,12 @@ module QueriesHelper
def total_tag(column, value)
label = content_tag('span', "#{column.caption}:")
value = if [:hours, :spent_hours, :total_spent_hours, :estimated_hours].include? column.name
format_hours(value)
else
format_object(value)
end
value =
if [:hours, :spent_hours, :total_spent_hours, :estimated_hours].include? column.name
format_hours(value)
else
format_object(value)
end
value = content_tag('span', value, :class => 'value')
content_tag('span', label + " " + value, :class => "total-for-#{column.name.to_s.dasherize}")
end
@ -166,18 +181,16 @@ module QueriesHelper
css, order = nil, column.default_order
if column.name.to_s == query.sort_criteria.first_key
if query.sort_criteria.first_asc?
css = 'sort asc'
css = 'sort asc icon icon-sorted-desc'
order = 'desc'
else
css = 'sort desc'
css = 'sort desc icon icon-sorted-asc'
order = 'asc'
end
end
param_key = options[:sort_param] || :sort
sort_param = { param_key => query.sort_criteria.add(column.name, order).to_param }
while sort_param.keys.first.to_s =~ /^(.+)\[(.+)\]$/
sort_param = {$1 => {$2 => sort_param.values.first}}
end
sort_param = {param_key => query.sort_criteria.add(column.name, order).to_param}
sort_param = {$1 => {$2 => sort_param.values.first}} while sort_param.keys.first.to_s =~ /^(.+)\[(.+)\]$/
link_options = {
:title => l(:label_sort_by, "\"#{column.caption}\""),
:class => css
@ -185,14 +198,15 @@ module QueriesHelper
if options[:sort_link_options]
link_options.merge! options[:sort_link_options]
end
content = link_to(column.caption,
content = link_to(
column.caption,
{:params => request.query_parameters.deep_merge(sort_param)},
link_options
)
else
content = column.caption
end
content_tag('th', content)
content_tag('th', content, :class => column.css_classes)
end
def column_content(column, item)
@ -220,7 +234,8 @@ module QueriesHelper
when :done_ratio
progress_bar(value)
when :relations
content_tag('span',
content_tag(
'span',
value.to_s(item) {|other| link_to_issue(other, :subject => false, :tracker => false)}.html_safe,
:class => value.css_classes_for(item))
when :hours, :estimated_hours
@ -258,7 +273,7 @@ module QueriesHelper
value.to_s(object)
when 'Issue'
if object.is_a?(TimeEntry)
"#{value.tracker} ##{value.id}: #{value.subject}"
value.visible? ? "#{value.tracker} ##{value.id}: #{value.subject}" : "##{value.id}"
else
value.id
end
@ -272,7 +287,7 @@ module QueriesHelper
def query_to_csv(items, query, options={})
columns = query.columns
Redmine::Export::CSV.generate do |csv|
Redmine::Export::CSV.generate(:encoding => params[:encoding]) do |csv|
# csv header fields
csv << columns.map {|c| c.caption.to_s}
# csv lines
@ -283,20 +298,20 @@ module QueriesHelper
end
# Retrieve query from session or build a new query
def retrieve_query(klass=IssueQuery, use_session=true)
def retrieve_query(klass=IssueQuery, use_session=true, options={})
session_key = klass.name.underscore.to_sym
if params[:query_id].present?
cond = "project_id IS NULL"
cond << " OR project_id = #{@project.id}" if @project
@query = klass.where(cond).find(params[:query_id])
scope = klass.where(:project_id => nil)
scope = scope.or(klass.where(:project_id => @project)) if @project
@query = scope.find(params[:query_id])
raise ::Unauthorized unless @query.visible?
@query.project = @project
session[session_key] = {:id => @query.id, :project_id => @query.project_id} if use_session
elsif api_request? || params[:set_filter] || !use_session || session[session_key].nil? || session[session_key][:project_id] != (@project ? @project.id : nil)
# Give it a name, required to be valid
@query = klass.new(:name => "_", :project => @project)
@query.build_from_params(params)
@query.build_from_params(params, options[:defaults])
session[session_key] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names, :totalable_names => @query.totalable_names, :sort => @query.sort_criteria.to_a} if use_session
else
# retrieve from session
@ -367,7 +382,7 @@ module QueriesHelper
tags
end
def query_hidden_sort_tag(query)
hidden_field_tag("sort", query.sort_criteria.to_param, :id => nil)
end
@ -381,19 +396,41 @@ module QueriesHelper
def query_links(title, queries)
return '' if queries.empty?
# links to #index on issues/show
url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : {}
url_params =
if controller_name == 'issues'
{:controller => 'issues', :action => 'index', :project_id => @project}
else
{}
end
content_tag('h3', title) + "\n" +
content_tag('ul',
content_tag(
'ul',
queries.collect {|query|
css = 'query'
css << ' selected' if query == @query
content_tag('li', link_to(query.name, url_params.merge(:query_id => query), :class => css))
}.join("\n").html_safe,
css = +'query'
clear_link = +''
if query == @query
css << ' selected'
clear_link += link_to_clear_query
end
content_tag('li',
link_to(query.name,
url_params.merge(:query_id => query),
:class => css) +
clear_link.html_safe)
}.join("\n").html_safe,
:class => 'queries'
) + "\n"
end
def link_to_clear_query
link_to(
l(:button_clear),
{:set_filter => 1, :sort => '', :project_id => @project},
:class => 'icon-only icon-clear-query',
:title => l(:button_clear)
)
end
# Renders the list of queries for the sidebar
def render_sidebar_queries(klass, project)
queries = sidebar_queries(klass, project)

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -32,9 +32,18 @@ module RepositoriesHelper
end
end
def render_pagination
pagination_links_each @paginator do |text, parameters, options|
if entry = @entries[parameters[:page] - 1]
ent_path = Redmine::CodesetUtil.replace_invalid_utf8(entry.path)
link_to text, {action: 'entry', id: @project, repository_id: @repository.identifier_param, path: to_path_param(ent_path), rev: @rev}
end
end if @paginator
end
def render_properties(properties)
unless properties.nil? || properties.empty?
content = ''
content = +''
properties.keys.sort.each do |property|
content << content_tag('li', "<b>#{h property}</b>: <span>#{h properties[property]}</span>".html_safe)
end
@ -49,7 +58,7 @@ module RepositoriesHelper
# Detects moved/copied files
if !change.from_path.blank?
change.action =
@changeset.filechanges.detect {|c| c.action == 'D' && c.path == change.from_path} ? 'R' : 'C'
@changeset.filechanges.detect {|c| c.action == 'D' && c.path == change.from_path} ? 'R' : 'C'
end
change
when 'D'
@ -78,10 +87,10 @@ module RepositoriesHelper
def render_changes_tree(tree)
return '' if tree.nil?
output = ''
output = +''
output << '<ul>'
tree.keys.sort.each do |file|
style = 'change'
style = +'change'
text = File.basename(h(file))
if s = tree[file][:s]
style << ' folder'
@ -130,7 +139,7 @@ module RepositoriesHelper
def scm_select_tag(repository)
scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']]
Redmine::Scm::Base.all.each do |scm|
if Setting.enabled_scm.include?(scm) ||
if Setting.enabled_scm.include?(scm) ||
(repository && repository.class.name.demodulize == scm)
scm_options << ["Repository::#{scm}".constantize.scm_name, scm]
end
@ -157,15 +166,6 @@ module RepositoriesHelper
:onchange => "this.name='repository[password]';"))
end
def darcs_field_tags(form, repository)
content_tag('p', form.text_field(
:url, :label => l(:field_path_to_repository),
:size => 60, :required => true,
:disabled => !repository.safe_attribute?('url')) +
scm_path_info_tag(repository)) +
scm_log_encoding_tag(form, repository)
end
def mercurial_field_tags(form, repository)
content_tag('p', form.text_field(
:url, :label => l(:field_path_to_repository),
@ -278,8 +278,8 @@ module RepositoriesHelper
:href => block_given? ? yield(commit.scmid) : commit.scmid
}
end
heads.sort! { |head1, head2| head1.to_s <=> head2.to_s }
space = nil
heads.sort_by!(&:to_s)
space = nil
heads.each do |head|
if commits_by_scmid.include? head.scmid
space = index_head((space || -1) + 1, head, commits_by_scmid)

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -18,14 +18,11 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module SearchHelper
include ActionView::Helpers::SanitizeHelper
def highlight_tokens(text, tokens)
return text unless text && tokens && !tokens.empty?
re_tokens = tokens.collect {|t| Regexp.escape(t)}
regexp = Regexp.new "(#{re_tokens.join('|')})", Regexp::IGNORECASE
result = ''
text = strip_tags(text)
result = +''
text.split(regexp).each_with_index do |words, i|
if result.length > 1200
# maximum length of the preview reached
@ -58,7 +55,7 @@ module SearchHelper
def render_results_by_type(results_by_type)
links = []
# Sorts types by results count
results_by_type.keys.sort {|a, b| results_by_type[b] <=> results_by_type[a]}.each do |t|
results_by_type.keys.sort_by {|k| results_by_type[k]}.reverse_each do |t|
c = results_by_type[t]
next if c == 0
text = "#{type_label(t)} (#{c})"

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -24,6 +24,7 @@ module SettingsHelper
{:name => 'authentication', :partial => 'settings/authentication', :label => :label_authentication},
{:name => 'api', :partial => 'settings/api', :label => :label_api},
{:name => 'projects', :partial => 'settings/projects', :label => :label_project_plural},
{:name => 'users', :partial => 'settings/users', :label => :label_user_plural},
{:name => 'issues', :partial => 'settings/issues', :label => :label_issue_tracking},
{:name => 'timelog', :partial => 'settings/timelog', :label => :label_time_tracking},
{:name => 'attachments', :partial => 'settings/attachments', :label => :label_attachment_plural},
@ -112,20 +113,16 @@ module SettingsHelper
tag_data = notifiable.parent.present? ?
{:parent_notifiable => notifiable.parent} :
{:disables => "input[data-parent-notifiable=#{notifiable.name}]"}
tag = check_box_tag('settings[notified_events][]',
notifiable.name,
setting_value('notified_events').include?(notifiable.name),
:id => nil,
:data => tag_data)
notifiable.name,
setting_value('notified_events').include?(notifiable.name),
:id => nil,
:data => tag_data)
text = l_or_humanize(notifiable.name, :prefix => 'label_')
options = {}
if notifiable.parent.present?
options[:class] = "parent"
end
content_tag(:label, tag + text, options)
end
@ -207,4 +204,13 @@ module SettingsHelper
["#{today} (#{format})", f]
end
end
def gravatar_default_setting_options
[['Identicons', 'identicon'],
['Monster ids', 'monsterid'],
['Mystery man', 'mm'],
['Retro', 'retro'],
['Robohash', 'robohash'],
['Wavatars', 'wavatar']]
end
end

View file

@ -1,5 +1,5 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Helpers to sort tables using clickable column headers.
#
# Author: Stuart Rackham <srackham@methods.co.nz>, March 2005.
@ -96,7 +96,7 @@ module SortHelper
# Returns an SQL sort clause corresponding to the current sort state.
# Use this to sort the controller's table items collection.
#
def sort_clause()
def sort_clause
@sort_criteria.sort_clause(@sortable_columns)
end
@ -115,10 +115,10 @@ module SortHelper
if column.to_s == @sort_criteria.first_key
if @sort_criteria.first_asc?
css = 'sort asc'
css = 'sort asc icon icon-sorted-desc'
order = 'desc'
else
css = 'sort desc'
css = 'sort desc icon icon-sorted-asc'
order = 'asc'
end
end
@ -160,4 +160,3 @@ module SortHelper
end
end
end

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -42,6 +42,12 @@ module TimelogHelper
collection
end
def user_collection_for_select_options(time_entry)
collection = time_entry.assignable_users
collection << time_entry.user unless time_entry.user.nil? && !collection.include?(time_entry.user)
principals_options_for_select(collection, time_entry.user_id.to_s)
end
def select_hours(data, criteria, value)
if value.to_s.empty?
data.select {|row| row[criteria].blank? }
@ -58,15 +64,19 @@ module TimelogHelper
sum
end
def format_criteria_value(criteria_options, value)
def format_criteria_value(criteria_options, value, html=true)
if value.blank?
"[#{l(:label_none)}]"
elsif k = criteria_options[:klass]
obj = k.find_by_id(value.to_i)
if obj.is_a?(Issue)
obj.visible? ? "#{obj.tracker} ##{obj.id}: #{obj.subject}" : "##{obj.id}"
if obj.visible?
html ? link_to_issue(obj) : "#{obj.tracker} ##{obj.id}: #{obj.subject}"
else
"##{obj.id}"
end
else
obj
format_object(obj, html)
end
elsif cf = criteria_options[:custom_field]
format_value(value, cf)
@ -76,9 +86,9 @@ module TimelogHelper
end
def report_to_csv(report)
Redmine::Export::CSV.generate do |csv|
Redmine::Export::CSV.generate(:encoding => params[:encoding]) do |csv|
# Column headers
headers = report.criteria.collect {|criteria| l(report.available_criteria[criteria][:label]) }
headers = report.criteria.collect {|criteria| l_or_humanize(report.available_criteria[criteria][:label]) }
headers += report.periods
headers << l(:label_total_time)
csv << headers
@ -103,7 +113,7 @@ module TimelogHelper
hours_for_value = select_hours(hours, criteria[level], value)
next if hours_for_value.empty?
row = [''] * level
row << format_criteria_value(available_criteria[criteria[level]], value).to_s
row << format_criteria_value(available_criteria[criteria[level]], value, false).to_s
row += [''] * (criteria.length - level - 1)
total = 0
periods.each do |period|
@ -118,4 +128,10 @@ module TimelogHelper
end
end
end
def cancel_button_tag_for_time_entry(project)
fallback_path = project ? project_time_entries_path(project) : time_entries_path
cancel_button_tag(fallback_path)
end
end

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -18,4 +18,10 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module TrackersHelper
def tracker_name_tag(tracker)
title = tracker.description.presence
css = title ? "field-description" : nil
content_tag 'span', tracker.name, :class => css, :title => title
end
end

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -18,12 +18,11 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module UsersHelper
include ApplicationHelper
def users_status_options_for_select(selected)
user_count_by_status = User.group('status').count.to_hash
options_for_select([[l(:label_all), ''],
["#{l(:status_active)} (#{user_count_by_status[1].to_i})", '1'],
["#{l(:status_registered)} (#{user_count_by_status[2].to_i})", '2'],
["#{l(:status_locked)} (#{user_count_by_status[3].to_i})", '3']], selected.to_s)
options_for_select([[l(:label_all), '']] + (User.valid_statuses.map {|c| ["#{l('status_' + User::LABEL_BY_STATUS[c])} (#{user_count_by_status[c].to_i})", c]}), selected.to_s)
end
def user_mail_notification_options(user)
@ -34,6 +33,15 @@ module UsersHelper
[[l(:label_font_default), '']] + UserPreference::TEXTAREA_FONT_OPTIONS.map {|o| [l("label_font_#{o}"), o]}
end
def history_default_tab_options
[[l('label_issue_history_notes'), 'notes'],
[l('label_history'), 'history'],
[l('label_issue_history_properties'), 'properties'],
[l('label_time_entry_plural'), 'time_entries'],
[l('label_associated_revisions'), 'changesets'],
[l('label_last_tab_visited'), 'last_tab_visited']]
end
def change_status_link(user)
url = {:controller => 'users', :action => 'update', :id => user, :page => params[:page], :status => params[:status], :tab => nil}
@ -61,4 +69,32 @@ module UsersHelper
end
tabs
end
def users_to_csv(users)
Redmine::Export::CSV.generate(:encoding => params[:encoding]) do |csv|
columns = [
'login',
'firstname',
'lastname',
'mail',
'admin',
'created_on',
'last_login_on',
'status'
]
# csv header fields
csv << columns.map{|column| l('field_' + column)}
# csv lines
users.each do |user|
csv << columns.map do |column|
if column == 'status'
l(("status_#{User::LABEL_BY_STATUS[user.status]}"))
else
format_object(user.send(column), false)
end
end
end
end
end
end

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -29,7 +29,8 @@ module VersionsHelper
def version_filtered_issues_path(version, options = {})
options = {:fixed_version_id => version, :set_filter => 1}.merge(options)
project = case version.sharing
project =
case version.sharing
when 'hierarchy', 'tree'
if version.project && version.project.root.visible?
version.project.root
@ -40,8 +41,7 @@ module VersionsHelper
nil
else
version.project
end
end
if project
project_issues_path(project, options)
else
@ -53,24 +53,55 @@ module VersionsHelper
def render_issue_status_by(version, criteria)
criteria = 'tracker' unless STATUS_BY_CRITERIAS.include?(criteria)
h = Hash.new {|k,v| k[v] = [0, 0]}
begin
# Total issue count
version.fixed_issues.group(criteria).count.each {|c,s| h[c][0] = s}
version.visible_fixed_issues.group(criteria).count.each {|c,s| h[c][0] = s}
# Open issues count
version.fixed_issues.open.group(criteria).count.each {|c,s| h[c][1] = s}
version.visible_fixed_issues.open.group(criteria).count.each {|c,s| h[c][1] = s}
rescue ActiveRecord::RecordNotFound
# When grouping by an association, Rails throws this exception if there's no result (bug)
# When grouping by an association, Rails throws this exception if there's no result (bug)
end
# Sort with nil keys in last position
counts = h.keys.sort {|a,b| a.nil? ? 1 : (b.nil? ? -1 : a <=> b)}.collect {|k| {:group => k, :total => h[k][0], :open => h[k][1], :closed => (h[k][0] - h[k][1])}}
sorted_keys =
h.keys.sort {|a, b|
if a.nil?
1
else
b.nil? ? -1 : a <=> b
end
}
counts =
sorted_keys.collect {|k|
{:group => k, :total => h[k][0], :open => h[k][1], :closed => (h[k][0] - h[k][1])}
}
max = counts.collect {|c| c[:total]}.max
render :partial => 'issue_counts', :locals => {:version => version, :criteria => criteria, :counts => counts, :max => max}
end
def status_by_options_for_select(value)
options_for_select(STATUS_BY_CRITERIAS.collect {|criteria| [l("field_#{criteria}".to_sym), criteria]}, value)
end
def link_to_new_issue(version, project)
if version.open? && User.current.allowed_to?(:add_issues, project)
trackers = Issue.allowed_target_trackers(project)
unless trackers.empty?
issue = Issue.new(:project => project)
new_issue_tracker = trackers.detect do |tracker|
issue.tracker = tracker
issue.safe_attribute?('fixed_version_id')
end
end
if new_issue_tracker
attrs = {
:tracker_id => new_issue_tracker,
:fixed_version_id => version.id
}
link_to l(:label_issue_new), new_project_issue_path(project, :issue => attrs, :back_url => version_path(version)), :class => 'icon icon-add'
end
end
end
end

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -22,10 +22,10 @@ module WikiHelper
def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0)
pages = pages.group_by(&:parent) unless pages.is_a?(Hash)
s = ''.html_safe
s = (+'').html_safe
if pages.has_key?(parent)
pages[parent].each do |page|
attrs = "value='#{page.id}'"
attrs = +"value='#{page.id}'"
attrs << " selected='selected'" if selected == page
indent = (level > 0) ? ('&nbsp;' * level * 2 + '&#187; ') : ''
@ -64,4 +64,8 @@ module WikiHelper
project_wiki_page_path(page.project, page.title)
end
end
def wiki_content_update_info(content)
l(:label_updated_time_by, :author => link_to_user(content.author), :age => time_tag(content.updated_on)).html_safe
end
end

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -21,7 +21,7 @@ module WorkflowsHelper
def options_for_workflow_select(name, objects, selected, options={})
option_tags = ''.html_safe
multiple = false
if selected
if selected
if selected.size == objects.size
selected = 'all'
else
@ -51,7 +51,7 @@ module WorkflowsHelper
options = [["", ""], [l(:label_readonly), "readonly"]]
options << [l(:label_required), "required"] unless field_required?(field)
html_options = {}
if perm = permissions[status.id][name]
if perm.uniq.size > 1 || perm.size < @roles.size * @trackers.size
options << [l(:label_no_change_option), "no_change"]
@ -74,22 +74,27 @@ module WorkflowsHelper
select_tag("permissions[#{status.id}][#{name}]", options_for_select(options, selected), html_options)
end
def transition_tag(workflows, old_status, new_status, name)
w = workflows.select {|w| w.old_status == old_status && w.new_status == new_status}.size
def transition_tag(transition_count, old_status, new_status, name)
w = transition_count
tag_name = "transitions[#{ old_status.try(:id) || 0 }][#{new_status.id}][#{name}]"
if w == 0 || w == @roles.size * @trackers.size
if old_status == new_status
check_box_tag(tag_name, "1", true,
{:disabled => true, :class => "old-status-#{old_status.try(:id) || 0} new-status-#{new_status.id}"})
elsif w == 0 || w == @roles.size * @trackers.size
hidden_field_tag(tag_name, "0", :id => nil) +
check_box_tag(tag_name, "1", w != 0,
:class => "old-status-#{old_status.try(:id) || 0} new-status-#{new_status.id}")
:class => "old-status-#{old_status.try(:id) || 0} new-status-#{new_status.id}")
else
select_tag tag_name,
options_for_select([
select_tag(
tag_name,
options_for_select(
[
[l(:general_text_Yes), "1"],
[l(:general_text_No), "0"],
[l(:label_no_change_option), "no_change"]
], "no_change")
],
"no_change")
)
end
end
end