45 changes: 25 additions & 20 deletions app/controllers/settings_controller.rb
@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand All @@ -17,11 +17,12 @@

class SettingsController < ApplicationController
layout 'admin'
self.main_menu = false
menu_item :plugins, :only => :plugin

helper :queries

before_filter :require_admin
before_action :require_admin

require_sudo_mode :index, :edit, :plugin

Expand All @@ -32,27 +33,30 @@ def index

def edit
@notifiables = Redmine::Notifiable.all
if request.post? && params[:settings] && params[:settings].is_a?(Hash)
settings = (params[:settings] || {}).dup.symbolize_keys
settings.each do |name, value|
Setting.set_from_params name, value
if request.post?
errors = Setting.set_all_from_params(params[:settings])
if errors.blank?
flash[:notice] = l(:notice_successful_update)
redirect_to settings_path(:tab => params[:tab])
return
else
@setting_errors = errors
# render the edit form with error messages
end
flash[:notice] = l(:notice_successful_update)
redirect_to settings_path(:tab => params[:tab])
else
@options = {}
user_format = User::USER_FORMATS.collect{|key, value| [key, value[:setting_order]]}.sort{|a, b| a[1] <=> b[1]}
@options[:user_format] = user_format.collect{|f| [User.current.name(f[0]), f[0].to_s]}
@deliveries = ActionMailer::Base.perform_deliveries
end

@guessed_host_and_path = request.host_with_port.dup
@guessed_host_and_path << ('/'+ Redmine::Utils.relative_url_root.gsub(%r{^\/}, '')) unless Redmine::Utils.relative_url_root.blank?
@options = {}
user_format = User::USER_FORMATS.collect{|key, value| [key, value[:setting_order]]}.sort{|a, b| a[1] <=> b[1]}
@options[:user_format] = user_format.collect{|f| [User.current.name(f[0]), f[0].to_s]}
@deliveries = ActionMailer::Base.perform_deliveries

@commit_update_keywords = Setting.commit_update_keywords.dup
@commit_update_keywords = [{}] unless @commit_update_keywords.is_a?(Array) && @commit_update_keywords.any?
@guessed_host_and_path = request.host_with_port.dup
@guessed_host_and_path << ('/'+ Redmine::Utils.relative_url_root.gsub(%r{^\/}, '')) unless Redmine::Utils.relative_url_root.blank?

Redmine::Themes.rescan
end
@commit_update_keywords = Setting.commit_update_keywords.dup
@commit_update_keywords = [{}] unless @commit_update_keywords.is_a?(Array) && @commit_update_keywords.any?

Redmine::Themes.rescan
end

def plugin
Expand All @@ -63,7 +67,8 @@ def plugin
end

if request.post?
Setting.send "plugin_#{@plugin.id}=", params[:settings]
setting = params[:settings] ? params[:settings].permit!.to_h : {}
Setting.send "plugin_#{@plugin.id}=", setting
flash[:notice] = l(:notice_successful_update)
redirect_to plugin_settings_path(@plugin)
else
Expand Down
21 changes: 11 additions & 10 deletions app/controllers/sys_controller.rb
@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand All @@ -16,13 +16,13 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

class SysController < ActionController::Base
before_filter :check_enabled
before_action :check_enabled

def projects
p = Project.active.has_module(:repository).
order("#{Project.table_name}.identifier").preload(:repository).to_a
# extra_info attribute from repository breaks activeresource client
render :xml => p.to_xml(
render :json => p.to_json(
:only => [:id, :identifier, :name, :is_public, :status],
:include => {:repository => {:only => [:id, :url]}}
)
Expand All @@ -31,15 +31,16 @@ def projects
def create_project_repository
project = Project.find(params[:id])
if project.repository
render :nothing => true, :status => 409
head 409
else
logger.info "Repository for #{project.name} was reported to be created by #{request.remote_ip}."
repository = Repository.factory(params[:vendor], params[:repository])
repository = Repository.factory(params[:vendor])
repository.safe_attributes = params[:repository]
repository.project = project
if repository.save
render :xml => {repository.class.name.underscore.gsub('/', '-') => {:id => repository.id, :url => repository.url}}, :status => 201
render :json => {repository.class.name.underscore.gsub('/', '-') => {:id => repository.id, :url => repository.url}}, :status => 201
else
render :nothing => true, :status => 422
head 422
end
end
end
Expand All @@ -64,17 +65,17 @@ def fetch_changesets
repository.fetch_changesets
end
end
render :nothing => true, :status => 200
head 200
rescue ActiveRecord::RecordNotFound
render :nothing => true, :status => 404
head 404
end

protected

def check_enabled
User.current = nil
unless Setting.sys_api_enabled? && params[:key].to_s == Setting.sys_api_key
render :text => 'Access denied. Repository management WS is disabled or key is invalid.', :status => 403
render :plain => 'Access denied. Repository management WS is disabled or key is invalid.', :status => 403
return false
end
end
Expand Down
114 changes: 59 additions & 55 deletions app/controllers/timelog_controller.rb
@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand All @@ -16,22 +16,22 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

class TimelogController < ApplicationController
menu_item :issues
menu_item :time_entries

before_filter :find_time_entry, :only => [:show, :edit, :update]
before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
before_filter :authorize, :only => [:show, :edit, :update, :bulk_edit, :bulk_update, :destroy]
before_action :find_time_entry, :only => [:show, :edit, :update]
before_action :check_editability, :only => [:edit, :update]
before_action :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
before_action :authorize, :only => [:show, :edit, :update, :bulk_edit, :bulk_update, :destroy]

before_filter :find_optional_project, :only => [:new, :create, :index, :report]
before_filter :authorize_global, :only => [:new, :create, :index, :report]
before_action :find_optional_issue, :only => [:new, :create]
before_action :find_optional_project, :only => [:index, :report]
before_action :authorize_global, :only => [:new, :create, :index, :report]

accept_rss_auth :index
accept_api_auth :index, :show, :create, :update, :destroy

rescue_from Query::StatementInvalid, :with => :query_statement_invalid

helper :sort
include SortHelper
helper :issues
include TimelogHelper
helper :custom_fields
Expand All @@ -40,20 +40,16 @@ class TimelogController < ApplicationController
include QueriesHelper

def index
@query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')

sort_init(@query.sort_criteria.empty? ? [['spent_on', 'desc']] : @query.sort_criteria)
sort_update(@query.sortable_columns)
scope = time_entry_scope(:order => sort_clause).
includes(:project, :user, :issue).
preload(:issue => [:project, :tracker, :status, :assigned_to, :priority])
retrieve_time_entry_query
scope = time_entry_scope.
preload(:issue => [:project, :tracker, :status, :assigned_to, :priority]).
preload(:project, :user)

respond_to do |format|
format.html {
@entry_count = scope.count
@entry_pages = Paginator.new @entry_count, per_page_option, params['page']
@entries = scope.offset(@entry_pages.offset).limit(@entry_pages.per_page).to_a
@total_hours = scope.sum(:hours).to_f

render :layout => !request.xhr?
}
Expand All @@ -75,7 +71,7 @@ def index
end

def report
@query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
retrieve_time_entry_query
scope = time_entry_scope

@report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], scope)
Expand All @@ -89,7 +85,7 @@ def report
def show
respond_to do |format|
# TODO: Implement html response
format.html { render :nothing => true, :status => 406 }
format.html { head 406 }
format.api
end
end
Expand Down Expand Up @@ -169,26 +165,40 @@ def update
end

def bulk_edit
@available_activities = TimeEntryActivity.shared.active
@custom_fields = TimeEntry.first.available_custom_fields
@available_activities = @projects.map(&:activities).reduce(:&)
@custom_fields = TimeEntry.first.available_custom_fields.select {|field| field.format.bulk_edit_supported}
end

def bulk_update
attributes = parse_params_for_bulk_time_entry_attributes(params)
attributes = parse_params_for_bulk_update(params[:time_entry])

unsaved_time_entries = []
saved_time_entries = []

unsaved_time_entry_ids = []
@time_entries.each do |time_entry|
time_entry.reload
time_entry.safe_attributes = attributes
call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
unless time_entry.save
logger.info "time entry could not be updated: #{time_entry.errors.full_messages}" if logger && logger.info?
# Keep unsaved time_entry ids to display them in flash error
unsaved_time_entry_ids << time_entry.id
if time_entry.save
saved_time_entries << time_entry
else
unsaved_time_entries << time_entry
end
end
set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
redirect_back_or_default project_time_entries_path(@projects.first)

if unsaved_time_entries.empty?
flash[:notice] = l(:notice_successful_update) unless saved_time_entries.empty?
redirect_back_or_default project_time_entries_path(@projects.first)
else
@saved_time_entries = @time_entries
@unsaved_time_entries = unsaved_time_entries
@time_entries = TimeEntry.where(:id => unsaved_time_entries.map(&:id)).
preload(:project => :time_entry_activities).
preload(:user).to_a

bulk_edit
render :action => 'bulk_edit'
end
end

def destroy
Expand All @@ -207,7 +217,7 @@ def destroy
else
flash[:error] = l(:notice_unable_delete_time_entry)
end
redirect_back_or_default project_time_entries_path(@projects.first)
redirect_back_or_default project_time_entries_path(@projects.first), :referer => true
}
format.api {
if destroyed
Expand All @@ -222,17 +232,23 @@ def destroy
private
def find_time_entry
@time_entry = TimeEntry.find(params[:id])
@project = @time_entry.project
rescue ActiveRecord::RecordNotFound
render_404
end

def check_editability
unless @time_entry.editable_by?(User.current)
render_403
return false
end
@project = @time_entry.project
rescue ActiveRecord::RecordNotFound
render_404
end

def find_time_entries
@time_entries = TimeEntry.where(:id => params[:id] || params[:ids]).to_a
@time_entries = TimeEntry.where(:id => params[:id] || params[:ids]).
preload(:project => :time_entry_activities).
preload(:user).to_a

raise ActiveRecord::RecordNotFound if @time_entries.empty?
raise Unauthorized unless @time_entries.all? {|t| t.editable_by?(User.current)}
@projects = @time_entries.collect(&:project).compact.uniq
Expand All @@ -241,22 +257,17 @@ def find_time_entries
render_404
end

def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
if unsaved_time_entry_ids.empty?
flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
def find_optional_issue
if params[:issue_id].present?
@issue = Issue.find(params[:issue_id])
@project = @issue.project
else
flash[:error] = l(:notice_failed_to_save_time_entries,
:count => unsaved_time_entry_ids.size,
:total => time_entries.size,
:ids => '#' + unsaved_time_entry_ids.join(', #'))
find_optional_project
end
end

def find_optional_project
if params[:issue_id].present?
@issue = Issue.find(params[:issue_id])
@project = @issue.project
elsif params[:project_id].present?
if params[:project_id].present?
@project = Project.find(params[:project_id])
end
rescue ActiveRecord::RecordNotFound
Expand All @@ -265,17 +276,10 @@ def find_optional_project

# Returns the TimeEntry scope for index and report actions
def time_entry_scope(options={})
scope = @query.results_scope(options)
if @issue
scope = scope.on_issue(@issue)
end
scope
@query.results_scope(options)
end

def parse_params_for_bulk_time_entry_attributes(params)
attributes = (params[:time_entry] || {}).reject {|k,v| v.blank?}
attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
attributes
def retrieve_time_entry_query
retrieve_query(TimeEntryQuery, false)
end
end
48 changes: 29 additions & 19 deletions app/controllers/trackers_controller.rb
@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand All @@ -17,35 +17,34 @@

class TrackersController < ApplicationController
layout 'admin'
self.main_menu = false

before_filter :require_admin, :except => :index
before_filter :require_admin_or_api_request, :only => :index
before_action :require_admin, :except => :index
before_action :require_admin_or_api_request, :only => :index
accept_api_auth :index

def index
@trackers = Tracker.sorted.to_a
respond_to do |format|
format.html {
@tracker_pages, @trackers = paginate Tracker.sorted, :per_page => 25
render :action => "index", :layout => false if request.xhr?
}
format.api {
@trackers = Tracker.sorted.to_a
}
format.html { render :layout => false if request.xhr? }
format.api
end
end

def new
@tracker ||= Tracker.new(params[:tracker])
@tracker ||= Tracker.new
@tracker.safe_attributes = params[:tracker]
@trackers = Tracker.sorted.to_a
@projects = Project.all
end

def create
@tracker = Tracker.new(params[:tracker])
@tracker = Tracker.new
@tracker.safe_attributes = params[:tracker]
if @tracker.save
# workflow copy
if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from]))
@tracker.workflow_rules.copy(copy_from)
@tracker.copy_workflow_rules(copy_from)
end
flash[:notice] = l(:notice_successful_create)
redirect_to trackers_path
Expand All @@ -62,13 +61,24 @@ def edit

def update
@tracker = Tracker.find(params[:id])
if @tracker.update_attributes(params[:tracker])
flash[:notice] = l(:notice_successful_update)
redirect_to trackers_path(:page => params[:page])
return
@tracker.safe_attributes = params[:tracker]
if @tracker.save
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_update)
redirect_to trackers_path(:page => params[:page])
}
format.js { head 200 }
end
else
respond_to do |format|
format.html {
edit
render :action => 'edit'
}
format.js { head 422 }
end
end
edit
render :action => 'edit'
end

def destroy
Expand Down
30 changes: 15 additions & 15 deletions app/controllers/users_controller.rb
@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand All @@ -17,9 +17,11 @@

class UsersController < ApplicationController
layout 'admin'
self.main_menu = false

before_filter :require_admin, :except => :show
before_filter :find_user, :only => [:show, :edit, :update, :destroy]
before_action :require_admin, :except => :show
before_action ->{ find_user(false) }, :only => :show
before_action :find_user, :only => [:edit, :update, :destroy]
accept_api_auth :index, :show, :create, :update, :destroy

helper :sort
Expand Down Expand Up @@ -54,7 +56,7 @@ def index

respond_to do |format|
format.html {
@groups = Group.all.sort
@groups = Group.givable.sort
render :layout => !request.xhr?
}
format.api
Expand All @@ -68,12 +70,12 @@ def show
end

# show projects based on current user visibility
@memberships = @user.memberships.where(Project.visible_condition(User.current)).to_a
@memberships = @user.memberships.preload(:roles, :project).where(Project.visible_condition(User.current)).to_a

respond_to do |format|
format.html {
events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
@events_by_day = events.group_by(&:event_date)
@events_by_day = events.group_by {|event| User.current.time_to_date(event.event_datetime)}
render :layout => 'base'
}
format.api
Expand All @@ -87,12 +89,10 @@ def new
end

def create
@user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
@user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option, :admin => false)
@user.safe_attributes = params[:user]
@user.admin = params[:user][:admin] || false
@user.login = params[:user][:login]
@user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation] unless @user.auth_source_id
@user.pref.attributes = params[:pref] if params[:pref]
@user.pref.safe_attributes = params[:pref]

if @user.save
Mailer.account_information(@user, @user.password).deliver if params[:send_information]
Expand Down Expand Up @@ -127,23 +127,21 @@ def edit
end

def update
@user.admin = params[:user][:admin] if params[:user][:admin]
@user.login = params[:user][:login] if params[:user][:login]
if params[:user][:password].present? && (@user.auth_source_id.nil? || params[:user][:auth_source_id].blank?)
@user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation]
end
@user.safe_attributes = params[:user]
# Was the account actived ? (do it before User#save clears the change)
was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
# TODO: Similar to My#account
@user.pref.attributes = params[:pref] if params[:pref]
@user.pref.safe_attributes = params[:pref]

if @user.save
@user.pref.save

if was_activated
Mailer.account_activated(@user).deliver
elsif @user.active? && params[:send_information] && @user.password.present? && @user.auth_source_id.nil?
elsif @user.active? && params[:send_information] && @user != User.current
Mailer.account_information(@user, @user.password).deliver
end

Expand Down Expand Up @@ -177,10 +175,12 @@ def destroy

private

def find_user
def find_user(logged = true)
if params[:id] == 'current'
require_login || return
@user = User.current
elsif logged
@user = User.logged.find(params[:id])
else
@user = User.find(params[:id])
end
Expand Down
19 changes: 10 additions & 9 deletions app/controllers/versions_controller.rb
@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand All @@ -18,10 +18,10 @@
class VersionsController < ApplicationController
menu_item :roadmap
model_object Version
before_filter :find_model_object, :except => [:index, :new, :create, :close_completed]
before_filter :find_project_from_association, :except => [:index, :new, :create, :close_completed]
before_filter :find_project_by_project_id, :only => [:index, :new, :create, :close_completed]
before_filter :authorize
before_action :find_model_object, :except => [:index, :new, :create, :close_completed]
before_action :find_project_from_association, :except => [:index, :new, :create, :close_completed]
before_action :find_project_by_project_id, :only => [:index, :new, :create, :close_completed]
before_action :authorize

accept_api_auth :index, :show, :create, :update, :destroy

Expand All @@ -36,11 +36,11 @@ def index
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]

@versions = @project.shared_versions || []
@versions += @project.rolled_up_versions.visible if @with_subprojects
@versions = @versions.uniq.sort
@versions = @project.shared_versions.preload(:custom_values)
@versions += @project.rolled_up_versions.visible.preload(:custom_values) if @with_subprojects
@versions = @versions.to_a.uniq.sort
unless params[:completed]
@completed_versions = @versions.select {|version| version.closed? || version.completed? }
@completed_versions = @versions.select(&:completed?).reverse
@versions -= @completed_versions
end

Expand All @@ -66,6 +66,7 @@ def show
format.html {
@issues = @version.fixed_issues.visible.
includes(:status, :tracker, :priority).
preload(:project).
reorder("#{Tracker.table_name}.position, #{Issue.table_name}.id").
to_a
}
Expand Down
81 changes: 51 additions & 30 deletions app/controllers/watchers_controller.rb
@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand All @@ -16,7 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

class WatchersController < ApplicationController
before_filter :require_login, :find_watchables, :only => [:watch, :unwatch]
before_action :require_login, :find_watchables, :only => [:watch, :unwatch]

def watch
set_watcher(@watchables, User.current, true)
Expand All @@ -26,7 +26,7 @@ def unwatch
set_watcher(@watchables, User.current, false)
end

before_filter :find_project, :authorize, :only => [:new, :create, :append, :destroy, :autocomplete_for_user]
before_action :find_project, :authorize, :only => [:new, :create, :append, :destroy, :autocomplete_for_user]
accept_api_auth :create, :destroy

def new
Expand All @@ -35,39 +35,46 @@ def new

def create
user_ids = []
if params[:watcher].is_a?(Hash)
if params[:watcher]
user_ids << (params[:watcher][:user_ids] || params[:watcher][:user_id])
else
user_ids << params[:user_id]
end
users = User.active.visible.where(:id => user_ids.flatten.compact.uniq)
users.each do |user|
Watcher.create(:watchable => @watched, :user => user)
@watchables.each do |watchable|
Watcher.create(:watchable => watchable, :user => user)
end
end
respond_to do |format|
format.html { redirect_to_referer_or {render :text => 'Watcher added.', :layout => true}}
format.html { redirect_to_referer_or {render :html => 'Watcher added.', :status => 200, :layout => true}}
format.js { @users = users_for_new_watcher }
format.api { render_api_ok }
end
end

def append
if params[:watcher].is_a?(Hash)
if params[:watcher]
user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
@users = User.active.visible.where(:id => user_ids).to_a
end
if @users.blank?
render :nothing => true
head 200
end
end

def destroy
@watched.set_watcher(User.visible.find(params[:user_id]), false)
user = User.find(params[:user_id])
@watchables.each do |watchable|
watchable.set_watcher(user, false)
end
respond_to do |format|
format.html { redirect_to :back }
format.html { redirect_to_referer_or {render :html => 'Watcher removed.', :status => 200, :layout => true} }
format.js
format.api { render_api_ok }
end
rescue ActiveRecord::RecordNotFound
render_404
end

def autocomplete_for_user
Expand All @@ -79,38 +86,32 @@ def autocomplete_for_user

def find_project
if params[:object_type] && params[:object_id]
klass = Object.const_get(params[:object_type].camelcase)
return false unless klass.respond_to?('watched_by')
@watched = klass.find(params[:object_id])
@project = @watched.project
@watchables = find_objets_from_params
@projects = @watchables.map(&:project).uniq
if @projects.size == 1
@project = @projects.first
end
elsif params[:project_id]
@project = Project.visible.find_by_param(params[:project_id])
end
rescue
render_404
end

def find_watchables
klass = Object.const_get(params[:object_type].camelcase) rescue nil
if klass && klass.respond_to?('watched_by')
@watchables = klass.where(:id => Array.wrap(params[:object_id])).to_a
raise Unauthorized if @watchables.any? {|w|
if w.respond_to?(:visible?)
!w.visible?
elsif w.respond_to?(:project) && w.project
!w.project.visible?
end
}
@watchables = find_objets_from_params
unless @watchables.present?
render_404
end
render_404 unless @watchables.present?
end

def set_watcher(watchables, user, watching)
watchables.each do |watchable|
watchable.set_watcher(user, watching)
end
respond_to do |format|
format.html { redirect_to_referer_or {render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true}}
format.html {
text = watching ? 'Watcher added.' : 'Watcher removed.'
redirect_to_referer_or {render :html => text, :status => 200, :layout => true}
}
format.js { render :partial => 'set_watcher', :locals => {:user => user, :watched => watchables} }
end
end
Expand All @@ -123,9 +124,29 @@ def users_for_new_watcher
scope = User.all.limit(100)
end
users = scope.active.visible.sorted.like(params[:q]).to_a
if @watched
users -= @watched.watcher_users
if @watchables && @watchables.size == 1
users -= @watchables.first.watcher_users
end
users
end

def find_objets_from_params
klass = Object.const_get(params[:object_type].camelcase) rescue nil
return unless klass && klass.respond_to?('watched_by')

scope = klass.where(:id => Array.wrap(params[:object_id]))
if klass.reflect_on_association(:project)
scope = scope.preload(:project => :enabled_modules)
end
objects = scope.to_a

raise Unauthorized if objects.any? do |w|
if w.respond_to?(:visible?)
!w.visible?
elsif w.respond_to?(:project) && w.project
!w.project.visible?
end
end
objects
end
end
5 changes: 2 additions & 3 deletions app/controllers/welcome_controller.rb
@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand All @@ -16,11 +16,10 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

class WelcomeController < ApplicationController
caches_action :robots
self.main_menu = false

def index
@news = News.latest User.current
@projects = Project.latest User.current
end

def robots
Expand Down
45 changes: 35 additions & 10 deletions app/controllers/wiki_controller.rb
@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down Expand Up @@ -31,11 +31,11 @@
# TODO: still being worked on
class WikiController < ApplicationController
default_search_scope :wiki_pages
before_filter :find_wiki, :authorize
before_filter :find_existing_or_new_page, :only => [:show, :edit, :update]
before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy, :destroy_version]
before_action :find_wiki, :authorize
before_action :find_existing_or_new_page, :only => [:show, :edit, :update]
before_action :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy, :destroy_version]
before_action :find_attachments, :only => [:preview]
accept_api_auth :index, :show, :update, :destroy
before_filter :find_attachments, :only => [:preview]

helper :attachments
include AttachmentsHelper
Expand All @@ -60,6 +60,25 @@ def date_index
@pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
end

def new
@page = WikiPage.new(:wiki => @wiki, :title => params[:title])
unless User.current.allowed_to?(:edit_wiki_pages, @project)
render_403
return
end
if request.post?
@page.title = '' unless editable?
@page.validate
if @page.errors[:title].blank?
path = project_wiki_page_path(@project, @page.title)
respond_to do |format|
format.html { redirect_to path }
format.js { render :js => "window.location = #{path.to_json}" }
end
end
end
end

# display a page (in editing mode if it doesn't exist)
def show
if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
Expand All @@ -76,6 +95,9 @@ def show
end
return
end

call_hook :controller_wiki_show_before_render, content: @content, format: params[:format]

if User.current.allowed_to?(:export_wiki_pages, @project)
if params[:format] == 'pdf'
send_file_headers! :type => 'application/pdf', :filename => "#{@page.title}.pdf"
Expand Down Expand Up @@ -134,7 +156,7 @@ def update

@content = @page.content || WikiContent.new(:page => @page)
content_params = params[:content]
if content_params.nil? && params[:wiki_page].is_a?(Hash)
if content_params.nil? && params[:wiki_page].present?
content_params = params[:wiki_page].slice(:text, :comments, :version)
end
content_params ||= {}
Expand All @@ -152,7 +174,7 @@ def update
@content.author = User.current

if @page.save_with_content(@content)
attachments = Attachment.attach_files(@page, params[:attachments])
attachments = Attachment.attach_files(@page, params[:attachments] || (params[:wiki_page] && params[:wiki_page][:uploads]))
render_attachment_warning_if_needed(@page)
call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})

Expand Down Expand Up @@ -266,9 +288,12 @@ def destroy
def destroy_version
return render_403 unless editable?

@content = @page.content_for_version(params[:version])
@content.destroy
redirect_to_referer_or history_project_wiki_page_path(@project, @page.title)
if content = @page.content.versions.find_by_version(params[:version])
content.destroy
redirect_to_referer_or history_project_wiki_page_path(@project, @page.title)
else
render_404
end
end

# Export wiki to a single pdf or html file
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/wikis_controller.rb
@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand All @@ -17,7 +17,7 @@

class WikisController < ApplicationController
menu_item :settings
before_filter :find_project, :authorize
before_action :find_project, :authorize

# Create or update a project's wiki
def edit
Expand Down
15 changes: 11 additions & 4 deletions app/controllers/workflows_controller.rb
@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand All @@ -17,8 +17,9 @@

class WorkflowsController < ApplicationController
layout 'admin'
self.main_menu = false

before_filter :require_admin
before_action :require_admin

def index
@roles = Role.sorted.select(&:consider_workflow?)
Expand All @@ -43,7 +44,9 @@ def edit
end

if @trackers && @roles && @statuses.any?
workflows = WorkflowTransition.where(:role_id => @roles.map(&:id), :tracker_id => @trackers.map(&:id))
workflows = WorkflowTransition.
where(:role_id => @roles.map(&:id), :tracker_id => @trackers.map(&:id)).
preload(:old_status, :new_status)
@workflows = {}
@workflows['always'] = workflows.select {|w| !w.author && !w.assignee}
@workflows['author'] = workflows.select {|w| w.author}
Expand Down Expand Up @@ -135,7 +138,11 @@ def find_trackers
def find_statuses
@used_statuses_only = (params[:used_statuses_only] == '0' ? false : true)
if @trackers && @used_statuses_only
@statuses = @trackers.map(&:issue_statuses).flatten.uniq.sort.presence
role_ids = Role.all.select(&:consider_workflow?).map(&:id)
status_ids = WorkflowTransition.where(
:tracker_id => @trackers.map(&:id), :role_id => role_ids
).distinct.pluck(:old_status_id, :new_status_id).flatten.uniq
@statuses = IssueStatus.where(:id => status_ids).sorted.to_a.presence
end
@statuses ||= IssueStatus.sorted.to_a
end
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/account_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/activities_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/admin_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down
341 changes: 270 additions & 71 deletions app/helpers/application_helper.rb

Large diffs are not rendered by default.

40 changes: 26 additions & 14 deletions app/helpers/attachments_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down Expand Up @@ -34,7 +34,12 @@ def container_attachments_path(container)
def link_to_attachments(container, options = {})
options.assert_valid_keys(:author, :thumbnails)

attachments = container.attachments.preload(:author).to_a
attachments = if container.attachments.loaded?
container.attachments
else
container.attachments.preload(:author).to_a
end

if attachments.any?
options = {
:editable => container.attachments_editable?,
Expand All @@ -51,19 +56,26 @@ def link_to_attachments(container, options = {})
end
end

def render_api_attachment(attachment, api)
def render_api_attachment(attachment, api, options={})
api.attachment do
api.id attachment.id
api.filename attachment.filename
api.filesize attachment.filesize
api.content_type attachment.content_type
api.description attachment.description
api.content_url download_named_attachment_url(attachment, attachment.filename)
if attachment.thumbnailable?
api.thumbnail_url thumbnail_url(attachment)
end
api.author(:id => attachment.author.id, :name => attachment.author.name) if attachment.author
api.created_on attachment.created_on
render_api_attachment_attributes(attachment, api)
options.each { |key, value| eval("api.#{key} value") }
end
end

def render_api_attachment_attributes(attachment, api)
api.id attachment.id
api.filename attachment.filename
api.filesize attachment.filesize
api.content_type attachment.content_type
api.description attachment.description
api.content_url download_named_attachment_url(attachment, attachment.filename)
if attachment.thumbnailable?
api.thumbnail_url thumbnail_url(attachment)
end
if attachment.author
api.author(:id => attachment.author.id, :name => attachment.author.name)
end
api.created_on attachment.created_on
end
end
2 changes: 1 addition & 1 deletion app/helpers/auth_sources_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/boards_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down
4 changes: 2 additions & 2 deletions app/helpers/calendars_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down Expand Up @@ -53,6 +53,6 @@ def link_to_next_month(year, month, options={})
end

def link_to_month(link_name, year, month, options={})
link_to_content_update(h(link_name), params.merge(:year => year, :month => month), options)
link_to(link_name, {:params => request.query_parameters.merge(:year => year, :month => month)}, options)
end
end
2 changes: 1 addition & 1 deletion app/helpers/context_menus_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down
25 changes: 21 additions & 4 deletions app/helpers/custom_fields_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down Expand Up @@ -51,6 +51,15 @@ def custom_field_type_options
CUSTOM_FIELDS_TABS.map {|h| [l(h[:label]), h[:name]]}
end

def custom_field_title(custom_field)
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)

title(*items)
end

def render_custom_field_format_partial(form, custom_field)
partial = custom_field.format.form_partial
if partial
Expand Down Expand Up @@ -80,22 +89,30 @@ def custom_field_tag(prefix, custom_value)
# Return custom field name tag
def custom_field_name_tag(custom_field)
title = custom_field.description.presence
content_tag 'span', custom_field.name, :title => title
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 +
(required ? " <span class=\"required\">*</span>".html_safe : ""),
:for => "#{name}_custom_field_values_#{custom_value.custom_field.id}"
:for => for_tag_id
end

# Return custom field tag with its label tag
def custom_field_tag_with_label(name, custom_value, options={})
custom_field_label_tag(name, custom_value, options) + custom_field_tag(name, custom_value)
tag = custom_field_tag(name, custom_value)
tag_id = nil
ids = tag.scan(/ id="(.+?)"/)
if ids.size == 1
tag_id = ids.first.first
end
custom_field_label_tag(name, custom_value, options.merge(:for_tag_id => tag_id)) + tag
end

# Returns the custom field tag for when bulk editing objects
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/documents_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down
14 changes: 7 additions & 7 deletions app/helpers/email_addresses_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand All @@ -22,17 +22,17 @@ 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 image_tag('email.png'),
link_to l(:label_disable_notifications),
user_email_address_path(address.user, address, :notify => '0'),
:method => :put,
:method => :put, :remote => true,
:title => l(:label_disable_notifications),
:remote => true
:class => 'icon-only icon-email'
else
link_to image_tag('email_disabled.png'),
link_to l(:label_enable_notifications),
user_email_address_path(address.user, address, :notify => '1'),
:method => :put,
:method => :put, :remote => true,
:title => l(:label_enable_notifications),
:remote => true
:class => 'icon-only icon-email-disabled'
end
end
end
2 changes: 1 addition & 1 deletion app/helpers/enumerations_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down
10 changes: 5 additions & 5 deletions app/helpers/gantt_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand All @@ -23,17 +23,17 @@ def gantt_zoom_link(gantt, in_or_out)
case in_or_out
when :in
if gantt.zoom < 4
link_to_content_update l(:text_zoom_in),
params.merge(gantt.params.merge(:zoom => (gantt.zoom + 1))),
link_to l(:text_zoom_in),
{:params => request.query_parameters.merge(gantt.params.merge(:zoom => (gantt.zoom + 1)))},
: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_content_update l(:text_zoom_out),
params.merge(gantt.params.merge(:zoom => (gantt.zoom - 1))),
link_to l(:text_zoom_out),
{:params => request.query_parameters.merge(gantt.params.merge(:zoom => (gantt.zoom - 1)))},
:class => 'icon icon-zoom-out'
else
content_tag(:span, l(:text_zoom_out), :class => 'icon icon-zoom-out').html_safe
Expand Down
4 changes: 2 additions & 2 deletions app/helpers/groups_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down Expand Up @@ -41,6 +41,6 @@ def render_principals_for_new_group_users(group, limit=100)
link_to text, autocomplete_for_user_group_path(group, parameters.merge(:q => params[:q], :format => 'js')), :remote => true
}

s + content_tag('p', links, :class => 'pagination')
s + content_tag('span', links, :class => 'pagination')
end
end
47 changes: 47 additions & 0 deletions app/helpers/imports_helper.rb
@@ -0,0 +1,47 @@
# encoding: utf-8
#
# 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 ImportsHelper
def options_for_mapping_select(import, field, options={})
tags = "".html_safe
blank_text = options[:required] ? "-- #{l(:actionview_instancetag_blank_option)} --" : "&nbsp;".html_safe
tags << content_tag('option', blank_text, :value => '')
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])
end
tags
end

def mapping_select_tag(import, field, options={})
name = "import_settings[mapping][#{field}]"
select_tag name, options_for_mapping_select(import, field, options), :id => "import_mapping_#{field}"
end

# Returns the options for the date_format setting
def date_format_options
Import::DATE_FORMATS.map do |f|
format = f.gsub('%', '').gsub(/[dmY]/) do
{'d' => 'DD', 'm' => 'MM', 'Y' => 'YYYY'}[$&]
end
[format, f]
end
end
end
2 changes: 1 addition & 1 deletion app/helpers/issue_categories_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/issue_relations_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/issue_statuses_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down
234 changes: 130 additions & 104 deletions app/helpers/issues_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down Expand Up @@ -32,21 +32,14 @@ def issue_list(issues, &block)
end
end

def grouped_issue_list(issues, query, issue_count_by_group, &block)
previous_group, first = false, true
issue_list(issues) do |issue, level|
group_name = group_count = nil
if query.grouped? && ((group = query.group_by_column.value(issue)) != previous_group || first)
if group.blank? && group != false
group_name = "(#{l(:label_blank_value)})"
else
group_name = column_content(query.group_by_column, issue)
end
group_name ||= ""
group_count = issue_count_by_group[group]
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))
ancestors.pop
end
yield issue, level, group_name, group_count
previous_group, first = group, false
yield issue, ancestors.size, group_name, group_count, group_totals
ancestors << issue unless issue.leaf?
end
end

Expand Down Expand Up @@ -97,22 +90,54 @@ def render_issue_subject_with_tree(issue)
end

def render_descendants_tree(issue)
s = '<form><table class="list issues">'
issue_list(issue.descendants.visible.preload(:status, :priority, :tracker).sort_by(&:lft)) do |child, level|
css = "issue issue-#{child.id} hascontextmenu"
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%') +
content_tag('td', h(child.status)) +
content_tag('td', link_to_user(child.assigned_to)) +
content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
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)
end
s << '</table></form>'
s << '</table>'
s.html_safe
end

# 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%') +
content_tag('td', other_issue.status, :class => 'status') +
content_tag('td', other_issue.start_date, :class => 'start_date') +
content_tag('td', 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)
end

content_tag('table', s, :class => 'list issues odd-even')
end

def issue_estimated_hours_details(issue)
if issue.total_estimated_hours.present?
if issue.total_estimated_hours == issue.estimated_hours
Expand All @@ -127,11 +152,13 @@ def issue_estimated_hours_details(issue)

def issue_spent_hours_details(issue)
if issue.total_spent_hours > 0
path = project_time_entries_path(issue.project, :issue_id => "~#{issue.id}")

if issue.total_spent_hours == issue.spent_hours
link_to(l_hours_short(issue.spent_hours), issue_time_entries_path(issue))
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), issue_time_entries_path(issue)})"
s << " (#{l(:label_total)}: #{link_to l_hours_short(issue.total_spent_hours), path})"
s.html_safe
end
end
Expand All @@ -154,10 +181,20 @@ def bulk_edit_error_messages(issues)
# Returns a link for adding a new subtask to the given issue
def link_to_new_subtask(issue)
attrs = {
:tracker_id => issue.tracker,
:parent_issue_id => issue
}
link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
attrs[:tracker_id] = issue.tracker unless issue.tracker.disabled_core_fields.include?('parent_issue_id')
link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs, :back_url => issue_path(issue)))
end

def trackers_options_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]}
end

class IssueFieldsRows
Expand All @@ -181,18 +218,18 @@ def size
end

def to_html
html = ''.html_safe
blank = content_tag('th', '') + content_tag('td', '')
size.times do |i|
left = @left[i] || blank
right = @right[i] || blank
html << content_tag('tr', left + right)
end
html
content =
content_tag('div', @left.reduce(&:+), :class => 'splitcontentleft') +
content_tag('div', @right.reduce(&:+), :class => 'splitcontentleft')

content_tag('div', content, :class => 'splitcontent')
end

def cells(label, text, options={})
content_tag('th', "#{label}:", options) + content_tag('td', text, options)
options[:class] = [options[:class] || "", 'attribute'].join(' ')
content_tag 'div',
content_tag('div', label + ":", :class => 'label') + content_tag('div', text, :class => 'value'),
options
end
end

Expand All @@ -202,25 +239,39 @@ def issue_fields_rows
r.to_html
end

def render_custom_fields_rows(issue)
values = issue.visible_custom_field_values
def render_half_width_custom_fields_rows(issue)
values = issue.visible_custom_field_values.reject {|value| value.custom_field.full_width_layout?}
return if values.empty?
ordered_values = []
half = (values.size / 2.0).ceil
half.times do |i|
ordered_values << values[i]
ordered_values << values[i + half]
end
s = "<tr>\n"
n = 0
ordered_values.compact.each do |value|
css = "cf_#{value.custom_field.id}"
s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
s << "\t<th class=\"#{css}\">#{ custom_field_name_tag(value.custom_field) }:</th><td class=\"#{css}\">#{ h(show_value(value)) }</td>\n"
n += 1
end
s << "</tr>\n"
s.html_safe
issue_fields_rows do |rows|
values.each_with_index do |value, i|
css = "cf_#{value.custom_field.id}"
m = (i < half ? :left : :right)
rows.send m, custom_field_name_tag(value.custom_field), show_value(value), :class => css
end
end
end

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

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")
end
s
end

# Returns the path for updating the issue form
Expand Down Expand Up @@ -266,57 +317,31 @@ def users_for_new_issue_watchers(issue)
users
end

def sidebar_queries
unless @sidebar_queries
@sidebar_queries = IssueQuery.visible.
order("#{Query.table_name}.name ASC").
# Project specific queries and global queries
where(@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]).
to_a
end
@sidebar_queries
end

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} : params

content_tag('h3', title) + "\n" +
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,
:class => 'queries'
) + "\n"
end

def render_sidebar_queries
out = ''.html_safe
out << query_links(l(:label_my_queries), sidebar_queries.select(&:is_private?))
out << query_links(l(:label_query_plural), sidebar_queries.reject(&:is_private?))
out
end

def email_issue_attributes(issue, user)
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")
items << "#{l("field_#{attribute}")}: #{issue.send attribute}"
if html
items << content_tag('strong', "#{l("field_#{attribute}")}: ") + (issue.send attribute)
else
items << "#{l("field_#{attribute}")}: #{issue.send attribute}"
end
end
end
issue.visible_custom_field_values(user).each do |value|
items << "#{value.custom_field.name}: #{show_value(value, false)}"
if html
items << content_tag('strong', "#{value.custom_field.name}: ") + show_value(value, false)
else
items << "#{value.custom_field.name}: #{show_value(value, false)}"
end
end
items
end

def render_email_issue_attributes(issue, user, html=false)
items = email_issue_attributes(issue, user)
items = email_issue_attributes(issue, user, html)
if html
content_tag('ul', items.map{|s| content_tag('li', s)}.join("\n").html_safe)
content_tag('ul', items.map{|s| content_tag('li', s)}.join("\n").html_safe, :class => "details")
else
items.map{|s| "* #{s}"}.join("\n")
end
Expand Down Expand Up @@ -366,6 +391,7 @@ def details_to_strings(details, no_html=false, options={})
def show_detail(detail, no_html=false, options={})
multiple = false
show_diff = false
no_details = false

case detail.property
when 'attr'
Expand All @@ -382,8 +408,8 @@ def show_detail(detail, no_html=false, options={})
old_value = find_name_by_reflection(field, detail.old_value)

when 'estimated_hours'
value = "%0.02f" % detail.value.to_f unless detail.value.blank?
old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
value = l_hours_short(detail.value.to_f) unless detail.value.blank?
old_value = l_hours_short(detail.old_value.to_f) unless detail.old_value.blank?

when 'parent_id'
label = l(:field_parent_issue)
Expand All @@ -401,7 +427,9 @@ def show_detail(detail, no_html=false, options={})
custom_field = detail.custom_field
if custom_field
label = custom_field.name
if custom_field.format.class.change_as_diff
if custom_field.format.class.change_no_details
no_details = true
elsif custom_field.format.class.change_as_diff
show_diff = true
else
multiple = custom_field.multiple?
Expand Down Expand Up @@ -440,25 +468,23 @@ def show_detail(detail, no_html=false, options={})
if detail.property == 'attachment' && value.present? &&
atta = detail.journal.journalized.attachments.detect {|a| a.id == detail.prop_key.to_i}
# Link to the attachment if it has not been removed
value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
if options[:only_path] != false && atta.is_text?
value += link_to(
image_tag('magnifier.png'),
:controller => 'attachments', :action => 'show',
:id => atta, :filename => atta.filename
)
value = link_to_attachment(atta, only_path: options[:only_path])
if options[:only_path] != false
value += ' '
value += link_to_attachment atta, class: 'icon-only icon-download', title: l(:button_download), download: true
end
else
value = content_tag("i", h(value)) if value
end
end

if show_diff
if no_details
s = l(:text_journal_changed_no_detail, :label => label).html_safe
elsif show_diff
s = l(:text_journal_changed_no_detail, :label => label)
unless no_html
diff_link = link_to 'diff',
{:controller => 'journals', :action => 'diff', :id => detail.journal_id,
:detail_id => detail.id, :only_path => options[:only_path]},
diff_journal_url(detail.journal_id, :detail_id => detail.id, :only_path => options[:only_path]),
:title => l(:label_view_diff)
s << " (#{ diff_link })"
end
Expand Down
51 changes: 37 additions & 14 deletions app/helpers/journals_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand All @@ -18,29 +18,52 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

module JournalsHelper

# Returns the attachments of a journal that are displayed as thumbnails
def journal_thumbnail_attachments(journal)
ids = journal.details.select {|d| d.property == 'attachment' && d.value.present?}.map(&:prop_key)
ids.any? ? Attachment.where(:id => ids).select(&:thumbnailable?) : []
end

def render_notes(issue, journal, options={})
content = ''
editable = User.current.logged? && (User.current.allowed_to?(:edit_issue_notes, issue.project) || (journal.user == User.current && User.current.allowed_to?(:edit_own_issue_notes, issue.project)))
css_classes = "wiki"
links = []
if !journal.notes.blank?
links << link_to(image_tag('comment.png'),
{:controller => 'journals', :action => 'new', :id => issue, :journal_id => journal},
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)) if options[:reply_links]
links << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes",
{ :controller => 'journals', :action => 'edit', :id => journal, :format => 'js' },
:title => l(:button_edit)) if editable
:title => l(:button_quote),
:class => 'icon-only icon-comment'
) if options[:reply_links]

if journal.editable_by?(User.current)
links << link_to(l(:button_edit),
edit_journal_path(journal),
:remote => true,
:method => 'get',
:title => l(:button_edit),
:class => 'icon-only icon-edit'
)
links << link_to(l(:button_delete),
journal_path(journal, :journal => {:notes => ""}),
:remote => true,
: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)
css_classes = "wiki"
css_classes << " editable" if editable
content_tag('div', content.html_safe, :id => "journal-#{journal.id}-notes", :class => css_classes)
end

def link_to_in_place_notes_editor(text, field_id, url, options={})
onclick = "$.ajax({url: '#{url_for(url)}', type: 'get'}); return false;"
link_to text, '#', options.merge(:onclick => onclick)
def render_private_notes_indicator(journal)
content = journal.private_notes? ? l(:field_is_private) : ''
css_classes = journal.private_notes? ? 'private' : ''
content_tag('span', content.html_safe, :id => "journal-#{journal.id}-private_notes", :class => css_classes)
end
end
2 changes: 1 addition & 1 deletion app/helpers/mail_handler_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down
4 changes: 2 additions & 2 deletions app/helpers/members_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down Expand Up @@ -33,6 +33,6 @@ def render_principals_for_new_members(project, limit=100)
link_to text, autocomplete_project_memberships_path(project, parameters.merge(:q => params[:q], :format => 'js')), :remote => true
}

s + content_tag('p', links, :class => 'pagination')
s + content_tag('span', links, :class => 'pagination')
end
end
2 changes: 1 addition & 1 deletion app/helpers/messages_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down
148 changes: 119 additions & 29 deletions app/helpers/my_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand All @@ -18,60 +18,150 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

module MyHelper
def calendar_items(startdt, enddt)
Issue.visible.
# Renders the blocks
def render_blocks(blocks, user, options={})
s = ''.html_safe

if blocks.present?
blocks.each do |block|
s << render_block(block, user).to_s
end
end
s
end

# Renders a single block
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))
close = link_to(l(:button_delete),
{:action => "remove_block", :block => block},
:remote => true, :method => 'post',
:class => "icon-only icon-close", :title => l(:button_delete))
content = content_tag('div', handle + close, :class => 'contextual') + content

content_tag('div', content, :class => "mypage-box", :id => "block-#{block}")
end
end

# Renders a single block content
def render_block_content(block, user)
unless block_definition = Redmine::MyPage.find_block(block)
Rails.logger.warn("Unknown block \"#{block}\" found in #{user.login} (id=#{user.id}) preferences")
return
end

settings = user.pref.my_page_settings(block)
if partial = block_definition[:partial]
begin
render(:partial => partial, :locals => {:user => user, :settings => settings, :block => block})
rescue ActionView::MissingTemplate
Rails.logger.warn("Partial \"#{partial}\" missing for block \"#{block}\" found in #{user.login} (id=#{user.id}) preferences")
return nil
end
else
send "render_#{block_definition[:name]}_block", block, settings
end
end

# Returns the select tag used to add a block to My page
def block_select_tag(user)
blocks_in_use = user.pref.my_page_layout.values.flatten
options = content_tag('option')
Redmine::MyPage.block_options(blocks_in_use).each do |label, block|
options << content_tag('option', label, :value => block, :disabled => block.blank?)
end
select_tag('block', options, :id => "block-select", :onchange => "$('#block-form').submit();")
end

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("(start_date>=? and start_date<=?) or (due_date>=? and due_date<=?)", startdt, enddt, startdt, enddt).
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).
to_a

render :partial => 'my/blocks/calendar', :locals => {:calendar => calendar, :block => block}
end

def render_documents_block(block, settings)
documents = Document.visible.order("#{Document.table_name}.created_on DESC").limit(10).to_a

render :partial => 'my/blocks/documents', :locals => {:block => block, :documents => documents}
end

def documents_items
Document.visible.order("#{Document.table_name}.created_on DESC").limit(10).to_a
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.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)

render :partial => 'my/blocks/issues', :locals => {:query => query, :issues => issues, :block => block}
end

def issuesassignedtome_items
Issue.visible.open.
where(:assigned_to_id => ([User.current.id] + User.current.group_ids)).
limit(10).
includes(:status, :project, :tracker, :priority).
references(:status, :project, :tracker, :priority).
order("#{IssuePriority.table_name}.position DESC, #{Issue.table_name}.updated_on DESC").
to_a
def render_issuesreportedbyme_block(block, settings)
query = IssueQuery.new(:name => l(:label_reported_issues), :user => User.current)
query.add_filter 'author_id', '=', ['me']
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 issuesreportedbyme_items
Issue.visible.
where(:author_id => User.current.id).
limit(10).
includes(:status, :project, :tracker).
references(:status, :project, :tracker).
order("#{Issue.table_name}.updated_on DESC").
to_a
def render_issueswatched_block(block, settings)
query = IssueQuery.new(:name => l(:label_watched_issues), :user => User.current)
query.add_filter 'watcher_id', '=', ['me']
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 issueswatched_items
Issue.visible.on_active_project.watched_by(User.current.id).recently_updated.limit(10).to_a
def render_issuequery_block(block, settings)
query = IssueQuery.visible.find_by_id(settings[:query_id])

if query
query.column_names = settings[:columns] if settings[:columns].present?
query.sort_criteria = settings[:sort] if settings[:sort].present?
issues = query.issues(:limit => 10)
render :partial => 'my/blocks/issues', :locals => {:query => query, :issues => issues, :block => block, :settings => settings}
else
queries = IssueQuery.visible.sorted
render :partial => 'my/blocks/issue_query_selection', :locals => {:queries => queries, :block => block, :settings => settings}
end
end

def news_items
News.visible.
def render_news_block(block, settings)
news = News.visible.
where(:project_id => User.current.projects.map(&:id)).
limit(10).
includes(:project, :author).
references(:project, :author).
order("#{News.table_name}.created_on DESC").
to_a

render :partial => 'my/blocks/news', :locals => {:block => block, :news => news}
end

def timelog_items
TimeEntry.
where("#{TimeEntry.table_name}.user_id = ? AND #{TimeEntry.table_name}.spent_on BETWEEN ? AND ?", User.current.id, Date.today - 6, Date.today).
def render_timelog_block(block, settings)
days = settings[:days].to_i
days = 7 if days < 1 || days > 365

entries = TimeEntry.
where("#{TimeEntry.table_name}.user_id = ? AND #{TimeEntry.table_name}.spent_on BETWEEN ? AND ?", User.current.id, User.current.today - (days - 1), User.current.today).
joins(:activity, :project).
references(:issue => [:tracker, :status]).
includes(:issue => [:tracker, :status]).
order("#{TimeEntry.table_name}.spent_on DESC, #{Project.table_name}.name ASC, #{Tracker.table_name}.position ASC, #{Issue.table_name}.id ASC").
to_a
entries_by_day = entries.group_by(&:spent_on)

render :partial => 'my/blocks/timelog', :locals => {:block => block, :entries => entries, :entries_by_day => entries_by_day, :days => days}
end
end
2 changes: 1 addition & 1 deletion app/helpers/news_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down
10 changes: 9 additions & 1 deletion app/helpers/principal_memberships_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down Expand Up @@ -46,6 +46,14 @@ def new_principal_membership_path(principal, *args)
end
end

def edit_principal_membership_path(principal, *args)
if principal.is_a?(Group)
edit_group_membership_path(principal, *args)
else
edit_user_membership_path(principal, *args)
end
end

def principal_membership_path(principal, membership, *args)
if principal.is_a?(Group)
group_membership_path(principal, membership, *args)
Expand Down
51 changes: 38 additions & 13 deletions app/helpers/projects_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand All @@ -22,7 +22,8 @@ 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},
{: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},
Expand All @@ -47,24 +48,17 @@ def parent_project_select_tag(project)
end

def render_project_action_links
links = []
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.allowed_to?(:view_issues, nil, :global => true)
links << link_to(l(:label_issue_view_all), issues_path)
end
if User.current.allowed_to?(:view_time_entries, nil, :global => true)
links << link_to(l(:label_overall_spent_time), time_entries_path)
end
links << link_to(l(:label_overall_activity), activity_path)
links.join(" | ").html_safe
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) ? 'my-project' : nil}")
s = link_to_project(project, {}, :class => "#{project.css_classes} #{User.current.member_of?(project) ? 'icon icon-fav my-project' : nil}")
if project.description.present?
s << content_tag('div', textilizable(project.short_description, :project => project), :class => 'wiki description')
end
Expand All @@ -87,11 +81,37 @@ def version_options_for_select(versions, selected=nil)
end
end

def project_default_version_options(project)
versions = project.shared_versions.open.to_a
if project.default_version && !versions.include?(project.default_version)
versions << project.default_version
end
version_options_for_select(versions, project.default_version)
end

def project_default_assigned_to_options(project)
assignable_users = (project.assignable_users.to_a + [project.default_assigned_to]).uniq.compact
principals_options_for_select(assignable_users, project.default_assigned_to)
end

def format_version_sharing(sharing)
sharing = 'none' unless Version::VERSION_SHARINGS.include?(sharing)
l("label_version_sharing_#{sharing}")
end

def render_boards_tree(boards, parent=nil, level=0, &block)
selection = boards.select {|b| b.parent == parent}
return '' if selection.empty?

s = ''.html_safe
selection.each do |board|
node = capture(board, level, &block)
node << render_boards_tree(boards, board, level+1, &block)
s << content_tag('div', node)
end
content_tag('div', s, :class => 'sort-level')
end

def render_api_includes(project, api)
api.array :trackers do
project.trackers.each do |tracker|
Expand All @@ -105,11 +125,16 @@ def render_api_includes(project, api)
end
end if include_in_api_response?('issue_categories')

api.array :time_entry_activities do
project.activities.each do |activity|
api.time_entry_activity(:id => activity.id, :name => activity.name)
end
end if include_in_api_response?('time_entry_activities')

api.array :enabled_modules do
project.enabled_modules.each do |enabled_module|
api.enabled_module(:id => enabled_module.id, :name => enabled_module.name)
end
end if include_in_api_response?('enabled_modules')

end
end
306 changes: 244 additions & 62 deletions app/helpers/queries_helper.rb

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion app/helpers/reports_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down
4 changes: 2 additions & 2 deletions app/helpers/repositories_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down Expand Up @@ -185,7 +185,7 @@ def git_field_tags(form, repository)
scm_path_info_tag(repository)) +
scm_path_encoding_tag(form, repository) +
content_tag('p', form.check_box(
:extra_report_last_commit,
:report_last_commit,
:label => l(:label_git_report_last_commit)
))
end
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/roles_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down
30 changes: 23 additions & 7 deletions app/helpers/routes_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down Expand Up @@ -29,6 +29,22 @@ def _project_issues_path(project, *args)
end
end

def _project_news_path(project, *args)
if project
project_news_index_path(project, *args)
else
news_index_path(*args)
end
end

def _new_project_issue_path(project, *args)
if project
new_project_issue_path(project, *args)
else
new_issue_path(*args)
end
end

def _project_calendar_path(project, *args)
project ? project_calendar_path(project, *args) : issues_calendar_path(*args)
end
Expand All @@ -38,19 +54,15 @@ def _project_gantt_path(project, *args)
end

def _time_entries_path(project, issue, *args)
if issue
issue_time_entries_path(issue, *args)
elsif project
if project
project_time_entries_path(project, *args)
else
time_entries_path(*args)
end
end

def _report_time_entries_path(project, issue, *args)
if issue
report_issue_time_entries_path(issue, *args)
elsif project
if project
report_project_time_entries_path(project, *args)
else
report_time_entries_path(*args)
Expand All @@ -66,4 +78,8 @@ def _new_time_entry_path(project, issue, *args)
new_time_entry_path(*args)
end
end

def board_path(board, *args)
project_board_path(board.project, board, *args)
end
end
2 changes: 1 addition & 1 deletion app/helpers/search_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down
55 changes: 47 additions & 8 deletions app/helpers/settings_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand All @@ -22,26 +22,46 @@ def administration_settings_tabs
tabs = [{:name => 'general', :partial => 'settings/general', :label => :label_general},
{:name => 'display', :partial => 'settings/display', :label => :label_display},
{: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 => '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},
{:name => 'notifications', :partial => 'settings/notifications', :label => :field_mail_notification},
{:name => 'mail_handler', :partial => 'settings/mail_handler', :label => :label_incoming_emails},
{:name => 'repositories', :partial => 'settings/repositories', :label => :label_repository_plural}
]
end

def render_settings_error(errors)
return if errors.blank?
s = ''.html_safe
errors.each do |name, message|
s << content_tag('li', content_tag('b', l("setting_#{name}")) + " " + message)
end
content_tag('div', content_tag('ul', s), :id => 'errorExplanation')
end

def setting_value(setting)
value = nil
if params[:settings]
value = params[:settings][setting]
end
value || Setting.send(setting)
end

def setting_select(setting, choices, options={})
if blank_text = options.delete(:blank)
choices = [[blank_text.is_a?(Symbol) ? l(blank_text) : blank_text, '']] + choices
end
setting_label(setting, options).html_safe +
select_tag("settings[#{setting}]",
options_for_select(choices, Setting.send(setting).to_s),
options_for_select(choices, setting_value(setting).to_s),
options).html_safe
end

def setting_multiselect(setting, choices, options={})
setting_values = Setting.send(setting)
setting_values = setting_value(setting)
setting_values = [] unless setting_values.is_a?(Array)

content_tag("label", l(options[:label] || "setting_#{setting}")) +
Expand All @@ -63,18 +83,18 @@ def setting_multiselect(setting, choices, options={})

def setting_text_field(setting, options={})
setting_label(setting, options).html_safe +
text_field_tag("settings[#{setting}]", Setting.send(setting), options).html_safe
text_field_tag("settings[#{setting}]", setting_value(setting), options).html_safe
end

def setting_text_area(setting, options={})
setting_label(setting, options).html_safe +
text_area_tag("settings[#{setting}]", Setting.send(setting), options).html_safe
text_area_tag("settings[#{setting}]", setting_value(setting), options).html_safe
end

def setting_check_box(setting, options={})
setting_label(setting, options).html_safe +
hidden_field_tag("settings[#{setting}]", 0, :id => nil).html_safe +
check_box_tag("settings[#{setting}]", 1, Setting.send("#{setting}?"), options).html_safe
check_box_tag("settings[#{setting}]", 1, setting_value(setting).to_s != '0', options).html_safe
end

def setting_label(setting, options={})
Expand All @@ -95,7 +115,7 @@ def notification_field(notifiable)

tag = check_box_tag('settings[notified_events][]',
notifiable.name,
Setting.notified_events.include?(notifiable.name),
setting_value('notified_events').include?(notifiable.name),
:id => nil,
:data => tag_data)

Expand All @@ -109,6 +129,25 @@ def notification_field(notifiable)
content_tag(:label, tag + text, options)
end

def session_lifetime_options
options = [[l(:label_disabled), 0]]
options += [4, 8, 12].map {|hours|
[l('datetime.distance_in_words.x_hours', :count => hours), (hours * 60).to_s]
}
options += [1, 7, 30, 60, 365].map {|days|
[l('datetime.distance_in_words.x_days', :count => days), (days * 24 * 60).to_s]
}
options
end

def session_timeout_options
options = [[l(:label_disabled), 0]]
options += [1, 2, 4, 8, 12, 24, 48].map {|hours|
[l('datetime.distance_in_words.x_hours', :count => hours), (hours * 60).to_s]
}
options
end

def link_copied_issue_options
options = [
[:general_text_Yes, 'yes'],
Expand Down Expand Up @@ -161,7 +200,7 @@ def parent_issue_done_ratio_options
# Returns the options for the date_format setting
def date_format_setting_options(locale)
Setting::DATE_FORMATS.map do |f|
today = ::I18n.l(Date.today, :locale => locale, :format => f)
today = ::I18n.l(User.current.today, :locale => locale, :format => f)
format = f.gsub('%', '').gsub(/[dmY]/) do
{'d' => 'dd', 'm' => 'mm', 'Y' => 'yyyy'}[$&]
end
Expand Down
106 changes: 4 additions & 102 deletions app/helpers/sort_helper.rb
Expand Up @@ -53,97 +53,6 @@
#

module SortHelper
class SortCriteria

def initialize
@criteria = []
end

def available_criteria=(criteria)
unless criteria.is_a?(Hash)
criteria = criteria.inject({}) {|h,k| h[k] = k; h}
end
@available_criteria = criteria
end

def from_param(param)
@criteria = param.to_s.split(',').collect {|s| s.split(':')[0..1]}
normalize!
end

def criteria=(arg)
@criteria = arg
normalize!
end

def to_param
@criteria.collect {|k,o| k + (o ? '' : ':desc')}.join(',')
end

# Returns an array of SQL fragments used to sort the list
def to_sql
sql = @criteria.collect do |k,o|
if s = @available_criteria[k]
s = [s] unless s.is_a?(Array)
s.collect {|c| append_order(c, o ? "ASC" : "DESC")}
end
end.flatten.compact
sql.blank? ? nil : sql
end

def to_a
@criteria.dup
end

def add!(key, asc)
@criteria.delete_if {|k,o| k == key}
@criteria = [[key, asc]] + @criteria
normalize!
end

def add(*args)
r = self.class.new.from_param(to_param)
r.add!(*args)
r
end

def first_key
@criteria.first && @criteria.first.first
end

def first_asc?
@criteria.first && @criteria.first.last
end

def empty?
@criteria.empty?
end

private

def normalize!
@criteria ||= []
@criteria = @criteria.collect {|s| s = Array(s); [s.first, (s.last == false || s.last == 'desc') ? false : true]}
@criteria = @criteria.select {|k,o| @available_criteria.has_key?(k)} if @available_criteria
@criteria.slice!(3)
self
end

# Appends ASC/DESC to the sort criterion unless it has a fixed order
def append_order(criterion, order)
if criterion =~ / (asc|desc)$/i
criterion
else
"#{criterion} #{order}"
end
end

# Appends DESC to the sort criterion unless it has a fixed order
def append_desc(criterion)
append_order(criterion, "DESC")
end
end

def sort_name
controller_name + '_' + action_name + '_sort'
end
Expand Down Expand Up @@ -173,10 +82,8 @@ def sort_init(*args)
#
def sort_update(criteria, sort_name=nil)
sort_name ||= self.sort_name
@sort_criteria = SortCriteria.new
@sort_criteria.available_criteria = criteria
@sort_criteria.from_param(params[:sort] || session[sort_name])
@sort_criteria.criteria = @sort_default if @sort_criteria.empty?
@sort_criteria = Redmine::SortCriteria.new(params[:sort] || session[sort_name] || @sort_default)
@sortable_columns = criteria
session[sort_name] = @sort_criteria.to_param
end

Expand All @@ -190,7 +97,7 @@ def sort_clear
# Use this to sort the controller's table items collection.
#
def sort_clause()
@sort_criteria.to_sql
@sort_criteria.sort_clause(@sortable_columns)
end

def sort_criteria
Expand Down Expand Up @@ -218,12 +125,7 @@ def sort_link(column, caption, default_order)
caption = column.to_s.humanize unless caption

sort_options = { :sort => @sort_criteria.add(column.to_s, order).to_param }
url_options = params.merge(sort_options)

# Add project_id to url_options
url_options = url_options.merge(:project_id => params[:project_id]) if params.has_key?(:project_id)

link_to_content_update(h(caption), url_options, :class => css)
link_to(caption, {:params => request.query_parameters.merge(sort_options)}, :class => css)
end

# Returns a table header <th> tag with a sort link for the named column
Expand Down
16 changes: 1 addition & 15 deletions app/helpers/timelog_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand All @@ -20,20 +20,6 @@
module TimelogHelper
include ApplicationHelper

def render_timelog_breadcrumb
links = []
links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
if @issue
if @issue.visible?
links << link_to_issue(@issue, :subject => false)
else
links << "##{@issue.id}"
end
end
breadcrumb links
end

# Returns a collection of activities for a select field. time_entry
# is optional and will be used to check if the selected TimeEntryActivity
# is active.
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/trackers_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down
6 changes: 5 additions & 1 deletion app/helpers/users_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down Expand Up @@ -30,6 +30,10 @@ def user_mail_notification_options(user)
user.valid_notification_options.collect {|o| [l(o.last), o.first]}
end

def textarea_font_options
[[l(:label_font_default), '']] + UserPreference::TEXTAREA_FONT_OPTIONS.map {|o| [l("label_font_#{o}"), o]}
end

def change_status_link(user)
url = {:controller => 'users', :action => 'update', :id => user, :page => params[:page], :status => params[:status], :tab => nil}

Expand Down
2 changes: 1 addition & 1 deletion app/helpers/versions_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down
10 changes: 6 additions & 4 deletions app/helpers/watchers_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down Expand Up @@ -47,7 +47,7 @@ def watcher_css(objects)
def watchers_list(object)
remove_allowed = User.current.allowed_to?("delete_#{object.class.name.underscore}_watchers".to_sym, object.project)
content = ''.html_safe
lis = object.watcher_users.collect do |user|
lis = object.watcher_users.preload(:email_address).collect do |user|
s = ''.html_safe
s << avatar(user, :size => "16").to_s
s << link_to_user(user, :class => 'user')
Expand All @@ -58,8 +58,10 @@ def watchers_list(object)
:object_id => object.id,
:user_id => user}
s << ' '
s << link_to(image_tag('delete.png'), url,
:remote => true, :method => 'delete', :class => "delete")
s << link_to(l(:button_delete), url,
:remote => true, :method => 'delete',
:class => "delete icon-only icon-del",
:title => l(:button_delete))
end
content << content_tag('li', s, :class => "user-#{user.id}")
end
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/welcome_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down
15 changes: 14 additions & 1 deletion app/helpers/wiki_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down Expand Up @@ -51,4 +51,17 @@ def wiki_page_breadcrumb(page)
link_to(h(parent.pretty_title), {:controller => 'wiki', :action => 'show', :id => parent.title, :project_id => parent.project, :version => nil})
})
end

# Returns the path for the Cancel link when editing a wiki page
def wiki_page_edit_cancel_path(page)
if page.new_record?
if parent = page.parent
project_wiki_page_path(parent.project, parent.title)
else
project_wiki_index_path(page.project)
end
else
project_wiki_page_path(page.project, page.title)
end
end
end
8 changes: 4 additions & 4 deletions app/helpers/workflows_helper.rb
@@ -1,7 +1,7 @@
# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2015 Jean-Philippe Lang
# 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
Expand Down Expand Up @@ -75,14 +75,14 @@ def field_permission_tag(permissions, status, field, roles)
end

def transition_tag(workflows, old_status, new_status, name)
w = workflows.select {|w| w.old_status_id == old_status.id && w.new_status_id == new_status.id}.size
w = workflows.select {|w| w.old_status == old_status && w.new_status == new_status}.size

tag_name = "transitions[#{ old_status.id }][#{new_status.id}][#{name}]"
tag_name = "transitions[#{ old_status.try(:id) || 0 }][#{new_status.id}][#{name}]"
if 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.id} 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([
Expand Down