Compare commits

..

9 Commits
1.1.2 ... 1.0.0

Author SHA1 Message Date
Eric Davis
cac7b0423f Tagging 1.0.0 (RC)
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/tags/1.0.0@3858 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-07-18 18:19:20 +00:00
Eric Davis
4b52b7a8df Change version to the show this is the stable branch.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/1.0-stable@3857 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-07-18 16:39:00 +00:00
Eric Davis
6ed2e003a1 Merged r3854 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/1.0-stable@3856 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-07-18 16:38:56 +00:00
Eric Davis
1cc116fe5c Merged r3853 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/1.0-stable@3855 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-07-18 16:38:51 +00:00
Eric Davis
619c148719 Merged r3849 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/1.0-stable@3852 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-07-18 15:53:56 +00:00
Eric Davis
0ae9cc7d07 Merged r3848 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/1.0-stable@3851 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-07-18 15:53:51 +00:00
Eric Davis
a2a3dae974 Merged r3847 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/1.0-stable@3850 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-07-18 15:53:46 +00:00
Eric Davis
4f5dc94170 Merged r3845 from trunk.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/1.0-stable@3846 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-07-16 03:47:03 +00:00
Eric Davis
4775864d69 Adding 1.0 stable branch to prepare for 1.0.0 RC
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/1.0-stable@3844 e93f8b46-1217-0410-a6f0-8f06a7374b81
2010-07-16 03:31:31 +00:00
619 changed files with 15183 additions and 35215 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,3 @@
/.project
/.loadpath
/config/additional_environment.rb
/config/database.yml
/config/email.yml

View File

@@ -1,24 +0,0 @@
syntax: glob
.project
.loadpath
config/additional_environment.rb
config/database.yml
config/email.yml
config/initializers/session_store.rb
coverage
db/*.db
db/*.sqlite3
db/schema.rb
files/*
log/*.log*
log/mongrel_debug
public/dispatch.*
public/plugin_assets
tmp/*
tmp/cache/*
tmp/sessions/*
tmp/sockets/*
tmp/test/*
vendor/rails
*.rbc

View File

@@ -83,9 +83,9 @@ class AccountController < ApplicationController
else
@user = User.new(params[:user])
@user.admin = false
@user.register
@user.status = User::STATUS_REGISTERED
if session[:auth_source_registration]
@user.activate
@user.status = User::STATUS_ACTIVE
@user.login = session[:auth_source_registration][:login]
@user.auth_source_id = session[:auth_source_registration][:auth_source_id]
if @user.save
@@ -116,8 +116,8 @@ class AccountController < ApplicationController
token = Token.find_by_action_and_value('register', params[:token])
redirect_to(home_url) && return unless token and !token.expired?
user = token.user
redirect_to(home_url) && return unless user.registered?
user.activate
redirect_to(home_url) && return unless user.status == User::STATUS_REGISTERED
user.status = User::STATUS_ACTIVE
if user.save
token.destroy
flash[:notice] = l(:notice_account_activated)
@@ -170,7 +170,7 @@ class AccountController < ApplicationController
user.mail = registration['email'] unless registration['email'].nil?
user.firstname, user.lastname = registration['fullname'].split(' ') unless registration['fullname'].nil?
user.random_password
user.register
user.status = User::STATUS_REGISTERED
case Setting.self_registration
when '1'
@@ -241,7 +241,7 @@ class AccountController < ApplicationController
# Pass a block for behavior when a user fails to save
def register_automatically(user, &block)
# Automatic activation
user.activate
user.status = User::STATUS_ACTIVE
user.last_login_on = Time.now
if user.save
self.logged_user = user

View File

@@ -1,59 +0,0 @@
class ActivitiesController < ApplicationController
menu_item :activity
before_filter :find_optional_project
accept_key_auth :index
def index
@days = Setting.activity_days_default.to_i
if params[:from]
begin; @date_to = params[:from].to_date + 1; rescue; end
end
@date_to ||= Date.today + 1
@date_from = @date_to - @days
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
@author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
@activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
:with_subprojects => @with_subprojects,
:author => @author)
@activity.scope_select {|t| !params["show_#{t}"].nil?}
@activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
events = @activity.events(@date_from, @date_to)
if events.empty? || stale?(:etag => [@activity.scope, @date_to, @date_from, @with_subprojects, @author, events.first, User.current, current_language])
respond_to do |format|
format.html {
@events_by_day = events.group_by(&:event_date)
render :layout => false if request.xhr?
}
format.atom {
title = l(:label_activity)
if @author
title = @author.name
elsif @activity.scope.size == 1
title = l("label_#{@activity.scope.first.singularize}_plural")
end
render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
}
end
end
rescue ActiveRecord::RecordNotFound
render_404
end
private
# TODO: refactor, duplicated in projects_controller
def find_optional_project
return true unless params[:id]
@project = Project.find(params[:id])
authorize
rescue ActiveRecord::RecordNotFound
render_404
end
end

View File

@@ -22,7 +22,7 @@ class ApplicationController < ActionController::Base
include Redmine::I18n
layout 'base'
exempt_from_layout 'builder', 'rsb'
exempt_from_layout 'builder'
# Remove broken cookie after upgrade from 0.8.x (#4292)
# See https://rails.lighthouseapp.com/projects/8994/tickets/3360
@@ -71,10 +71,10 @@ class ApplicationController < ActionController::Base
elsif params[:format] == 'atom' && params[:key] && accept_key_auth_actions.include?(params[:action])
# RSS key authentication does not start a session
User.find_by_rss_key(params[:key])
elsif Setting.rest_api_enabled? && api_request?
if (key = api_key_from_request) && accept_key_auth_actions.include?(params[:action])
elsif Setting.rest_api_enabled? && ['xml', 'json'].include?(params[:format])
if params[:key].present? && accept_key_auth_actions.include?(params[:action])
# Use API key
User.find_by_api_key(key)
User.find_by_api_key(params[:key])
else
# HTTP Basic, either username/password or API key/random
authenticate_with_http_basic do |username, password|
@@ -153,16 +153,8 @@ class ApplicationController < ActionController::Base
# Authorize the user for the requested action
def authorize(ctrl = params[:controller], action = params[:action], global = false)
allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
if allowed
true
else
if @project && @project.archived?
render_403 :message => :notice_not_authorized_archived_project
else
deny_access
end
end
allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project, :global => global)
allowed ? true : deny_access
end
# Authorize the user for the requested action outside a project
@@ -177,13 +169,6 @@ class ApplicationController < ActionController::Base
render_404
end
# Find project of id params[:project_id]
def find_project_by_project_id
@project = Project.find(params[:project_id])
rescue ActiveRecord::RecordNotFound
render_404
end
# Find a project based on params[:project_id]
# TODO: some subclasses override this, see about merging their logic
def find_optional_project
@@ -216,26 +201,7 @@ class ApplicationController < ActionController::Base
def self.model_object(model)
write_inheritable_attribute('model_object', model)
end
# Filter for bulk issue operations
def find_issues
@issues = Issue.find_all_by_id(params[:id] || params[:ids])
raise ActiveRecord::RecordNotFound if @issues.empty?
@projects = @issues.collect(&:project).compact.uniq
@project = @projects.first if @projects.size == 1
rescue ActiveRecord::RecordNotFound
render_404
end
# Check if project is unique before bulk operations
def check_project_uniqueness
unless @project
# TODO: let users bulk edit/move/destroy issues from different projects
render_error 'Can not bulk edit/move/destroy issues from different projects'
return false
end
end
# make sure that the user is a member of the project (or admin) if project is private
# used as a before_filter for actions that do not require any particular permission on the project
def check_project_privacy
@@ -252,10 +218,6 @@ class ApplicationController < ActionController::Base
end
end
def back_url
params[:back_url] || request.env['HTTP_REFERER']
end
def redirect_back_or_default(default)
back_url = CGI.unescape(params[:back_url].to_s)
if !back_url.blank?
@@ -273,42 +235,41 @@ class ApplicationController < ActionController::Base
redirect_to default
end
def render_403(options={})
def render_403
@project = nil
render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
respond_to do |format|
format.html { render :template => "common/403", :layout => (request.xhr? ? false : 'base'), :status => 403 }
format.atom { head 403 }
format.xml { head 403 }
format.js { head 403 }
format.json { head 403 }
end
return false
end
def render_404(options={})
render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
def render_404
respond_to do |format|
format.html { render :template => "common/404", :layout => !request.xhr?, :status => 404 }
format.atom { head 404 }
format.xml { head 404 }
format.js { head 404 }
format.json { head 404 }
end
return false
end
# Renders an error response
def render_error(arg)
arg = {:message => arg} unless arg.is_a?(Hash)
@message = arg[:message]
@message = l(@message) if @message.is_a?(Symbol)
@status = arg[:status] || 500
def render_error(msg)
respond_to do |format|
format.html {
render :template => 'common/error', :layout => use_layout, :status => @status
format.html {
flash.now[:error] = msg
render :text => '', :layout => !request.xhr?, :status => 500
}
format.atom { head @status }
format.xml { head @status }
format.js { head @status }
format.json { head @status }
format.atom { head 500 }
format.xml { head 500 }
format.js { head 500 }
format.json { head 500 }
end
end
# Picks which layout to use based on the request
#
# @return [boolean, string] name of the layout to use or false for no layout
def use_layout
request.xhr? ? false : 'base'
end
def invalid_authenticity_token
if api_request?
@@ -349,30 +310,6 @@ class ApplicationController < ActionController::Base
per_page
end
# Returns offset and limit used to retrieve objects
# for an API response based on offset, limit and page parameters
def api_offset_and_limit(options=params)
if options[:offset].present?
offset = options[:offset].to_i
if offset < 0
offset = 0
end
end
limit = options[:limit].to_i
if limit < 1
limit = 25
elsif limit > 100
limit = 100
end
if offset.nil? && options[:page].present?
offset = (options[:page].to_i - 1) * limit
offset = 0 if offset < 0
end
offset ||= 0
[offset, limit]
end
# qvalues http header parser
# code taken from webrick
def parse_qvalues(value)
@@ -402,36 +339,12 @@ class ApplicationController < ActionController::Base
def api_request?
%w(xml json).include? params[:format]
end
# Returns the API key present in the request
def api_key_from_request
if params[:key].present?
params[:key]
elsif request.headers["X-Redmine-API-Key"].present?
request.headers["X-Redmine-API-Key"]
end
end
# Renders a warning flash if obj has unsaved attachments
def render_attachment_warning_if_needed(obj)
flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
end
# Sets the `flash` notice or error based the number of issues that did not save
#
# @param [Array, Issue] issues all of the saved and unsaved Issues
# @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
if unsaved_issue_ids.empty?
flash[:notice] = l(:notice_successful_update) unless issues.empty?
else
flash[:error] = l(:notice_failed_to_save_issues,
:count => unsaved_issue_ids.size,
:total => issues.size,
:ids => '#' + unsaved_issue_ids.join(', #'))
end
end
# Rescues an invalid query statement. Just in case...
def query_statement_invalid(exception)
logger.error "Query::StatementInvalid: #{exception.message}" if logger
@@ -446,37 +359,5 @@ class ApplicationController < ActionController::Base
{ attribute => error }
end.to_json
end
# Renders API response on validation failure
def render_validation_errors(object)
options = { :status => :unprocessable_entity, :layout => false }
options.merge!(case params[:format]
when 'xml'; { :xml => object.errors }
when 'json'; { :json => {'errors' => object.errors} } # ActiveResource client compliance
else
raise "Unknown format #{params[:format]} in #render_validation_errors"
end
)
render options
end
# Overrides #default_template so that the api template
# is used automatically if it exists
def default_template(action_name = self.action_name)
if api_request?
begin
return self.view_paths.find_template(default_template_name(action_name), 'api')
rescue ::ActionView::MissingTemplate
# the api template was not found
# fallback to the default behaviour
end
end
super
end
# Overrides #pick_layout so that #render with no arguments
# doesn't use the layout for api requests
def pick_layout(*args)
api_request? ? nil : super
end
end

View File

@@ -1,27 +0,0 @@
class AutoCompletesController < ApplicationController
before_filter :find_project
def issues
@issues = []
q = params[:q].to_s
query = (params[:scope] == "all" && Setting.cross_project_issue_relations?) ? Issue : @project.issues
if q.match(/^\d+$/)
@issues << query.visible.find_by_id(q.to_i)
end
unless q.blank?
@issues += query.visible.find(:all, :conditions => ["LOWER(#{Issue.table_name}.subject) LIKE ?", "%#{q.downcase}%"], :limit => 10)
end
@issues.compact!
render :layout => false
end
private
def find_project
project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
@project = Project.find(project_id)
rescue ActiveRecord::RecordNotFound
render_404
end
end

View File

@@ -18,7 +18,6 @@
class BoardsController < ApplicationController
default_search_scope :messages
before_filter :find_project, :find_board_if_available, :authorize
accept_key_auth :index, :show
helper :messages
include MessagesHelper

View File

@@ -1,5 +1,4 @@
class CalendarsController < ApplicationController
menu_item :calendar
before_filter :find_optional_project
rescue_from Query::StatementInvalid, :with => :query_statement_invalid
@@ -8,8 +7,6 @@ class CalendarsController < ApplicationController
helper :projects
helper :queries
include QueriesHelper
helper :sort
include SortHelper
def show
if params[:year] and params[:year].to_i > 1900
@@ -34,11 +31,8 @@ class CalendarsController < ApplicationController
@calendar.events = events
end
render :action => 'show', :layout => false if request.xhr?
render :layout => false if request.xhr?
end
def update
show
end
end

View File

@@ -1,36 +0,0 @@
class CommentsController < ApplicationController
default_search_scope :news
model_object News
before_filter :find_model_object
before_filter :find_project_from_association
before_filter :authorize
verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
def create
@comment = Comment.new(params[:comment])
@comment.author = User.current
if @news.comments << @comment
flash[:notice] = l(:label_comment_added)
end
redirect_to :controller => 'news', :action => 'show', :id => @news
end
verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed }
def destroy
@news.comments.find(params[:comment_id]).destroy
redirect_to :controller => 'news', :action => 'show', :id => @news
end
private
# ApplicationController's find_model_object sets it based on the controller
# name so it needs to be overriden and set to @news instead
def find_model_object
super
@news = @object
@comment = nil
@news
end
end

View File

@@ -1,44 +0,0 @@
class ContextMenusController < ApplicationController
helper :watchers
def issues
@issues = Issue.visible.all(:conditions => {:id => params[:ids]}, :include => :project)
if (@issues.size == 1)
@issue = @issues.first
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
else
@allowed_statuses = @issues.map do |i|
i.new_statuses_allowed_to(User.current)
end.inject do |memo,s|
memo & s
end
end
@projects = @issues.collect(&:project).compact.uniq
@project = @projects.first if @projects.size == 1
@can = {:edit => User.current.allowed_to?(:edit_issues, @projects),
:log_time => (@project && User.current.allowed_to?(:log_time, @project)),
:update => (User.current.allowed_to?(:edit_issues, @projects) || (User.current.allowed_to?(:change_status, @projects) && !@allowed_statuses.blank?)),
:move => (@project && User.current.allowed_to?(:move_issues, @project)),
:copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
:delete => User.current.allowed_to?(:delete_issues, @projects)
}
if @project
@assignables = @project.assignable_users
@assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
@trackers = @project.trackers
else
#when multiple projects, we only keep the intersection of each set
@assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a}
@trackers = @projects.map(&:trackers).inject{|memo,t| memo & t}
end
@priorities = IssuePriority.all.reverse
@statuses = IssueStatus.find(:all, :order => 'position')
@back = back_url
render :layout => false
end
end

View File

@@ -1,36 +0,0 @@
class FilesController < ApplicationController
menu_item :files
before_filter :find_project_by_project_id
before_filter :authorize
helper :sort
include SortHelper
def index
sort_init 'filename', 'asc'
sort_update 'filename' => "#{Attachment.table_name}.filename",
'created_on' => "#{Attachment.table_name}.created_on",
'size' => "#{Attachment.table_name}.filesize",
'downloads' => "#{Attachment.table_name}.downloads"
@containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
@containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
render :layout => !request.xhr?
end
def new
@versions = @project.versions.sort
end
def create
container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
attachments = Attachment.attach_files(container, params[:attachments])
render_attachment_warning_if_needed(container)
if !attachments.empty? && !attachments[:files].blank? && Setting.notified_events.include?('file_added')
Mailer.deliver_attachments_added(attachments[:files])
end
redirect_to project_files_path(@project)
end
end

View File

@@ -1,10 +1,8 @@
class GanttsController < ApplicationController
menu_item :gantt
before_filter :find_optional_project
rescue_from Query::StatementInvalid, :with => :query_statement_invalid
helper :gantt
helper :issues
helper :projects
helper :queries
@@ -15,22 +13,33 @@ class GanttsController < ApplicationController
def show
@gantt = Redmine::Helpers::Gantt.new(params)
@gantt.project = @project
retrieve_query
@query.group_by = nil
@gantt.query = @query if @query.valid?
if @query.valid?
events = []
# Issues that have start and due dates
events += @query.issues(:include => [:tracker, :assigned_to, :priority],
:order => "start_date, due_date",
:conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
)
# Issues that don't have a due date but that are assigned to a version with a date
events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
:order => "start_date, effective_date",
:conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
)
# Versions
events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
@gantt.events = events
end
basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
respond_to do |format|
format.html { render :action => "show", :layout => !request.xhr? }
format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
format.pdf { send_data(@gantt.to_pdf, :type => 'application/pdf', :filename => "#{basename}.pdf") }
format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
end
end
def update
show
end
end

View File

@@ -141,22 +141,14 @@ class GroupsController < ApplicationController
@membership = Member.edit_membership(params[:membership_id], params[:membership], @group)
@membership.save if request.post?
respond_to do |format|
if @membership.valid?
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
format.js {
render(:update) {|page|
page.replace_html "tab-content-memberships", :partial => 'groups/memberships'
page.visual_effect(:highlight, "member-#{@membership.id}")
}
}
else
format.js {
render(:update) {|page|
page.alert(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', ')))
}
}
end
end
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
format.js {
render(:update) {|page|
page.replace_html "tab-content-memberships", :partial => 'groups/memberships'
page.visual_effect(:highlight, "member-#{@membership.id}")
}
}
end
end
def destroy_membership

View File

@@ -1,68 +0,0 @@
class IssueMovesController < ApplicationController
default_search_scope :issues
before_filter :find_issues, :check_project_uniqueness
before_filter :authorize
def new
prepare_for_issue_move
render :layout => false if request.xhr?
end
def create
prepare_for_issue_move
if request.post?
new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
unsaved_issue_ids = []
moved_issues = []
@issues.each do |issue|
issue.reload
issue.init_journal(User.current)
issue.current_journal.notes = @notes if @notes.present?
call_hook(:controller_issues_move_before_save, { :params => params, :issue => issue, :target_project => @target_project, :copy => !!@copy })
if r = issue.move_to_project(@target_project, new_tracker, {:copy => @copy, :attributes => extract_changed_attributes_for_move(params)})
moved_issues << r
else
unsaved_issue_ids << issue.id
end
end
set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
if params[:follow]
if @issues.size == 1 && moved_issues.size == 1
redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
else
redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
end
else
redirect_to :controller => 'issues', :action => 'index', :project_id => @project
end
return
end
end
private
def prepare_for_issue_move
@issues.sort!
@copy = params[:copy_options] && params[:copy_options][:copy]
@allowed_projects = Issue.allowed_target_projects_on_move
@target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
@target_project ||= @project
@trackers = @target_project.trackers
@available_statuses = Workflow.available_statuses(@project)
@notes = params[:notes]
@notes ||= ''
end
def extract_changed_attributes_for_move(params)
changed_attributes = {}
[:assigned_to_id, :status_id, :start_date, :due_date, :priority_id].each do |valid_attribute|
unless params[valid_attribute].blank?
changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
end
end
changed_attributes
end
end

View File

@@ -28,7 +28,6 @@ class IssueRelationsController < ApplicationController
respond_to do |format|
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
format.js do
@relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
render :update do |page|
page.replace_html "relations", :partial => 'issues/relations'
if @relation.errors.empty?
@@ -48,10 +47,7 @@ class IssueRelationsController < ApplicationController
end
respond_to do |format|
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
format.js {
@relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
render(:update) {|page| page.replace_html "relations", :partial => 'issues/relations'}
}
format.js { render(:update) {|page| page.replace_html "relations", :partial => 'issues/relations'} }
end
end

View File

@@ -19,15 +19,14 @@ class IssuesController < ApplicationController
menu_item :new_issue, :only => [:new, :create]
default_search_scope :issues
before_filter :find_issue, :only => [:show, :edit, :update]
before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy]
before_filter :check_project_uniqueness, :only => [:move, :perform_move]
before_filter :find_project, :only => [:new, :create]
before_filter :authorize, :except => [:index]
before_filter :find_optional_project, :only => [:index]
before_filter :find_issue, :only => [:show, :edit, :update, :reply]
before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
before_filter :find_project, :only => [:new, :create, :update_form, :preview, :auto_complete]
before_filter :authorize, :except => [:index, :changes, :preview, :context_menu]
before_filter :find_optional_project, :only => [:index, :changes]
before_filter :check_for_default_issue_status, :only => [:new, :create]
before_filter :build_new_issue_from_params, :only => [:new, :create]
accept_key_auth :index, :show, :create, :update, :destroy
accept_key_auth :index, :show, :changes
rescue_from Query::StatementInvalid, :with => :query_statement_invalid
@@ -44,13 +43,10 @@ class IssuesController < ApplicationController
include AttachmentsHelper
helper :queries
include QueriesHelper
helper :repositories
include RepositoriesHelper
helper :sort
include SortHelper
include IssuesHelper
helper :timelog
helper :gantt
include Redmine::Export::PDF
verify :method => [:post, :delete],
@@ -58,7 +54,6 @@ class IssuesController < ApplicationController
:render => { :nothing => true, :status => :method_not_allowed }
verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
def index
@@ -67,29 +62,27 @@ class IssuesController < ApplicationController
sort_update(@query.sortable_columns)
if @query.valid?
case params[:format]
limit = case params[:format]
when 'csv', 'pdf'
@limit = Setting.issues_export_limit.to_i
Setting.issues_export_limit.to_i
when 'atom'
@limit = Setting.feeds_limit.to_i
when 'xml', 'json'
@offset, @limit = api_offset_and_limit
Setting.feeds_limit.to_i
else
@limit = per_page_option
per_page_option
end
@issue_count = @query.issue_count
@issue_pages = Paginator.new self, @issue_count, @limit, params['page']
@offset ||= @issue_pages.current.offset
@issue_pages = Paginator.new self, @issue_count, limit, params['page']
@issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
:order => sort_clause,
:offset => @offset,
:limit => @limit)
:offset => @issue_pages.current.offset,
:limit => limit)
@issue_count_by_group = @query.issue_count_by_group
respond_to do |format|
format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
format.api
format.xml { render :layout => false }
format.json { render :text => @issues.to_json, :layout => false }
format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
@@ -102,21 +95,36 @@ class IssuesController < ApplicationController
render_404
end
def changes
retrieve_query
sort_init 'id', 'desc'
sort_update(@query.sortable_columns)
if @query.valid?
@journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
:limit => 25)
end
@title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
render :layout => false, :content_type => 'application/atom+xml'
rescue ActiveRecord::RecordNotFound
render_404
end
def show
@journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
@journals.each_with_index {|j,i| j.indice = i+1}
@journals.reverse! if User.current.wants_comments_in_reverse_order?
@changesets = @issue.changesets.visible.all
@changesets.reverse! if User.current.wants_comments_in_reverse_order?
@relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
@priorities = IssuePriority.all
@time_entry = TimeEntry.new
respond_to do |format|
format.html { render :template => 'issues/show.rhtml' }
format.api
format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
format.xml { render :layout => false }
format.json { render :text => @issue.to_json, :layout => false }
format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
end
end
@@ -124,10 +132,7 @@ class IssuesController < ApplicationController
# Add a new issue
# The new issue will be created from an existing one if copy_from parameter is given
def new
respond_to do |format|
format.html { render :action => 'new', :layout => !request.xhr? }
format.js { render :partial => 'attributes' }
end
render :action => 'new', :layout => !request.xhr?
end
def create
@@ -139,20 +144,26 @@ class IssuesController < ApplicationController
call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
respond_to do |format|
format.html {
redirect_to(params[:continue] ? { :action => 'new', :project_id => @project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
redirect_to(params[:continue] ? { :action => 'new', :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
{ :action => 'show', :id => @issue })
}
format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
format.json { render :text => @issue.to_json, :status => :created, :location => url_for(:controller => 'issues', :action => 'show'), :layout => false }
end
return
else
respond_to do |format|
format.html { render :action => 'new' }
format.api { render_validation_errors(@issue) }
format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return }
format.json { render :text => object_errors_to_json(@issue), :status => :unprocessable_entity, :layout => false }
end
end
end
# Attributes that can be updated on workflow transition (without :edit permission)
# TODO: make it configurable (at least per role)
UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
def edit
update_issue_from_params
@@ -173,7 +184,8 @@ class IssuesController < ApplicationController
respond_to do |format|
format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
format.api { head :ok }
format.xml { head :ok }
format.json { head :ok }
end
else
render_attachment_warning_if_needed(@issue)
@@ -182,37 +194,104 @@ class IssuesController < ApplicationController
respond_to do |format|
format.html { render :action => 'edit' }
format.api { render_validation_errors(@issue) }
format.xml { render :xml => @issue.errors, :status => :unprocessable_entity }
format.json { render :text => object_errors_to_json(@issue), :status => :unprocessable_entity, :layout => false }
end
end
end
def reply
journal = Journal.find(params[:journal_id]) if params[:journal_id]
if journal
user = journal.user
text = journal.notes
else
user = @issue.author
text = @issue.description
end
# Replaces pre blocks with [...]
text = text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]')
content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> "
content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
render(:update) { |page|
page.<< "$('notes').value = \"#{escape_javascript content}\";"
page.show 'update'
page << "Form.Element.focus('notes');"
page << "Element.scrollTo('update');"
page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
}
end
# Bulk edit a set of issues
def bulk_edit
@issues.sort!
@available_statuses = @projects.map{|p|Workflow.available_statuses(p)}.inject{|memo,w|memo & w}
@custom_fields = @projects.map{|p|p.all_issue_custom_fields}.inject{|memo,c|memo & c}
@assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a}
@trackers = @projects.map(&:trackers).inject{|memo,t| memo & t}
if request.post?
attributes = (params[:issue] || {}).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]
unsaved_issue_ids = []
@issues.each do |issue|
issue.reload
journal = issue.init_journal(User.current, params[:notes])
issue.safe_attributes = attributes
call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
unless issue.save
# Keep unsaved issue ids to display them in flash error
unsaved_issue_ids << issue.id
end
end
set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
return
end
@available_statuses = Workflow.available_statuses(@project)
@custom_fields = @project.all_issue_custom_fields
end
def bulk_update
def move
@issues.sort!
attributes = parse_params_for_bulk_issue_attributes(params)
unsaved_issue_ids = []
@issues.each do |issue|
issue.reload
journal = issue.init_journal(User.current, params[:notes])
issue.safe_attributes = attributes
call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
unless issue.save
# Keep unsaved issue ids to display them in flash error
unsaved_issue_ids << issue.id
@copy = params[:copy_options] && params[:copy_options][:copy]
@allowed_projects = Issue.allowed_target_projects_on_move
@target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
@target_project ||= @project
@trackers = @target_project.trackers
@available_statuses = Workflow.available_statuses(@project)
if request.post?
new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
unsaved_issue_ids = []
moved_issues = []
@issues.each do |issue|
issue.reload
changed_attributes = {}
[:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
unless params[valid_attribute].blank?
changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
end
end
issue.init_journal(User.current)
call_hook(:controller_issues_move_before_save, { :params => params, :issue => issue, :target_project => @target_project, :copy => !!@copy })
if r = issue.move_to_project(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
moved_issues << r
else
unsaved_issue_ids << issue.id
end
end
set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
if params[:follow]
if @issues.size == 1 && moved_issues.size == 1
redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
else
redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
end
else
redirect_to :controller => 'issues', :action => 'index', :project_id => @project
end
return
end
set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
render :layout => false if request.xhr?
end
def destroy
@@ -232,17 +311,90 @@ class IssuesController < ApplicationController
TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
end
else
# display the destroy form if it's a user request
return unless api_request?
unless params[:format] == 'xml' || params[:format] == 'json'
# display the destroy form if it's a user request
return
end
end
end
@issues.each(&:destroy)
respond_to do |format|
format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
format.api { head :ok }
format.html { redirect_to :action => 'index', :project_id => @project }
format.xml { head :ok }
format.json { head :ok }
end
end
def context_menu
@issues = Issue.find_all_by_id(params[:ids], :include => :project)
if (@issues.size == 1)
@issue = @issues.first
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
end
projects = @issues.collect(&:project).compact.uniq
@project = projects.first if projects.size == 1
@can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
:log_time => (@project && User.current.allowed_to?(:log_time, @project)),
:update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
:move => (@project && User.current.allowed_to?(:move_issues, @project)),
:copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
:delete => (@project && User.current.allowed_to?(:delete_issues, @project))
}
if @project
@assignables = @project.assignable_users
@assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
@trackers = @project.trackers
end
@priorities = IssuePriority.all.reverse
@statuses = IssueStatus.find(:all, :order => 'position')
@back = params[:back_url] || request.env['HTTP_REFERER']
render :layout => false
end
def update_form
if params[:id].blank?
@issue = Issue.new
@issue.project = @project
else
@issue = @project.issues.visible.find(params[:id])
end
@issue.attributes = params[:issue]
@allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
@priorities = IssuePriority.all
render :partial => 'attributes'
end
def preview
@issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
if @issue
@attachements = @issue.attachments
@description = params[:issue] && params[:issue][:description]
if @description && @description.gsub(/(\r?\n|\n\r?)/, "\n") == @issue.description.to_s.gsub(/(\r?\n|\n\r?)/, "\n")
@description = nil
end
@notes = params[:notes]
else
@description = (params[:issue] ? params[:issue][:description] : nil)
end
render :layout => false
end
def auto_complete
@issues = []
q = params[:q].to_s
if q.match(/^\d+$/)
@issues << @project.issues.visible.find_by_id(q.to_i)
end
unless q.blank?
@issues += @project.issues.visible.find(:all, :conditions => ["LOWER(#{Issue.table_name}.subject) LIKE ?", "%#{q.downcase}%"], :limit => 10)
end
render :layout => false
end
private
def find_issue
@issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
@@ -251,6 +403,22 @@ private
render_404
end
# Filter for bulk operations
def find_issues
@issues = Issue.find_all_by_id(params[:id] || params[:ids])
raise ActiveRecord::RecordNotFound if @issues.empty?
projects = @issues.collect(&:project).compact.uniq
if projects.size == 1
@project = projects.first
else
# TODO: let users bulk edit/move/destroy issues from different projects
render_error 'Can not bulk edit/move/destroy issues from different projects'
return false
end
rescue ActiveRecord::RecordNotFound
render_404
end
def find_project
project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
@project = Project.find(project_id)
@@ -266,24 +434,23 @@ private
@priorities = IssuePriority.all
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
@time_entry = TimeEntry.new
@time_entry.attributes = params[:time_entry]
@notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
@notes = params[:notes]
@issue.init_journal(User.current, @notes)
@issue.safe_attributes = params[:issue]
# User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
attrs = params[:issue].dup
attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
@issue.safe_attributes = attrs
end
end
# TODO: Refactor, lots of extra code in here
# TODO: Changing tracker on an existing issue should not trigger this
def build_new_issue_from_params
if params[:id].blank?
@issue = Issue.new
@issue.copy_from(params[:copy_from]) if params[:copy_from]
@issue.project = @project
else
@issue = @project.issues.visible.find(params[:id])
end
@issue = Issue.new
@issue.copy_from(params[:copy_from]) if params[:copy_from]
@issue.project = @project
# Tracker must be set before custom field values
@issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
@@ -291,29 +458,31 @@ private
render_error l(:error_no_tracker_in_project)
return false
end
@issue.start_date ||= Date.today
if params[:issue].is_a?(Hash)
@issue.safe_attributes = params[:issue]
if User.current.allowed_to?(:add_issue_watchers, @project) && @issue.new_record?
@issue.watcher_user_ids = params[:issue]['watcher_user_ids']
end
@issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
end
@issue.author = User.current
@issue.start_date ||= Date.today
@priorities = IssuePriority.all
@allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
end
def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
if unsaved_issue_ids.empty?
flash[:notice] = l(:notice_successful_update) unless issues.empty?
else
flash[:error] = l(:notice_failed_to_save_issues,
:count => unsaved_issue_ids.size,
:total => issues.size,
:ids => '#' + unsaved_issue_ids.join(', #'))
end
end
def check_for_default_issue_status
if IssueStatus.default.nil?
render_error l(:error_no_default_issue_status)
return false
end
end
def parse_params_for_bulk_issue_attributes(params)
attributes = (params[:issue] || {}).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
end
end

View File

@@ -16,56 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class JournalsController < ApplicationController
before_filter :find_journal, :only => [:edit]
before_filter :find_issue, :only => [:new]
before_filter :find_optional_project, :only => [:index]
before_filter :authorize, :only => [:new, :edit]
accept_key_auth :index
helper :issues
helper :custom_fields
helper :queries
include QueriesHelper
helper :sort
include SortHelper
def index
retrieve_query
sort_init 'id', 'desc'
sort_update(@query.sortable_columns)
if @query.valid?
@journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
:limit => 25)
end
@title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
render :layout => false, :content_type => 'application/atom+xml'
rescue ActiveRecord::RecordNotFound
render_404
end
def new
journal = Journal.find(params[:journal_id]) if params[:journal_id]
if journal
user = journal.user
text = journal.notes
else
user = @issue.author
text = @issue.description
end
# Replaces pre blocks with [...]
text = text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]')
content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> "
content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
render(:update) { |page|
page.<< "$('notes').value = \"#{escape_javascript content}\";"
page.show 'update'
page << "Form.Element.focus('notes');"
page << "Element.scrollTo('update');"
page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
}
end
before_filter :find_journal
def edit
if request.post?
@@ -76,14 +27,6 @@ class JournalsController < ApplicationController
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @journal.journalized_id }
format.js { render :action => 'update' }
end
else
respond_to do |format|
format.html {
# TODO: implement non-JS journal update
render :nothing => true
}
format.js
end
end
end
@@ -95,12 +38,4 @@ private
rescue ActiveRecord::RecordNotFound
render_404
end
# TODO: duplicated in IssuesController
def find_issue
@issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
@project = @issue.project
rescue ActiveRecord::RecordNotFound
render_404
end
end

View File

@@ -19,7 +19,6 @@ class MyController < ApplicationController
before_filter :require_login
helper :issues
helper :users
helper :custom_fields
BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues,
@@ -54,18 +53,25 @@ class MyController < ApplicationController
@user = User.current
@pref = @user.pref
if request.post?
@user.safe_attributes = params[:user]
@user.attributes = params[:user]
@user.mail_notification = (params[:notification_option] == 'all')
@user.pref.attributes = params[:pref]
@user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
if @user.save
@user.pref.save
@user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
@user.notified_project_ids = (params[:notification_option] == 'selected' ? params[:notified_project_ids] : [])
set_language_if_valid @user.language
flash[:notice] = l(:notice_account_updated)
redirect_to :action => 'account'
return
end
end
@notification_options = [[l(:label_user_mail_option_all), 'all'],
[l(:label_user_mail_option_none), 'none']]
# Only users that belong to more than 1 project can select projects for which they are notified
# Note that @user.membership.size would fail since AR ignores :include association option when doing a count
@notification_options.insert 1, [l(:label_user_mail_option_selected), 'selected'] if @user.memberships.length > 1
@notification_option = @user.mail_notification? ? 'all' : (@user.notified_projects_ids.empty? ? 'none' : 'selected')
end
# Manage user's password

View File

@@ -18,34 +18,23 @@
class NewsController < ApplicationController
default_search_scope :news
model_object News
before_filter :find_model_object, :except => [:new, :create, :index]
before_filter :find_project_from_association, :except => [:new, :create, :index]
before_filter :find_project, :only => [:new, :create]
before_filter :authorize, :except => [:index]
before_filter :find_model_object, :except => [:new, :index, :preview]
before_filter :find_project_from_association, :except => [:new, :index, :preview]
before_filter :find_project, :only => [:new, :preview]
before_filter :authorize, :except => [:index, :preview]
before_filter :find_optional_project, :only => :index
accept_key_auth :index
def index
case params[:format]
when 'xml', 'json'
@offset, @limit = api_offset_and_limit
else
@limit = 10
end
scope = @project ? @project.news.visible : News.visible
@news_count = scope.count
@news_pages = Paginator.new self, @news_count, @limit, params['page']
@offset ||= @news_pages.current.offset
@newss = scope.all(:include => [:author, :project],
:order => "#{News.table_name}.created_on DESC",
:offset => @offset,
:limit => @limit)
@news_pages, @newss = paginate :news,
:per_page => 10,
:conditions => Project.allowed_to_condition(User.current, :view_news, :project => @project),
:include => [:author, :project],
:order => "#{News.table_name}.created_on DESC"
respond_to do |format|
format.html { render :layout => false if request.xhr? }
format.api
format.xml { render :xml => @newss.to_xml }
format.json { render :json => @newss.to_json }
format.atom { render_feed(@newss, :title => (@project ? @project.name : Setting.app_title) + ": #{l(:label_news_plural)}") }
end
end
@@ -57,38 +46,49 @@ class NewsController < ApplicationController
def new
@news = News.new(:project => @project, :author => User.current)
end
def create
@news = News.new(:project => @project, :author => User.current)
if request.post?
@news.attributes = params[:news]
if @news.save
flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'news', :action => 'index', :project_id => @project
else
render :action => 'new'
end
end
end
def edit
end
def update
if request.put? and @news.update_attributes(params[:news])
def edit
if request.post? and @news.update_attributes(params[:news])
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'show', :id => @news
else
render :action => 'edit'
end
end
def add_comment
@comment = Comment.new(params[:comment])
@comment.author = User.current
if @news.comments << @comment
flash[:notice] = l(:label_comment_added)
redirect_to :action => 'show', :id => @news
else
show
render :action => 'show'
end
end
def destroy_comment
@news.comments.find(params[:comment_id]).destroy
redirect_to :action => 'show', :id => @news
end
def destroy
@news.destroy
redirect_to :action => 'index', :project_id => @project
end
def preview
@text = (params[:news] ? params[:news][:description] : nil)
render :partial => 'common/preview'
end
private
def find_project
@project = Project.find(params[:project_id])

View File

@@ -1,33 +0,0 @@
class PreviewsController < ApplicationController
before_filter :find_project
def issue
@issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
if @issue
@attachements = @issue.attachments
@description = params[:issue] && params[:issue][:description]
if @description && @description.gsub(/(\r?\n|\n\r?)/, "\n") == @issue.description.to_s.gsub(/(\r?\n|\n\r?)/, "\n")
@description = nil
end
@notes = params[:notes]
else
@description = (params[:issue] ? params[:issue][:description] : nil)
end
render :layout => false
end
def news
@text = (params[:news] ? params[:news][:description] : nil)
render :partial => 'common/preview'
end
private
def find_project
project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
@project = Project.find(project_id)
rescue ActiveRecord::RecordNotFound
render_404
end
end

View File

@@ -1,26 +0,0 @@
class ProjectEnumerationsController < ApplicationController
before_filter :find_project_by_project_id
before_filter :authorize
def update
if request.put? && params[:enumerations]
Project.transaction do
params[:enumerations].each do |id, activity|
@project.update_or_create_time_entry_activity(id, activity)
end
end
flash[:notice] = l(:notice_successful_update)
end
redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
end
def destroy
@project.time_entry_activities.each do |time_entry_activity|
time_entry_activity.destroy(time_entry_activity.parent)
end
flash[:notice] = l(:notice_successful_update)
redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
end
end

View File

@@ -17,21 +17,24 @@
class ProjectsController < ApplicationController
menu_item :overview
menu_item :activity, :only => :activity
menu_item :roadmap, :only => :roadmap
menu_item :files, :only => [:list_files, :add_file]
menu_item :settings, :only => :settings
before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
before_filter :authorize_global, :only => [:new, :create]
before_filter :find_project, :except => [ :index, :list, :add, :copy, :activity ]
before_filter :find_optional_project, :only => :activity
before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy, :activity ]
before_filter :authorize_global, :only => :add
before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
accept_key_auth :index, :show, :create, :update, :destroy
after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
accept_key_auth :activity, :index
after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
if controller.request.post?
controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
end
end
helper :sort
include SortHelper
helper :custom_fields
@@ -49,10 +52,8 @@ class ProjectsController < ApplicationController
format.html {
@projects = Project.visible.find(:all, :order => 'lft')
}
format.api {
@offset, @limit = api_offset_and_limit
@project_count = Project.visible.count
@projects = Project.visible.all(:offset => @offset, :limit => @limit, :order => 'lft')
format.xml {
@projects = Project.visible.find(:all, :order => 'lft')
}
format.atom {
projects = Project.visible.find(:all, :order => 'created_on DESC',
@@ -62,41 +63,40 @@ class ProjectsController < ApplicationController
end
end
def new
# Add a new project
def add
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
@trackers = Tracker.all
@project = Project.new(params[:project])
end
verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
def create
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
@trackers = Tracker.all
@project = Project.new
@project.safe_attributes = params[:project]
if validate_parent_id && @project.save
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
# Add current user as a project member if he is not admin
unless User.current.admin?
r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
m = Member.new(:user => User.current, :roles => [r])
@project.members << m
end
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'projects', :action => 'settings', :id => @project
}
format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
end
if request.get?
@project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
@project.trackers = Tracker.all
@project.is_public = Setting.default_projects_public?
@project.enabled_module_names = Setting.default_projects_modules
else
respond_to do |format|
format.html { render :action => 'new' }
format.api { render_validation_errors(@project) }
@project.enabled_module_names = params[:enabled_modules]
if validate_parent_id && @project.save
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
# Add current user as a project member if he is not admin
unless User.current.admin?
r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
m = Member.new(:user => User.current, :roles => [r])
@project.members << m
end
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'projects', :action => 'settings', :id => @project
}
format.xml { head :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
end
else
respond_to do |format|
format.html
format.xml { render :xml => @project.errors, :status => :unprocessable_entity }
end
end
end
end
end
def copy
@@ -115,19 +115,18 @@ class ProjectsController < ApplicationController
end
else
Mailer.with_deliveries(params[:notifications] == '1') do
@project = Project.new
@project.safe_attributes = params[:project]
@project = Project.new(params[:project])
@project.enabled_module_names = params[:enabled_modules]
if validate_parent_id && @project.copy(@source_project, :only => params[:only])
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'projects', :action => 'settings', :id => @project
redirect_to :controller => 'admin', :action => 'projects'
elsif !@project.new_record?
# Project was created
# But some objects were not copied due to validation failures
# (eg. issues from disabled trackers)
# TODO: inform about that
redirect_to :controller => 'projects', :action => 'settings', :id => @project
redirect_to :controller => 'admin', :action => 'projects'
end
end
end
@@ -165,7 +164,7 @@ class ProjectsController < ApplicationController
respond_to do |format|
format.html
format.api
format.xml
end
end
@@ -178,36 +177,34 @@ class ProjectsController < ApplicationController
@wiki ||= @project.wiki
end
# Edit @project
def edit
end
# TODO: convert to PUT only
verify :method => [:post, :put], :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
def update
@project.safe_attributes = params[:project]
if validate_parent_id && @project.save
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'settings', :id => @project
}
format.api { head :ok }
end
if request.get?
else
respond_to do |format|
format.html {
settings
render :action => 'settings'
}
format.api { render_validation_errors(@project) }
@project.attributes = params[:project]
if validate_parent_id && @project.save
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'settings', :id => @project
}
format.xml { head :ok }
end
else
respond_to do |format|
format.html {
settings
render :action => 'settings'
}
format.xml { render :xml => @project.errors, :status => :unprocessable_entity }
end
end
end
end
verify :method => :post, :only => :modules, :render => {:nothing => true, :status => :method_not_allowed }
def modules
@project.enabled_module_names = params[:enabled_module_names]
@project.enabled_module_names = params[:enabled_modules]
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'settings', :id => @project, :tab => 'modules'
end
@@ -232,11 +229,11 @@ class ProjectsController < ApplicationController
if request.get?
# display confirmation view
else
if api_request? || params[:confirm]
if params[:format] == 'xml' || params[:confirm]
@project_to_destroy.destroy
respond_to do |format|
format.html { redirect_to :controller => 'admin', :action => 'projects' }
format.api { head :ok }
format.xml { head :ok }
end
end
end
@@ -244,6 +241,120 @@ class ProjectsController < ApplicationController
@project = nil
end
def add_file
if request.post?
container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
attachments = Attachment.attach_files(container, params[:attachments])
render_attachment_warning_if_needed(container)
if !attachments.empty? && Setting.notified_events.include?('file_added')
Mailer.deliver_attachments_added(attachments[:files])
end
redirect_to :controller => 'projects', :action => 'list_files', :id => @project
return
end
@versions = @project.versions.sort
end
def save_activities
if request.post? && params[:enumerations]
Project.transaction do
params[:enumerations].each do |id, activity|
@project.update_or_create_time_entry_activity(id, activity)
end
end
flash[:notice] = l(:notice_successful_update)
end
redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
end
def reset_activities
@project.time_entry_activities.each do |time_entry_activity|
time_entry_activity.destroy(time_entry_activity.parent)
end
flash[:notice] = l(:notice_successful_update)
redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
end
def list_files
sort_init 'filename', 'asc'
sort_update 'filename' => "#{Attachment.table_name}.filename",
'created_on' => "#{Attachment.table_name}.created_on",
'size' => "#{Attachment.table_name}.filesize",
'downloads' => "#{Attachment.table_name}.downloads"
@containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
@containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
render :layout => !request.xhr?
end
def roadmap
@trackers = @project.trackers.find(:all, :order => 'position')
retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?})
@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.reject! {|version| version.closed? || version.completed? } unless params[:completed]
@issues_by_version = {}
unless @selected_tracker_ids.empty?
@versions.each do |version|
issues = version.fixed_issues.visible.find(:all,
:include => [:project, :status, :tracker, :priority],
:conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids},
:order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id")
@issues_by_version[version] = issues
end
end
@versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?}
end
def activity
@days = Setting.activity_days_default.to_i
if params[:from]
begin; @date_to = params[:from].to_date + 1; rescue; end
end
@date_to ||= Date.today + 1
@date_from = @date_to - @days
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
@author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
@activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
:with_subprojects => @with_subprojects,
:author => @author)
@activity.scope_select {|t| !params["show_#{t}"].nil?}
@activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
events = @activity.events(@date_from, @date_to)
if events.empty? || stale?(:etag => [events.first, User.current])
respond_to do |format|
format.html {
@events_by_day = events.group_by(&:event_date)
render :layout => false if request.xhr?
}
format.atom {
title = l(:label_activity)
if @author
title = @author.name
elsif @activity.scope.size == 1
title = l("label_#{@activity.scope.first.singularize}_plural")
end
render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
}
end
end
rescue ActiveRecord::RecordNotFound
render_404
end
private
def find_optional_project
return true unless params[:id]
@@ -253,6 +364,14 @@ private
render_404
end
def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
if ids = params[:tracker_ids]
@selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
else
@selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s }
end
end
# Validates parent_id param according to user's permissions
# TODO: move it to Project model in a validation that depends on User.current
def validate_parent_id

View File

@@ -123,7 +123,7 @@ class RepositoriesController < ApplicationController
(show_error_not_found; return) unless @content
if 'raw' == params[:format] || @content.is_binary_data? || (@entry.size && @entry.size > Setting.file_max_size_displayed.to_i.kilobyte)
# Force the download
send_data @content, :filename => filename_for_content_disposition(@path.split('/').last)
send_data @content, :filename => @path.split('/').last
else
# Prevent empty lines when displaying a file with Windows style eol
@content.gsub!("\r\n", "\n")
@@ -139,7 +139,6 @@ class RepositoriesController < ApplicationController
end
def revision
raise ChangesetNotFound if @rev.nil? || @rev.empty?
@changeset = @repository.find_changeset_by_name(@rev)
raise ChangesetNotFound unless @changeset
@@ -175,9 +174,6 @@ class RepositoriesController < ApplicationController
@diff = @repository.diff(@path, @rev, @rev_to)
show_error_not_found unless @diff
end
@changeset = @repository.find_changeset_by_name(@rev)
@changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
end
end
@@ -200,10 +196,7 @@ class RepositoriesController < ApplicationController
end
end
private
REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
private
def find_repository
@project = Project.find(params[:id])
@repository = @project.repository
@@ -212,12 +205,6 @@ class RepositoriesController < ApplicationController
@path ||= ''
@rev = params[:rev].blank? ? @repository.default_branch : params[:rev].strip
@rev_to = params[:rev_to]
unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
if @repository.branches.blank?
raise InvalidRevisionParam
end
end
rescue ActiveRecord::RecordNotFound
render_404
rescue InvalidRevisionParam
@@ -225,7 +212,7 @@ class RepositoriesController < ApplicationController
end
def show_error_not_found
render_error :message => l(:error_scm_not_found), :status => 404
render_error l(:error_scm_not_found)
end
# Handler for Redmine::Scm::Adapters::CommandFailed exception

View File

@@ -26,7 +26,7 @@ class SettingsController < ApplicationController
end
def edit
@notifiables = Redmine::Notifiable.all
@notifiables = %w(issue_added issue_updated news_added document_added file_added message_posted wiki_content_added wiki_content_updated)
if request.post? && params[:settings] && params[:settings].is_a?(Hash)
settings = (params[:settings] || {}).dup.symbolize_keys
settings.each do |name, value|
@@ -44,8 +44,6 @@ class SettingsController < ApplicationController
@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
def plugin

View File

@@ -1,209 +0,0 @@
class TimeEntryReportsController < ApplicationController
menu_item :issues
before_filter :find_optional_project
before_filter :load_available_criterias
helper :sort
include SortHelper
helper :issues
helper :timelog
include TimelogHelper
helper :custom_fields
include CustomFieldsHelper
def report
@criterias = params[:criterias] || []
@criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
@criterias.uniq!
@criterias = @criterias[0,3]
@columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
retrieve_date_range
unless @criterias.empty?
sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
sql_condition = ''
if @project.nil?
sql_condition = Project.allowed_to_condition(User.current, :view_time_entries)
elsif @issue.nil?
sql_condition = @project.project_condition(Setting.display_subprojects_issues?)
else
sql_condition = "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}"
end
sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
sql << " FROM #{TimeEntry.table_name}"
sql << time_report_joins
sql << " WHERE"
sql << " (%s) AND" % sql_condition
sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from), ActiveRecord::Base.connection.quoted_date(@to)]
sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
@hours = ActiveRecord::Base.connection.select_all(sql)
@hours.each do |row|
case @columns
when 'year'
row['year'] = row['tyear']
when 'month'
row['month'] = "#{row['tyear']}-#{row['tmonth']}"
when 'week'
row['week'] = "#{row['tyear']}-#{row['tweek']}"
when 'day'
row['day'] = "#{row['spent_on']}"
end
end
@total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
@periods = []
# Date#at_beginning_of_ not supported in Rails 1.2.x
date_from = @from.to_time
# 100 columns max
while date_from <= @to.to_time && @periods.length < 100
case @columns
when 'year'
@periods << "#{date_from.year}"
date_from = (date_from + 1.year).at_beginning_of_year
when 'month'
@periods << "#{date_from.year}-#{date_from.month}"
date_from = (date_from + 1.month).at_beginning_of_month
when 'week'
@periods << "#{date_from.year}-#{date_from.to_date.cweek}"
date_from = (date_from + 7.day).at_beginning_of_week
when 'day'
@periods << "#{date_from.to_date}"
date_from = date_from + 1.day
end
end
end
respond_to do |format|
format.html { render :layout => !request.xhr? }
format.csv { send_data(report_to_csv(@criterias, @periods, @hours), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
end
end
private
# TODO: duplicated in TimelogController
def find_optional_project
if !params[:issue_id].blank?
@issue = Issue.find(params[:issue_id])
@project = @issue.project
elsif !params[:project_id].blank?
@project = Project.find(params[:project_id])
end
deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
end
# Retrieves the date range based on predefined ranges or specific from/to param dates
# TODO: duplicated in TimelogController
def retrieve_date_range
@free_period = false
@from, @to = nil, nil
if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
case params[:period].to_s
when 'today'
@from = @to = Date.today
when 'yesterday'
@from = @to = Date.today - 1
when 'current_week'
@from = Date.today - (Date.today.cwday - 1)%7
@to = @from + 6
when 'last_week'
@from = Date.today - 7 - (Date.today.cwday - 1)%7
@to = @from + 6
when '7_days'
@from = Date.today - 7
@to = Date.today
when 'current_month'
@from = Date.civil(Date.today.year, Date.today.month, 1)
@to = (@from >> 1) - 1
when 'last_month'
@from = Date.civil(Date.today.year, Date.today.month, 1) << 1
@to = (@from >> 1) - 1
when '30_days'
@from = Date.today - 30
@to = Date.today
when 'current_year'
@from = Date.civil(Date.today.year, 1, 1)
@to = Date.civil(Date.today.year, 12, 31)
end
elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
@free_period = true
else
# default
end
@from, @to = @to, @from if @from && @to && @from > @to
@from ||= (TimeEntry.earilest_date_for_project(@project) || Date.today)
@to ||= (TimeEntry.latest_date_for_project(@project) || Date.today)
end
def load_available_criterias
@available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
:klass => Project,
:label => :label_project},
'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
:klass => Version,
:label => :label_version},
'category' => {:sql => "#{Issue.table_name}.category_id",
:klass => IssueCategory,
:label => :field_category},
'member' => {:sql => "#{TimeEntry.table_name}.user_id",
:klass => User,
:label => :label_member},
'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
:klass => Tracker,
:label => :label_tracker},
'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
:klass => TimeEntryActivity,
:label => :label_activity},
'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
:klass => Issue,
:label => :label_issue}
}
# Add list and boolean custom fields as available criterias
custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id)",
:format => cf.field_format,
:label => cf.name}
end if @project
# Add list and boolean time entry custom fields
TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id)",
:format => cf.field_format,
:label => cf.name}
end
# Add list and boolean time entry activity custom fields
TimeEntryActivityCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Enumeration' AND c.customized_id = #{TimeEntry.table_name}.activity_id)",
:format => cf.field_format,
:label => cf.name}
end
call_hook(:controller_timelog_available_criterias, { :available_criterias => @available_criterias, :project => @project })
@available_criterias
end
def time_report_joins
sql = ''
sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
# TODO: rename hook
call_hook(:controller_timelog_time_report_joins, {:sql => sql} )
sql
end
end

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2010 Jean-Philippe Lang
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -17,11 +17,11 @@
class TimelogController < ApplicationController
menu_item :issues
before_filter :find_project, :only => [:new, :create]
before_filter :find_time_entry, :only => [:show, :edit, :update, :destroy]
before_filter :authorize, :except => [:index]
before_filter :find_optional_project, :only => [:index]
accept_key_auth :index, :show, :create, :update, :destroy
before_filter :find_project, :authorize, :only => [:edit, :destroy]
before_filter :find_optional_project, :only => [:report, :details]
before_filter :load_available_criterias, :only => [:report]
verify :method => :post, :only => :destroy, :redirect_to => { :action => :details }
helper :sort
include SortHelper
@@ -30,7 +30,84 @@ class TimelogController < ApplicationController
helper :custom_fields
include CustomFieldsHelper
def index
def report
@criterias = params[:criterias] || []
@criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
@criterias.uniq!
@criterias = @criterias[0,3]
@columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
retrieve_date_range
unless @criterias.empty?
sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
sql_condition = ''
if @project.nil?
sql_condition = Project.allowed_to_condition(User.current, :view_time_entries)
elsif @issue.nil?
sql_condition = @project.project_condition(Setting.display_subprojects_issues?)
else
sql_condition = "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}"
end
sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
sql << " FROM #{TimeEntry.table_name}"
sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
sql << " WHERE"
sql << " (%s) AND" % sql_condition
sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from), ActiveRecord::Base.connection.quoted_date(@to)]
sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
@hours = ActiveRecord::Base.connection.select_all(sql)
@hours.each do |row|
case @columns
when 'year'
row['year'] = row['tyear']
when 'month'
row['month'] = "#{row['tyear']}-#{row['tmonth']}"
when 'week'
row['week'] = "#{row['tyear']}-#{row['tweek']}"
when 'day'
row['day'] = "#{row['spent_on']}"
end
end
@total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
@periods = []
# Date#at_beginning_of_ not supported in Rails 1.2.x
date_from = @from.to_time
# 100 columns max
while date_from <= @to.to_time && @periods.length < 100
case @columns
when 'year'
@periods << "#{date_from.year}"
date_from = (date_from + 1.year).at_beginning_of_year
when 'month'
@periods << "#{date_from.year}-#{date_from.month}"
date_from = (date_from + 1.month).at_beginning_of_month
when 'week'
@periods << "#{date_from.year}-#{date_from.to_date.cweek}"
date_from = (date_from + 7.day).at_beginning_of_week
when 'day'
@periods << "#{date_from.to_date}"
date_from = date_from + 1.day
end
end
end
respond_to do |format|
format.html { render :layout => !request.xhr? }
format.csv { send_data(report_to_csv(@criterias, @periods, @hours), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
end
end
def details
sort_init 'spent_on', 'desc'
sort_update 'spent_on' => 'spent_on',
'user' => 'user_id',
@@ -67,16 +144,6 @@ class TimelogController < ApplicationController
render :layout => !request.xhr?
}
format.api {
@entry_count = TimeEntry.count(:include => [:project, :issue], :conditions => cond.conditions)
@entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
@entries = TimeEntry.find(:all,
:include => [:project, :activity, :user, {:issue => :tracker}],
:conditions => cond.conditions,
:order => sort_clause,
:limit => @entry_pages.items_per_page,
:offset => @entry_pages.current.offset)
}
format.atom {
entries = TimeEntry.find(:all,
:include => [:project, :activity, :user, {:issue => :tracker}],
@@ -97,114 +164,43 @@ class TimelogController < ApplicationController
end
end
def show
respond_to do |format|
# TODO: Implement html response
format.html { render :nothing => true, :status => 406 }
format.api
end
end
def new
@time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
@time_entry.attributes = params[:time_entry]
call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
render :action => 'edit'
end
verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
def create
def edit
(render_403; return) if @time_entry && !@time_entry.editable_by?(User.current)
@time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
@time_entry.attributes = params[:time_entry]
call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
if @time_entry.save
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_update)
redirect_back_or_default :action => 'index', :project_id => @time_entry.project
}
format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
end
else
respond_to do |format|
format.html { render :action => 'edit' }
format.api { render_validation_errors(@time_entry) }
end
if request.post? and @time_entry.save
flash[:notice] = l(:notice_successful_update)
redirect_back_or_default :action => 'details', :project_id => @time_entry.project
return
end
end
def edit
@time_entry.attributes = params[:time_entry]
call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
end
verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
def update
@time_entry.attributes = params[:time_entry]
call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
if @time_entry.save
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_update)
redirect_back_or_default :action => 'index', :project_id => @time_entry.project
}
format.api { head :ok }
end
else
respond_to do |format|
format.html { render :action => 'edit' }
format.api { render_validation_errors(@time_entry) }
end
end
end
verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed }
def destroy
(render_404; return) unless @time_entry
(render_403; return) unless @time_entry.editable_by?(User.current)
if @time_entry.destroy && @time_entry.destroyed?
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_delete)
redirect_to :back
}
format.api { head :ok }
end
flash[:notice] = l(:notice_successful_delete)
else
respond_to do |format|
format.html {
flash[:error] = l(:notice_unable_delete_time_entry)
redirect_to :back
}
format.api { render_validation_errors(@time_entry) }
end
flash[:error] = l(:notice_unable_delete_time_entry)
end
redirect_to :back
rescue ::ActionController::RedirectBackError
redirect_to :action => 'index', :project_id => @time_entry.project
redirect_to :action => 'details', :project_id => @time_entry.project
end
private
def find_time_entry
@time_entry = TimeEntry.find(params[:id])
unless @time_entry.editable_by?(User.current)
render_403
return false
end
@project = @time_entry.project
rescue ActiveRecord::RecordNotFound
render_404
end
def find_project
if (issue_id = (params[:issue_id] || params[:time_entry] && params[:time_entry][:issue_id])).present?
@issue = Issue.find(issue_id)
if params[:id]
@time_entry = TimeEntry.find(params[:id])
@project = @time_entry.project
elsif params[:issue_id]
@issue = Issue.find(params[:issue_id])
@project = @issue.project
elsif (project_id = (params[:project_id] || params[:time_entry] && params[:time_entry][:project_id])).present?
@project = Project.find(project_id)
elsif params[:project_id]
@project = Project.find(params[:project_id])
else
render_404
return false
@@ -265,8 +261,57 @@ private
end
@from, @to = @to, @from if @from && @to && @from > @to
@from ||= (TimeEntry.earilest_date_for_project(@project) || Date.today)
@to ||= (TimeEntry.latest_date_for_project(@project) || Date.today)
@from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today) - 1
@to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today)
end
def load_available_criterias
@available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
:klass => Project,
:label => :label_project},
'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
:klass => Version,
:label => :label_version},
'category' => {:sql => "#{Issue.table_name}.category_id",
:klass => IssueCategory,
:label => :field_category},
'member' => {:sql => "#{TimeEntry.table_name}.user_id",
:klass => User,
:label => :label_member},
'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
:klass => Tracker,
:label => :label_tracker},
'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
:klass => TimeEntryActivity,
:label => :label_activity},
'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
:klass => Issue,
:label => :label_issue}
}
# Add list and boolean custom fields as available criterias
custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id)",
:format => cf.field_format,
:label => cf.name}
end if @project
# Add list and boolean time entry custom fields
TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id)",
:format => cf.field_format,
:label => cf.name}
end
# Add list and boolean time entry activity custom fields
TimeEntryActivityCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Enumeration' AND c.customized_id = #{TimeEntry.table_name}.activity_id)",
:format => cf.field_format,
:label => cf.name}
end
call_hook(:controller_timelog_available_criterias, { :available_criterias => @available_criterias, :project => @project })
@available_criterias
end
end

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2010 Jean-Philippe Lang
# Copyright (C) 2006-2009 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -19,8 +19,6 @@ class UsersController < ApplicationController
layout 'admin'
before_filter :require_admin, :except => :show
before_filter :find_user, :only => [:show, :edit, :update, :edit_membership, :destroy_membership]
accept_key_auth :index, :show, :create, :update
helper :sort
include SortHelper
@@ -31,13 +29,6 @@ class UsersController < ApplicationController
sort_init 'login', 'asc'
sort_update %w(login firstname lastname mail admin created_on last_login_on)
case params[:format]
when 'xml', 'json'
@offset, @limit = api_offset_and_limit
else
@limit = per_page_option
end
@status = params[:status] ? params[:status].to_i : 1
c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
@@ -47,23 +38,25 @@ class UsersController < ApplicationController
end
@user_count = User.count(:conditions => c.conditions)
@user_pages = Paginator.new self, @user_count, @limit, params['page']
@offset ||= @user_pages.current.offset
@users = User.find :all,
:order => sort_clause,
@user_pages = Paginator.new self, @user_count,
per_page_option,
params['page']
@users = User.find :all,:order => sort_clause,
:conditions => c.conditions,
:limit => @limit,
:offset => @offset
:limit => @user_pages.items_per_page,
:offset => @user_pages.current.offset
respond_to do |format|
format.html { render :layout => !request.xhr? }
format.api
end
render :layout => !request.xhr?
end
def show
# show projects based on current user visibility
@memberships = @user.memberships.all(:conditions => Project.visible_by(User.current))
@user = User.find(params[:id])
@custom_values = @user.custom_values
# show only public projects and private projects that the logged in user is also a member of
@memberships = @user.memberships.select do |membership|
membership.project.is_public? || (User.current.member_of?(membership.project))
end
events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
@events_by_day = events.group_by(&:event_date)
@@ -74,132 +67,74 @@ class UsersController < ApplicationController
return
end
end
respond_to do |format|
format.html { render :layout => 'base' }
format.api
end
render :layout => 'base'
rescue ActiveRecord::RecordNotFound
render_404
end
def new
@user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
@auth_sources = AuthSource.find(:all)
end
verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
def create
@user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
@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
# TODO: Similar to My#account
@user.pref.attributes = params[:pref]
@user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
if @user.save
@user.pref.save
@user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
Mailer.deliver_account_information(@user, params[:user][:password]) if params[:send_information]
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_create)
redirect_to(params[:continue] ?
{:controller => 'users', :action => 'new'} :
{:controller => 'users', :action => 'edit', :id => @user}
)
}
format.api { render :action => 'show', :status => :created, :location => user_url(@user) }
end
def add
if request.get?
@user = User.new(:language => Setting.default_language)
else
@auth_sources = AuthSource.find(:all)
# Clear password input
@user.password = @user.password_confirmation = nil
respond_to do |format|
format.html { render :action => 'new' }
format.api { render_validation_errors(@user) }
@user = User.new(params[:user])
@user.admin = params[:user][:admin] || false
@user.login = params[:user][:login]
@user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless @user.auth_source_id
if @user.save
Mailer.deliver_account_information(@user, params[:password]) if params[:send_information]
flash[:notice] = l(:notice_successful_create)
redirect_to(params[:continue] ? {:controller => 'users', :action => 'add'} :
{:controller => 'users', :action => 'edit', :id => @user})
return
end
end
@auth_sources = AuthSource.find(:all)
end
def edit
@user = User.find(params[:id])
if request.post?
@user.admin = params[:user][:admin] if params[:user][:admin]
@user.login = params[:user][:login] if params[:user][:login]
@user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id
@user.group_ids = params[:user][:group_ids] if params[:user][:group_ids]
@user.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])
if @user.save
if was_activated
Mailer.deliver_account_activated(@user)
elsif @user.active? && params[:send_information] && !params[:password].blank? && @user.auth_source_id.nil?
Mailer.deliver_account_information(@user, params[:password])
end
flash[:notice] = l(:notice_successful_update)
redirect_to :back
end
end
@auth_sources = AuthSource.find(:all)
@membership ||= Member.new
end
verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
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]
@user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
if @user.save
@user.pref.save
@user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
if was_activated
Mailer.deliver_account_activated(@user)
elsif @user.active? && params[:send_information] && !params[:user][:password].blank? && @user.auth_source_id.nil?
Mailer.deliver_account_information(@user, params[:user][:password])
end
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_update)
redirect_to :back
}
format.api { head :ok }
end
else
@auth_sources = AuthSource.find(:all)
@membership ||= Member.new
# Clear password input
@user.password = @user.password_confirmation = nil
respond_to do |format|
format.html { render :action => :edit }
format.api { render_validation_errors(@user) }
end
end
rescue ::ActionController::RedirectBackError
redirect_to :controller => 'users', :action => 'edit', :id => @user
end
def edit_membership
@user = User.find(params[:id])
@membership = Member.edit_membership(params[:membership_id], params[:membership], @user)
@membership.save if request.post?
respond_to do |format|
if @membership.valid?
format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
format.js {
render(:update) {|page|
page.replace_html "tab-content-memberships", :partial => 'users/memberships'
page.visual_effect(:highlight, "member-#{@membership.id}")
}
}
else
format.js {
render(:update) {|page|
page.alert(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', ')))
}
}
end
end
format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
format.js {
render(:update) {|page|
page.replace_html "tab-content-memberships", :partial => 'users/memberships'
page.visual_effect(:highlight, "member-#{@membership.id}")
}
}
end
end
def destroy_membership
@user = User.find(params[:id])
@membership = Member.find(params[:membership_id])
if request.post? && @membership.deletable?
@membership.destroy
@@ -209,17 +144,4 @@ class UsersController < ApplicationController
format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'users/memberships'} }
end
end
private
def find_user
if params[:id] == 'current'
require_login || return
@user = User.current
else
@user = User.find(params[:id])
end
rescue ActiveRecord::RecordNotFound
render_404
end
end

View File

@@ -18,42 +18,15 @@
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, :only => [:index, :new, :create, :close_completed]
before_filter :find_model_object, :except => [:new, :close_completed]
before_filter :find_project_from_association, :except => [:new, :close_completed]
before_filter :find_project, :only => [:new, :close_completed]
before_filter :authorize
helper :custom_fields
helper :projects
def index
@trackers = @project.trackers.find(:all, :order => 'position')
retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?})
@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.reject! {|version| version.closed? || version.completed? } unless params[:completed]
@issues_by_version = {}
unless @selected_tracker_ids.empty?
@versions.each do |version|
issues = version.fixed_issues.visible.find(:all,
:include => [:project, :status, :tracker, :priority],
:conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids},
:order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id")
@issues_by_version[version] = issues
end
end
@versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?}
end
def show
@issues = @version.fixed_issues.visible.find(:all,
:include => [:status, :tracker, :priority],
:order => "#{Tracker.table_name}.position, #{Issue.table_name}.id")
end
def new
@@ -63,17 +36,6 @@ class VersionsController < ApplicationController
attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
@version.attributes = attributes
end
end
def create
# TODO: refactor with code above in #new
@version = @project.versions.build
if params[:version]
attributes = params[:version].dup
attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
@version.attributes = attributes
end
if request.post?
if @version.save
respond_to do |format|
@@ -90,7 +52,7 @@ class VersionsController < ApplicationController
end
else
respond_to do |format|
format.html { render :action => 'new' }
format.html
format.js do
render(:update) {|page| page.alert(@version.errors.full_messages.join('\n')) }
end
@@ -98,27 +60,20 @@ class VersionsController < ApplicationController
end
end
end
def edit
end
def update
if request.put? && params[:version]
def edit
if request.post? && params[:version]
attributes = params[:version].dup
attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing'])
if @version.update_attributes(attributes)
flash[:notice] = l(:notice_successful_update)
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
else
respond_to do |format|
format.html { render :action => 'edit' }
end
end
end
end
def close_completed
if request.put?
if request.post?
@project.close_completed_versions
end
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
@@ -147,13 +102,4 @@ private
rescue ActiveRecord::RecordNotFound
render_404
end
def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
if ids = params[:tracker_ids]
@selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
else
@selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s }
end
end
end

View File

@@ -17,39 +17,20 @@
require 'diff'
# The WikiController follows the Rails REST controller pattern but with
# a few differences
#
# * index - shows a list of WikiPages grouped by page or date
# * new - not used
# * create - not used
# * show - will also show the form for creating a new wiki page
# * edit - used to edit an existing or new page
# * update - used to save a wiki page update to the database, including new pages
# * destroy - normal
#
# Other member and collection methods are also used
#
# TODO: still being worked on
class WikiController < ApplicationController
default_search_scope :wiki_pages
before_filter :find_wiki, :authorize
before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
verify :method => :post, :only => [:protect], :redirect_to => { :action => :show }
verify :method => :post, :only => [:destroy, :protect], :redirect_to => { :action => :index }
helper :attachments
include AttachmentsHelper
helper :watchers
# List of pages, sorted alphabetically and by parent (hierarchy)
def index
load_pages_grouped_by_date_without_content
end
# display a page (in editing mode if it doesn't exist)
def show
page_title = params[:id]
def index
page_title = params[:page]
@page = @wiki.find_or_new_page(page_title)
if @page.new_record?
if User.current.allowed_to?(:edit_wiki_pages, @project) && editable?
@@ -82,7 +63,7 @@ class WikiController < ApplicationController
# edit an existing page or a new one
def edit
@page = @wiki.find_or_new_page(params[:id])
@page = @wiki.find_or_new_page(params[:page])
return render_403 unless editable?
@page.content = WikiContent.new(:page => @page) if @page.new_record?
@@ -90,50 +71,34 @@ class WikiController < ApplicationController
@content.text = initial_page_content(@page) if @content.text.blank?
# don't keep previous comment
@content.comments = nil
# To prevent StaleObjectError exception when reverting to a previous version
@content.version = @page.content.version
rescue ActiveRecord::StaleObjectError
# Optimistic locking exception
flash[:error] = l(:notice_locking_conflict)
end
verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
# Creates a new page or updates an existing one
def update
@page = @wiki.find_or_new_page(params[:id])
return render_403 unless editable?
@page.content = WikiContent.new(:page => @page) if @page.new_record?
@content = @page.content_for_version(params[:version])
@content.text = initial_page_content(@page) if @content.text.blank?
# don't keep previous comment
@content.comments = nil
if !@page.new_record? && params[:content].present? && @content.text == params[:content][:text]
attachments = Attachment.attach_files(@page, params[:attachments])
render_attachment_warning_if_needed(@page)
# don't save if text wasn't changed
redirect_to :action => 'show', :project_id => @project, :id => @page.title
return
end
@content.attributes = params[:content]
@content.author = User.current
# if page is new @page.save will also save content, but not if page isn't a new record
if (@page.new_record? ? @page.save : @content.save)
attachments = Attachment.attach_files(@page, params[:attachments])
render_attachment_warning_if_needed(@page)
call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
redirect_to :action => 'show', :project_id => @project, :id => @page.title
if request.get?
# To prevent StaleObjectError exception when reverting to a previous version
@content.version = @page.content.version
else
render :action => 'edit'
if !@page.new_record? && @content.text == params[:content][:text]
attachments = Attachment.attach_files(@page, params[:attachments])
render_attachment_warning_if_needed(@page)
# don't save if text wasn't changed
redirect_to :action => 'index', :id => @project, :page => @page.title
return
end
#@content.text = params[:content][:text]
#@content.comments = params[:content][:comments]
@content.attributes = params[:content]
@content.author = User.current
# if page is new @page.save will also save content, but not if page isn't a new record
if (@page.new_record? ? @page.save : @content.save)
attachments = Attachment.attach_files(@page, params[:attachments])
render_attachment_warning_if_needed(@page)
call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
redirect_to :action => 'index', :id => @project, :page => @page.title
end
end
rescue ActiveRecord::StaleObjectError
# Optimistic locking exception
flash[:error] = l(:notice_locking_conflict)
end
# rename a page
def rename
return render_403 unless editable?
@@ -142,13 +107,13 @@ class WikiController < ApplicationController
@original_title = @page.pretty_title
if request.post? && @page.update_attributes(params[:wiki_page])
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'show', :project_id => @project, :id => @page.title
redirect_to :action => 'index', :id => @project, :page => @page.title
end
end
def protect
@page.update_attribute :protected, params[:protected]
redirect_to :action => 'show', :project_id => @project, :id => @page.title
redirect_to :action => 'index', :id => @project, :page => @page.title
end
# show page history
@@ -174,8 +139,7 @@ class WikiController < ApplicationController
@annotate = @page.annotate(params[:version])
render_404 unless @annotate
end
verify :method => :delete, :only => [:destroy], :redirect_to => { :action => :show }
# Removes a wiki page and its history
# Children can be either set as root pages, removed or reassigned to another parent page
def destroy
@@ -202,26 +166,41 @@ class WikiController < ApplicationController
end
end
@page.destroy
redirect_to :action => 'index', :project_id => @project
redirect_to :action => 'special', :id => @project, :page => 'Page_index'
end
# Export wiki to a single html file
def export
if User.current.allowed_to?(:export_wiki_pages, @project)
@pages = @wiki.pages.find :all, :order => 'title'
export = render_to_string :action => 'export_multiple', :layout => false
send_data(export, :type => 'text/html', :filename => "wiki.html")
# display special pages
def special
page_title = params[:page].downcase
case page_title
# show pages index, sorted by title
when 'page_index', 'date_index'
# eager load information about last updates, without loading text
@pages = @wiki.pages.find :all, :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
:joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id",
:order => 'title'
@pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
@pages_by_parent_id = @pages.group_by(&:parent_id)
# export wiki to a single html file
when 'export'
if User.current.allowed_to?(:export_wiki_pages, @project)
@pages = @wiki.pages.find :all, :order => 'title'
export = render_to_string :action => 'export_multiple', :layout => false
send_data(export, :type => 'text/html', :filename => "wiki.html")
else
redirect_to :action => 'index', :id => @project, :page => nil
end
return
else
redirect_to :action => 'show', :project_id => @project, :id => nil
# requested special page doesn't exist, redirect to default page
redirect_to :action => 'index', :id => @project, :page => nil
return
end
end
def date_index
load_pages_grouped_by_date_without_content
render :action => "special_#{page_title}"
end
def preview
page = @wiki.find_page(params[:id])
page = @wiki.find_page(params[:page])
# page is nil when previewing a new page
return render_403 unless page.nil? || editable?(page)
if page
@@ -236,13 +215,13 @@ class WikiController < ApplicationController
return render_403 unless editable?
attachments = Attachment.attach_files(@page, params[:attachments])
render_attachment_warning_if_needed(@page)
redirect_to :action => 'show', :id => @page.title, :project_id => @project
redirect_to :action => 'index', :page => @page.title
end
private
def find_wiki
@project = Project.find(params[:project_id])
@project = Project.find(params[:id])
@wiki = @project.wiki
render_404 unless @wiki
rescue ActiveRecord::RecordNotFound
@@ -251,7 +230,7 @@ private
# Finds the requested page and returns a 404 error if it doesn't exist
def find_existing_page
@page = @wiki.find_page(params[:id])
@page = @wiki.find_page(params[:page])
render_404 if @page.nil?
end
@@ -266,14 +245,4 @@ private
extend helper unless self.instance_of?(helper)
helper.instance_method(:initial_page_content).bind(self).call(page)
end
# eager load information about last updates, without loading text
def load_pages_grouped_by_date_without_content
@pages = @wiki.pages.find :all, :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
:joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id",
:order => 'title'
@pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
@pages_by_parent_id = @pages.group_by(&:parent_id)
end
end

View File

@@ -20,4 +20,12 @@ module AdminHelper
options_for_select([[l(:label_all), ''],
[l(:status_active), 1]], selected)
end
def css_project_classes(project)
s = 'project'
s << ' root' if project.root?
s << ' child' if project.child?
s << (project.leaf? ? ' leaf' : ' parent')
s
end
end

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2010 Jean-Philippe Lang
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -32,11 +32,6 @@ module ApplicationHelper
end
# Display a link if user is authorized
#
# @param [String] name Anchor text (passed to link_to)
# @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
# @param [optional, Hash] html_options Options passed to link_to
# @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
end
@@ -104,27 +99,8 @@ module ApplicationHelper
# * :text - Link text (default to the formatted revision)
def link_to_revision(revision, project, options={})
text = options.delete(:text) || format_revision(revision)
rev = revision.respond_to?(:identifier) ? revision.identifier : revision
link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
:title => l(:label_revision_id, format_revision(revision)))
end
# Generates a link to a project if active
# Examples:
#
# link_to_project(project) # => link to the specified project overview
# link_to_project(project, :action=>'settings') # => link to project settings
# link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
# link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
#
def link_to_project(project, options={}, html_options = nil)
if project.active?
url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
link_to(h(project), url, html_options)
else
h(project)
end
link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
end
def toggle_link(name, id, options={})
@@ -179,7 +155,7 @@ module ApplicationHelper
content << "<ul class=\"pages-hierarchy\">\n"
pages[node].each do |page|
content << "<li>"
content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
:title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
content << "</li>\n"
@@ -240,10 +216,15 @@ module ApplicationHelper
end
# Yields the given block for each project with its level in the tree
#
# Wrapper for Project#project_tree
def project_tree(projects, &block)
Project.project_tree(projects, &block)
ancestors = []
projects.sort_by(&:lft).each do |project|
while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
ancestors.pop
end
yield project, ancestors.size
ancestors << project
end
end
def project_nested_ul(projects, &block)
@@ -304,7 +285,7 @@ module ApplicationHelper
def time_tag(time)
text = distance_of_time_in_words(Time.now, time)
if @project
link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
link_to(text, {:controller => 'projects', :action => 'activity', :id => @project, :from => time.to_date}, :title => format_time(time))
else
content_tag('acronym', text, :title => format_time(time))
end
@@ -387,12 +368,12 @@ module ApplicationHelper
ancestors = (@project.root? ? [] : @project.ancestors.visible)
if ancestors.any?
root = ancestors.shift
b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
b << link_to(h(root), {:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item}, :class => 'root')
if ancestors.size > 2
b << '&#8230;'
ancestors = ancestors[-2, 2]
end
b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
b += ancestors.collect {|p| link_to(h(p), {:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item}, :class => 'ancestor') }
end
b << h(@project)
b.join(' &#187; ')
@@ -412,19 +393,6 @@ module ApplicationHelper
end
end
# Returns the theme, controller name, and action as css classes for the
# HTML body.
def body_css_classes
css = []
if theme = Redmine::Themes.theme(Setting.ui_theme)
css << 'theme-' + theme.name
end
css << 'controller-' + params[:controller]
css << 'action-' + params[:action]
css.join(' ')
end
def accesskey(s)
Redmine::AccessKeys.key_for s
end
@@ -451,19 +419,12 @@ module ApplicationHelper
only_path = options.delete(:only_path) == false ? false : true
text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
@parsed_headings = []
text = parse_non_pre_blocks(text) do |text|
[:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
parse_non_pre_blocks(text) do |text|
[:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
send method_name, text, project, obj, attr, only_path, options
end
end
if @parsed_headings.any?
replace_toc(text, @parsed_headings)
end
text
end
def parse_non_pre_blocks(text)
@@ -533,7 +494,7 @@ module ApplicationHelper
esc, all, page, title = $1, $2, $3, $5
if esc.nil?
if page =~ /^([^\:]+)\:(.*)$/
link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
page = $2
title ||= $1 if page.blank?
end
@@ -550,8 +511,7 @@ module ApplicationHelper
when :local; "#{title}.html"
when :anchor; "##{title}" # used for single-file wiki export
else
wiki_page_id = page.present? ? Wiki.titleize(page) : nil
url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => link_project, :page => Wiki.titleize(page), :anchor => anchor)
end
link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
else
@@ -632,7 +592,8 @@ module ApplicationHelper
end
when 'project'
if p = Project.visible.find_by_id(oid)
link = link_to_project(p, {:only_path => only_path}, :class => 'project')
link = link_to h(p.name), {:only_path => only_path, :controller => 'projects', :action => 'show', :id => p},
:class => 'project'
end
end
elsif sep == ':'
@@ -674,7 +635,8 @@ module ApplicationHelper
end
when 'project'
if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
link = link_to_project(p, {:only_path => only_path}, :class => 'project')
link = link_to h(p.name), {:only_path => only_path, :controller => 'projects', :action => 'show', :id => p},
:class => 'project'
end
end
end
@@ -682,55 +644,6 @@ module ApplicationHelper
leading + (link || "#{prefix}#{sep}#{identifier}")
end
end
HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
# Headings and TOC
# Adds ids and links to headings unless options[:headings] is set to false
def parse_headings(text, project, obj, attr, only_path, options)
return if options[:headings] == false
text.gsub!(HEADING_RE) do
level, attrs, content = $1.to_i, $2, $3
item = strip_tags(content).strip
anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
@parsed_headings << [level, anchor, item]
"<h#{level} #{attrs} id=\"#{anchor}\">#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
end
end
TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
# Renders the TOC with given headings
def replace_toc(text, headings)
text.gsub!(TOC_RE) do
if headings.empty?
''
else
div_class = 'toc'
div_class << ' right' if $1 == '>'
div_class << ' left' if $1 == '<'
out = "<ul class=\"#{div_class}\"><li>"
root = headings.map(&:first).min
current = root
started = false
headings.each do |level, anchor, item|
if level > current
out << '<ul><li>' * (level - current)
elsif level < current
out << "</li></ul>\n" * (current - level) + "</li><li>"
elsif started
out << '</li><li>'
end
out << "<a href=\"##{anchor}\">#{item}</a>"
current = level
started = true
end
out << '</li></ul>' * (current - root)
out << '</li></ul>'
end
end
end
# Same as Rails' simple_format helper without using paragraphs
def simple_format_without_paragraph(text)
@@ -796,11 +709,6 @@ module ApplicationHelper
javascript_include_tag('context_menu') +
stylesheet_link_tag('context_menu')
end
if l(:direction) == 'rtl'
content_for :header_tags do
stylesheet_link_tag('context_menu_rtl')
end
end
@context_menu_included = true
end
javascript_tag "new ContextMenu('#{ url_for(url) }')"
@@ -864,7 +772,7 @@ module ApplicationHelper
# +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
def avatar(user, options = { })
if Setting.gravatar_enabled?
options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
options.merge!({:ssl => Setting.protocol == 'https', :default => Setting.gravatar_default})
email = nil
if user.respond_to?(:mail)
email = user.mail
@@ -872,37 +780,9 @@ module ApplicationHelper
email = $1
end
return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
else
''
end
end
def favicon
"<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
end
# Returns true if arg is expected in the API response
def include_in_api_response?(arg)
unless @included_in_api_response
param = params[:include]
@included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
@included_in_api_response.collect!(&:strip)
end
@included_in_api_response.include?(arg.to_s)
end
# Returns options or nil if nometa param or X-Redmine-Nometa header
# was set in the request
def api_meta(options)
if params[:nometa].present? || request.headers['X-Redmine-Nometa']
# compatibility mode for activeresource clients that raise
# an error when unserializing an array with attributes
nil
else
options
end
end
private
def wiki_helper

View File

@@ -1,45 +0,0 @@
module CalendarsHelper
def link_to_previous_month(year, month, options={})
target_year, target_month = if month == 1
[year - 1, 12]
else
[year, month - 1]
end
name = if target_month == 12
"#{month_name(target_month)} #{target_year}"
else
"#{month_name(target_month)}"
end
link_to_month(('&#171; ' + name), target_year, target_month, options)
end
def link_to_next_month(year, month, options={})
target_year, target_month = if month == 12
[year + 1, 1]
else
[year, month + 1]
end
name = if target_month == 1
"#{month_name(target_month)} #{target_year}"
else
"#{month_name(target_month)}"
end
link_to_month((name + ' &#187;'), target_year, target_month, options)
end
def link_to_month(link_name, year, month, options={})
project_id = options[:project].present? ? options[:project].to_param : nil
link_target = calendar_path(:year => year, :month => month, :project_id => project_id)
link_to_remote(link_name,
{:update => "content", :url => link_target, :method => :put},
{:href => link_target})
end
end

View File

@@ -104,15 +104,4 @@ module CustomFieldsHelper
def custom_field_formats_for_select
Redmine::CustomFieldFormat.as_select
end
# Renders the custom_values in api views
def render_api_custom_values(custom_values, api)
api.array :custom_fields do
custom_values.each do |custom_value|
api.custom_field :id => custom_value.custom_field_id, :name => custom_value.custom_field.name do
api.value custom_value.value
end
end
end unless custom_values.empty?
end
end

View File

@@ -1,49 +0,0 @@
# redMine - project management software
# Copyright (C) 2006 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 GanttHelper
def gantt_zoom_link(gantt, in_or_out)
case in_or_out
when :in
if gantt.zoom < 4
link_to_remote(l(:text_zoom_in),
{:url => gantt.params.merge(:zoom => (gantt.zoom+1)), :method => :get, :update => 'content'},
{:href => url_for(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')
end
when :out
if gantt.zoom > 1
link_to_remote(l(:text_zoom_out),
{:url => gantt.params.merge(:zoom => (gantt.zoom-1)), :method => :get, :update => 'content'},
{:href => url_for(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')
end
end
end
def number_of_issues_on_versions(gantt)
versions = gantt.events.collect {|event| (event.is_a? Version) ? event : nil}.compact
versions.sum {|v| v.fixed_issues.for_gantt.with_query(@query).count}
end
end

View File

@@ -1,2 +0,0 @@
module IssueMovesHelper
end

View File

@@ -28,27 +28,14 @@ module IssuesHelper
ancestors << issue unless issue.leaf?
end
end
# Renders a HTML/CSS tooltip
#
# To use, a trigger div is needed. This is a div with the class of "tooltip"
# that contains this method wrapped in a span with the class of "tip"
#
# <div class="tooltip"><%= link_to_issue(issue) %>
# <span class="tip"><%= render_issue_tooltip(issue) %></span>
# </div>
#
def render_issue_tooltip(issue)
@cached_label_status ||= l(:field_status)
@cached_label_start_date ||= l(:field_start_date)
@cached_label_due_date ||= l(:field_due_date)
@cached_label_assigned_to ||= l(:field_assigned_to)
@cached_label_priority ||= l(:field_priority)
@cached_label_project ||= l(:field_project)
link_to_issue(issue) + "<br /><br />" +
"<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />" +
"<strong>#{@cached_label_status}</strong>: #{issue.status.name}<br />" +
"<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
"<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
"<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
@@ -189,20 +176,6 @@ module IssuesHelper
end
end
# Renders issue children recursively
def render_api_issue_children(issue, api)
return if issue.leaf?
api.array :children do
issue.children.each do |child|
api.issue(:id => child.id) do
api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
api.subject child.subject
render_api_issue_children(child, api)
end
end
end
end
def issues_to_csv(issues, project = nil)
ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
decimal_separator = l(:general_csv_decimal_separator)
@@ -260,4 +233,30 @@ module IssuesHelper
end
export
end
def gantt_zoom_link(gantt, in_or_out)
img_attributes = {:style => 'height:1.4em; width:1.4em; margin-left: 3px;'} # em for accessibility
case in_or_out
when :in
if gantt.zoom < 4
link_to_remote(l(:text_zoom_in) + image_tag('zoom_in.png', img_attributes.merge(:alt => l(:text_zoom_in))),
{:url => gantt.params.merge(:zoom => (gantt.zoom+1)), :update => 'content'},
{:href => url_for(gantt.params.merge(:zoom => (gantt.zoom+1)))})
else
l(:text_zoom_in) +
image_tag('zoom_in_g.png', img_attributes.merge(:alt => l(:text_zoom_in)))
end
when :out
if gantt.zoom > 1
link_to_remote(l(:text_zoom_out) + image_tag('zoom_out.png', img_attributes.merge(:alt => l(:text_zoom_out))),
{:url => gantt.params.merge(:zoom => (gantt.zoom-1)), :update => 'content'},
{:href => url_for(gantt.params.merge(:zoom => (gantt.zoom-1)))})
else
l(:text_zoom_out) +
image_tag('zoom_out_g.png', img_attributes.merge(:alt => l(:text_zoom_out)))
end
end
end
end

View File

@@ -22,7 +22,7 @@ module JournalsHelper
links = []
if !journal.notes.blank?
links << link_to_remote(image_tag('comment.png'),
{ :url => {:controller => 'journals', :action => 'new', :id => issue, :journal_id => journal} },
{ :url => {:controller => 'issues', :action => 'reply', :id => issue, :journal_id => journal} },
: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 },

View File

@@ -72,7 +72,7 @@ module ProjectsHelper
end
classes = (ancestors.empty? ? 'root' : 'child')
s << "<li class='#{classes}'><div class='#{classes}'>" +
link_to_project(project, {}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}")
link_to(h(project), {:controller => 'projects', :action => 'show', :id => project}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}")
s << "<div class='wiki description'>#{textilizable(project.short_description, :project => project)}</div>" unless project.description.blank?
s << "</div>\n"
ancestors << project

View File

@@ -50,7 +50,7 @@ module QueriesHelper
when 'User'
link_to_user value
when 'Project'
link_to_project value
link_to(h(value), :controller => 'projects', :action => 'show', :id => value)
when 'Version'
link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
when 'TrueClass'
@@ -78,9 +78,10 @@ module QueriesHelper
# Give it a name, required to be valid
@query = Query.new(:name => "_")
@query.project = @project
if params[:fields]
@query.filters = {}
@query.add_filters(params[:fields], params[:operators], params[:values])
if params[:fields] and params[:fields].is_a? Array
params[:fields].each do |field|
@query.add_filter(field, params[:operators][field], params[:values][field])
end
else
@query.available_filters.keys.each do |field|
@query.add_short_filter(field, params[field]) if params[field]

View File

@@ -18,12 +18,8 @@
require 'iconv'
module RepositoriesHelper
def format_revision(revision)
if revision.respond_to? :format_identifier
revision.format_identifier
else
revision.to_s
end
def format_revision(txt)
txt.to_s[0,8]
end
def truncate_at_line_break(text, length = 255)
@@ -91,7 +87,7 @@ module RepositoriesHelper
:action => 'show',
:id => @project,
:path => path_param,
:rev => @changeset.identifier)
:rev => @changeset.revision)
output << "<li class='#{style}'>#{text}</li>"
output << render_changes_tree(s)
elsif c = tree[file][:c]
@@ -101,13 +97,13 @@ module RepositoriesHelper
:action => 'entry',
:id => @project,
:path => path_param,
:rev => @changeset.identifier) unless c.action == 'D'
:rev => @changeset.revision) unless c.action == 'D'
text << " - #{c.revision}" unless c.revision.blank?
text << ' (' + link_to('diff', :controller => 'repositories',
:action => 'diff',
:id => @project,
:path => path_param,
:rev => @changeset.identifier) + ') ' if c.action == 'M'
:rev => @changeset.revision) + ') ' if c.action == 'M'
text << ' ' + content_tag('span', c.from_path, :class => 'copied-from') unless c.from_path.blank?
output << "<li class='#{style}'>#{text}</li>"
end

View File

@@ -39,7 +39,7 @@ module SearchHelper
end
def type_label(t)
l("label_#{t.singularize}_plural", :default => t.to_s.humanize)
l("label_#{t.singularize}_plural")
end
def project_select_tag

View File

@@ -71,14 +71,4 @@ module SettingsHelper
label = options.delete(:label)
label != false ? content_tag("label", l(label || "setting_#{setting}")) : ''
end
# Renders a notification field for a Redmine::Notifiable option
def notification_field(notifiable)
return content_tag(:label,
check_box_tag('settings[notified_events][]',
notifiable.name,
Setting.notified_events.include?(notifiable.name)) +
l_or_humanize(notifiable.name, :prefix => 'label_'),
:class => notifiable.parent.present? ? "parent" : '')
end
end

View File

@@ -33,19 +33,15 @@ module UsersHelper
options
end
def user_mail_notification_options(user)
user.valid_notification_options.collect {|o| [l(o.last), o.first]}
end
def change_status_link(user)
url = {:controller => 'users', :action => 'update', :id => user, :page => params[:page], :status => params[:status], :tab => nil}
url = {:controller => 'users', :action => 'edit', :id => user, :page => params[:page], :status => params[:status], :tab => nil}
if user.locked?
link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock'
link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock'
elsif user.registered?
link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock'
link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock'
elsif user != User.current
link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :put, :class => 'icon icon-lock'
link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :post, :class => 'icon icon-lock'
end
end

View File

@@ -24,7 +24,7 @@ module WikiHelper
attrs << " selected='selected'" if selected == page
indent = (level > 0) ? ('&nbsp;' * level * 2 + '&#187; ') : nil
s << "<option #{attrs}>#{indent}#{h page.pretty_title}</option>\n" +
s << "<option value='#{page.id}'>#{indent}#{h page.pretty_title}</option>\n" +
wiki_page_options_for_select(pages, selected, page, level + 1)
end
s

View File

@@ -18,7 +18,7 @@
class Board < ActiveRecord::Base
belongs_to :project
has_many :topics, :class_name => 'Message', :conditions => "#{Message.table_name}.parent_id IS NULL", :order => "#{Message.table_name}.created_on DESC"
has_many :messages, :dependent => :destroy, :order => "#{Message.table_name}.created_on DESC"
has_many :messages, :dependent => :delete_all, :order => "#{Message.table_name}.created_on DESC"
belongs_to :last_message, :class_name => 'Message', :foreign_key => :last_message_id
acts_as_list :scope => :project_id
acts_as_watchable

View File

@@ -19,13 +19,12 @@ class Change < ActiveRecord::Base
belongs_to :changeset
validates_presence_of :changeset_id, :action, :path
before_save :init_path
def relative_path
changeset.repository.relative_path(path)
end
def init_path
self.path ||= ""
def before_save
path ||= ""
end
end

View File

@@ -23,10 +23,10 @@ class Changeset < ActiveRecord::Base
has_many :changes, :dependent => :delete_all
has_and_belongs_to_many :issues
acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.format_identifier}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
:description => :long_comments,
:datetime => :committed_on,
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.identifier}}
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.revision}}
acts_as_searchable :columns => 'comments',
:include => {:repository => :project},
@@ -47,15 +47,6 @@ class Changeset < ActiveRecord::Base
def revision=(r)
write_attribute :revision, (r.nil? ? nil : r.to_s)
end
# Returns the identifier of this changeset; depending on repository backends
def identifier
if repository.class.respond_to? :changeset_identifier
repository.class.changeset_identifier self
else
revision.to_s
end
end
def comments=(comment)
write_attribute(:comments, Changeset.normalize_comments(comment))
@@ -65,15 +56,6 @@ class Changeset < ActiveRecord::Base
self.commit_date = date
super
end
# Returns the readable identifier
def format_identifier
if repository.class.respond_to? :format_changeset_identifier
repository.class.format_changeset_identifier self
else
identifier
end
end
def committer=(arg)
write_attribute(:committer, self.class.to_utf8(arg.to_s))
@@ -94,43 +76,54 @@ class Changeset < ActiveRecord::Base
def after_create
scan_comment_for_issue_ids
end
TIMELOG_RE = /
(
((\d+)(h|hours?))((\d+)(m|min)?)?
|
((\d+)(h|hours?|m|min))
|
(\d+):(\d+)
|
(\d+([\.,]\d+)?)h?
)
/x
require 'pp'
def scan_comment_for_issue_ids
return if comments.blank?
# keywords used to reference issues
ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
ref_keywords_any = ref_keywords.delete('*')
# keywords used to fix issues
fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
return if kw_regexp.blank?
referenced_issues = []
comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
action, refs = match[2], match[3]
next unless action.present? || ref_keywords_any
refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
if issue
referenced_issues << issue
fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
if ref_keywords.delete('*')
# find any issue ID in the comments
target_issue_ids = []
comments.scan(%r{([\s\(\[,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
referenced_issues += find_referenced_issues_by_id(target_issue_ids)
end
comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
action = match[0]
target_issue_ids = match[1].scan(/\d+/)
target_issues = find_referenced_issues_by_id(target_issue_ids)
if fix_keywords.include?(action.downcase) && fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
# update status of issues
logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
target_issues.each do |issue|
# the issue may have been updated by the closure of another one (eg. duplicate)
issue.reload
# don't change the status is the issue is closed
next if issue.status.is_closed?
csettext = "r#{self.revision}"
if self.scmid && (! (csettext =~ /^r[0-9]+$/))
csettext = "commit:\"#{self.scmid}\""
end
journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, csettext))
issue.status = fix_status
unless Setting.commit_fix_done_ratio.blank?
issue.done_ratio = Setting.commit_fix_done_ratio.to_i
end
Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
{ :changeset => self, :issue => issue })
issue.save
end
end
referenced_issues += target_issues
end
referenced_issues.uniq!
@@ -144,14 +137,6 @@ class Changeset < ActiveRecord::Base
def long_comments
@long_comments || split_comments.last
end
def text_tag
if scmid?
"commit:#{scmid}"
else
"r#{revision}"
end
end
# Returns the previous changeset
def previous
@@ -179,64 +164,13 @@ class Changeset < ActiveRecord::Base
private
# Finds an issue that can be referenced by the commit message
# i.e. an issue that belong to the repository project, a subproject or a parent project
def find_referenced_issue_by_id(id)
return nil if id.blank?
issue = Issue.find_by_id(id.to_i, :include => :project)
if issue
unless project == issue.project || project.is_ancestor_of?(issue.project) || project.is_descendant_of?(issue.project)
issue = nil
end
end
issue
end
def fix_issue(issue)
status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
if status.nil?
logger.warn("No status macthes commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
return issue
end
# the issue may have been updated by the closure of another one (eg. duplicate)
issue.reload
# don't change the status is the issue is closed
return if issue.status && issue.status.is_closed?
journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag))
issue.status = status
unless Setting.commit_fix_done_ratio.blank?
issue.done_ratio = Setting.commit_fix_done_ratio.to_i
end
Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
{ :changeset => self, :issue => issue })
unless issue.save
logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
end
issue
end
def log_time(issue, hours)
time_entry = TimeEntry.new(
:user => user,
:hours => hours,
:issue => issue,
:spent_on => commit_date,
:comments => l(:text_time_logged_by_changeset, :value => text_tag, :locale => Setting.default_language)
)
time_entry.activity = log_time_activity unless log_time_activity.nil?
unless time_entry.save
logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
end
time_entry
end
def log_time_activity
if Setting.commit_logtime_activity_id.to_i > 0
TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
end
# Finds issues that can be referenced by the commit message
# i.e. issues that belong to the repository project, a subproject or a parent project
def find_referenced_issues_by_id(ids)
return [] if ids.compact.empty?
Issue.find_all_by_id(ids, :include => :project).select {|issue|
project == issue.project || project.is_ancestor_of?(issue.project) || project.is_descendant_of?(issue.project)
}
end
def split_comments

View File

@@ -34,10 +34,6 @@ class CustomValue < ActiveRecord::Base
custom_field.editable?
end
def visible?
custom_field.visible?
end
def required?
custom_field.is_required?
end

View File

@@ -31,7 +31,6 @@ class Group < Principal
def user_added(user)
members.each do |member|
next if member.project.nil?
user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id)
member.member_roles.each do |member_role|
user_member.member_roles << MemberRole.new(:role => member_role.role, :inherited_from => member_role.id)

View File

@@ -16,8 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Issue < ActiveRecord::Base
include Redmine::SafeAttributes
belongs_to :project
belongs_to :tracker
belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
@@ -64,31 +62,14 @@ class Issue < ActiveRecord::Base
named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
named_scope :recently_updated, :order => "#{self.table_name}.updated_on DESC"
named_scope :with_limit, lambda { |limit| { :limit => limit} }
named_scope :on_active_project, :include => [:status, :project, :tracker],
:conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
named_scope :for_gantt, lambda {
{
:include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version]
}
}
named_scope :without_version, lambda {
{
:conditions => { :fixed_version_id => nil}
}
}
named_scope :with_query, lambda {|query|
{
:conditions => Query.merge_conditions(query.statement)
}
}
before_create :default_assign
before_save :close_duplicates, :update_done_ratio_from_issue_status
after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
before_save :reschedule_following_issues, :close_duplicates, :update_done_ratio_from_issue_status
after_save :update_nested_set_attributes, :update_parent_attributes, :create_journal
after_destroy :destroy_children
after_destroy :update_parent_attributes
@@ -216,47 +197,32 @@ class Issue < ActiveRecord::Base
write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
end
safe_attributes 'tracker_id',
'status_id',
'parent_issue_id',
'category_id',
'assigned_to_id',
'priority_id',
'fixed_version_id',
'subject',
'description',
'start_date',
'due_date',
'done_ratio',
'estimated_hours',
'custom_field_values',
'custom_fields',
'lock_version',
:if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
SAFE_ATTRIBUTES = %w(
tracker_id
status_id
parent_issue_id
category_id
assigned_to_id
priority_id
fixed_version_id
subject
description
start_date
due_date
done_ratio
estimated_hours
custom_field_values
lock_version
) unless const_defined?(:SAFE_ATTRIBUTES)
safe_attributes 'status_id',
'assigned_to_id',
'fixed_version_id',
'done_ratio',
:if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
# Safely sets attributes
# Should be called from controllers instead of #attributes=
# attr_accessible is too rough because we still want things like
# Issue.new(:project => foo) to work
# TODO: move workflow/permission checks from controllers to here
def safe_attributes=(attrs, user=User.current)
return unless attrs.is_a?(Hash)
# User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
attrs = delete_unsafe_attributes(attrs, user)
return if attrs.empty?
# Tracker must be set before since new_statuses_allowed_to depends on it.
if t = attrs.delete('tracker_id')
self.tracker_id = t
end
return if attrs.nil?
attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES.include?(k)}
if attrs['status_id']
unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
attrs.delete('status_id')
@@ -271,7 +237,7 @@ class Issue < ActiveRecord::Base
if !user.allowed_to?(:manage_subtasks, project)
attrs.delete('parent_issue_id')
elsif !attrs['parent_issue_id'].blank?
attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'])
end
end
@@ -279,7 +245,7 @@ class Issue < ActiveRecord::Base
end
def done_ratio
if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
status.default_done_ratio
else
read_attribute(:done_ratio)
@@ -342,7 +308,7 @@ class Issue < ActiveRecord::Base
# Set the done_ratio using the status if that setting is set. This will keep the done_ratios
# even if the user turns off the setting later
def update_done_ratio_from_issue_status
if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
self.done_ratio = status.default_done_ratio
end
end
@@ -391,24 +357,10 @@ class Issue < ActiveRecord::Base
def overdue?
!due_date.nil? && (due_date < Date.today) && !status.is_closed?
end
# Is the amount of work done less than it should for the due date
def behind_schedule?
return false if start_date.nil? || due_date.nil?
done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
return done_date <= Date.today
end
# Does this issue have children?
def children?
!leaf?
end
# Users the issue can be assigned to
def assignable_users
users = project.assignable_users
users << author if author
users.uniq.sort
project.assignable_users
end
# Versions that the issue can be assigned to
@@ -433,10 +385,9 @@ class Issue < ActiveRecord::Base
# Returns the mail adresses of users that should be notified
def recipients
notified = project.notified_users
# Author and assignee are always notified unless they have been
# locked or don't want to be notified
notified << author if author && author.active? && author.notify_about?(self)
notified << assigned_to if assigned_to && assigned_to.active? && assigned_to.notify_about?(self)
# Author and assignee are always notified unless they have been locked
notified << author if author && author.active?
notified << assigned_to if assigned_to && assigned_to.active?
notified.uniq!
# Remove users that can not view the issue
notified.reject! {|user| !visible?(user)}
@@ -733,7 +684,7 @@ class Issue < ActiveRecord::Base
end
# done ratio = weighted average ratio of leaves
unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio?
leaves_count = p.leaves.count
if leaves_count > 0
average = p.leaves.average(:estimated_hours).to_f
@@ -870,7 +821,7 @@ class Issue < ActiveRecord::Base
j.id as #{select_field},
count(i.id) as total
from
#{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j
#{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} as j
where
i.status_id=s.id
and #{where}

View File

@@ -84,15 +84,14 @@ class IssueRelation < ActiveRecord::Base
def set_issue_to_dates
soonest_start = self.successor_soonest_start
if soonest_start && issue_to
if soonest_start
issue_to.reschedule_after(soonest_start)
end
end
def successor_soonest_start
if (TYPE_PRECEDES == self.relation_type) && delay && issue_from && (issue_from.start_date || issue_from.due_date)
(issue_from.due_date || issue_from.start_date) + 1 + delay
end
return nil unless (TYPE_PRECEDES == self.relation_type) && (issue_from.start_date || issue_from.due_date)
(issue_from.due_date || issue_from.start_date) + 1 + delay
end
def <=>(relation)

View File

@@ -17,10 +17,8 @@
class IssueStatus < ActiveRecord::Base
before_destroy :check_integrity
has_many :workflows, :foreign_key => "old_status_id"
has_many :workflows, :foreign_key => "old_status_id", :dependent => :delete_all
acts_as_list
before_destroy :delete_workflows
validates_presence_of :name
validates_uniqueness_of :name
@@ -91,9 +89,4 @@ private
def check_integrity
raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id])
end
# Deletes associated workflows
def delete_workflows
Workflow.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}])
end
end

View File

@@ -65,12 +65,4 @@ class Journal < ActiveRecord::Base
def attachments
journalized.respond_to?(:attachments) ? journalized.attachments : nil
end
# Returns a string of css classes
def css_classes
s = 'journal'
s << ' has-notes' unless notes.blank?
s << ' has-details' unless details.blank?
s
end
end

View File

@@ -17,11 +17,6 @@
class JournalObserver < ActiveRecord::Observer
def after_create(journal)
if Setting.notified_events.include?('issue_updated') ||
(Setting.notified_events.include?('issue_note_added') && journal.notes.present?) ||
(Setting.notified_events.include?('issue_status_updated') && journal.new_status.present?) ||
(Setting.notified_events.include?('issue_priority_updated') && journal.new_value_for('priority_id').present?)
Mailer.deliver_issue_edit(journal)
end
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
end
end

View File

@@ -17,7 +17,6 @@
class MailHandler < ActionMailer::Base
include ActionView::Helpers::SanitizeHelper
include Redmine::I18n
class UnauthorizedAction < StandardError; end
class MissingInformation < StandardError; end
@@ -100,7 +99,7 @@ class MailHandler < ActionMailer::Base
elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
receive_message_reply(m[1].to_i)
else
dispatch_to_default
receive_issue
end
rescue ActiveRecord::RecordInvalid => e
# TODO: send a email to the user
@@ -113,28 +112,40 @@ class MailHandler < ActionMailer::Base
logger.error "MailHandler: unauthorized attempt from #{user}" if logger
false
end
def dispatch_to_default
receive_issue
end
# Creates a new issue
def receive_issue
project = target_project
tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
priority = (get_keyword(:priority) && IssuePriority.find_by_name(get_keyword(:priority)))
status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
assigned_to = (get_keyword(:assigned_to, :override => true) && find_user_from_keyword(get_keyword(:assigned_to, :override => true)))
due_date = get_keyword(:due_date, :override => true)
start_date = get_keyword(:start_date, :override => true)
# check permission
unless @@handler_options[:no_permission_check]
raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
end
issue = Issue.new(:author => user, :project => project)
issue.safe_attributes = issue_attributes_from_keywords(issue)
issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
issue.subject = email.subject.to_s.chomp[0,255]
issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority, :due_date => due_date, :start_date => start_date, :assigned_to => assigned_to)
# check workflow
if status && issue.new_statuses_allowed_to(user).include?(status)
issue.status = status
end
issue.subject = email.subject.chomp[0,255]
if issue.subject.blank?
issue.subject = '(no subject)'
end
# custom fields
issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
if value = get_keyword(c.name, :override => true)
h[c.id] = value
end
h
end
issue.description = cleaned_up_text_body
# add To and Cc as watchers before saving so the watchers can reply to Redmine
add_watchers(issue)
issue.save!
@@ -143,22 +154,41 @@ class MailHandler < ActionMailer::Base
issue
end
def target_project
# TODO: other ways to specify project:
# * parse the email To field
# * specific project (eg. Setting.mail_handler_target_project)
target = Project.find_by_identifier(get_keyword(:project))
raise MissingInformation.new('Unable to determine target project') if target.nil?
target
end
# Adds a note to an existing issue
def receive_issue_reply(issue_id)
status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
due_date = get_keyword(:due_date, :override => true)
start_date = get_keyword(:start_date, :override => true)
assigned_to = (get_keyword(:assigned_to, :override => true) && find_user_from_keyword(get_keyword(:assigned_to, :override => true)))
issue = Issue.find_by_id(issue_id)
return unless issue
# check permission
unless @@handler_options[:no_permission_check]
raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
end
# ignore CLI-supplied defaults for new issues
@@handler_options[:issue].clear
# add the note
journal = issue.init_journal(user, cleaned_up_text_body)
issue.safe_attributes = issue_attributes_from_keywords(issue)
issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
add_attachments(issue)
# check workflow
if status && issue.new_statuses_allowed_to(user).include?(status)
issue.status = status
end
issue.start_date = start_date if start_date
issue.due_date = due_date if due_date
issue.assigned_to = assigned_to if assigned_to
issue.save!
logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
journal
@@ -225,8 +255,8 @@ class MailHandler < ActionMailer::Base
@keywords[attr]
else
@keywords[attr] = begin
if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && (v = extract_keyword!(plain_text_body, attr, options[:format]))
v
if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr.to_s.humanize}[ \t]*:[ \t]*(.+)\s*$/i, '')
$1.strip
elsif !@@handler_options[:issue][attr].blank?
@@handler_options[:issue][attr]
end
@@ -234,65 +264,6 @@ class MailHandler < ActionMailer::Base
end
end
# Destructively extracts the value for +attr+ in +text+
# Returns nil if no matching keyword found
def extract_keyword!(text, attr, format=nil)
keys = [attr.to_s.humanize]
if attr.is_a?(Symbol)
keys << l("field_#{attr}", :default => '', :locale => user.language) if user
keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
end
keys.reject! {|k| k.blank?}
keys.collect! {|k| Regexp.escape(k)}
format ||= '.+'
text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i, '')
$2 && $2.strip
end
def target_project
# TODO: other ways to specify project:
# * parse the email To field
# * specific project (eg. Setting.mail_handler_target_project)
target = Project.find_by_identifier(get_keyword(:project))
raise MissingInformation.new('Unable to determine target project') if target.nil?
target
end
# Returns a Hash of issue attributes extracted from keywords in the email body
def issue_attributes_from_keywords(issue)
assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_user_from_keyword(k)
assigned_to = nil if assigned_to && !issue.assignable_users.include?(assigned_to)
attrs = {
'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.find_by_name(k).try(:id),
'status_id' => (k = get_keyword(:status)) && IssueStatus.find_by_name(k).try(:id),
'priority_id' => (k = get_keyword(:priority)) && IssuePriority.find_by_name(k).try(:id),
'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.find_by_name(k).try(:id),
'assigned_to_id' => assigned_to.try(:id),
'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && issue.project.shared_versions.find_by_name(k).try(:id),
'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
'estimated_hours' => get_keyword(:estimated_hours, :override => true),
'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
}.delete_if {|k, v| v.blank? }
if issue.new_record? && attrs['tracker_id'].nil?
attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
end
attrs
end
# Returns a Hash of issue custom field values extracted from keywords in the email body
def custom_field_values_from_keywords(customized)
customized.custom_field_values.inject({}) do |h, v|
if value = get_keyword(v.custom_field.name, :override => true)
h[v.custom_field.id.to_s] = value
end
h
end
end
# Returns the text/plain part of the email
# If not found (eg. HTML-only email), returns the body with tags removed
def plain_text_body
@@ -347,7 +318,7 @@ class MailHandler < ActionMailer::Base
def cleanup_body(body)
delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
unless delimiters.empty?
regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
regex = Regexp.new("^(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
body = body.gsub(regex, '')
end
body.strip

View File

@@ -15,8 +15,6 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'ar_condition'
class Mailer < ActionMailer::Base
layout 'mailer'
helper :application
@@ -82,7 +80,7 @@ class Mailer < ActionMailer::Base
def reminder(user, issues, days)
set_language_if_valid user.language
recipients user.mail
subject l(:mail_subject_reminder, :count => issues.size, :days => days)
subject l(:mail_subject_reminder, issues.size)
body :issues => issues,
:days => days,
:issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc')
@@ -178,9 +176,9 @@ class Mailer < ActionMailer::Base
message_id wiki_content
recipients wiki_content.recipients
cc(wiki_content.page.wiki.watcher_recipients - recipients)
subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :id => wiki_content.page.pretty_title)}"
subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :page => wiki_content.page.pretty_title)}"
body :wiki_content => wiki_content,
:wiki_content_url => url_for(:controller => 'wiki', :action => 'show', :project_id => wiki_content.project, :id => wiki_content.page.title)
:wiki_content_url => url_for(:controller => 'wiki', :action => 'index', :id => wiki_content.project, :page => wiki_content.page.title)
render_multipart('wiki_content_added', body)
end
@@ -195,10 +193,10 @@ class Mailer < ActionMailer::Base
message_id wiki_content
recipients wiki_content.recipients
cc(wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients)
subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :id => wiki_content.page.pretty_title)}"
subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :page => wiki_content.page.pretty_title)}"
body :wiki_content => wiki_content,
:wiki_content_url => url_for(:controller => 'wiki', :action => 'show', :project_id => wiki_content.project, :id => wiki_content.page.title),
:wiki_diff_url => url_for(:controller => 'wiki', :action => 'diff', :project_id => wiki_content.project, :id => wiki_content.page.title, :version => wiki_content.version)
:wiki_content_url => url_for(:controller => 'wiki', :action => 'index', :id => wiki_content.project, :page => wiki_content.page.title),
:wiki_diff_url => url_for(:controller => 'wiki', :action => 'diff', :id => wiki_content.project, :page => wiki_content.page.title, :version => wiki_content.version)
render_multipart('wiki_content_updated', body)
end
@@ -308,16 +306,13 @@ class Mailer < ActionMailer::Base
# * :days => how many days in the future to remind about (defaults to 7)
# * :tracker => id of tracker for filtering issues (defaults to all trackers)
# * :project => id or identifier of project to process (defaults to all projects)
# * :users => array of user ids who should be reminded
def self.reminders(options={})
days = options[:days] || 7
project = options[:project] ? Project.find(options[:project]) : nil
tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
user_ids = options[:users]
s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date]
s << "#{Issue.table_name}.assigned_to_id IS NOT NULL"
s << ["#{Issue.table_name}.assigned_to_id IN (?)", user_ids] if user_ids.present?
s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}"
s << "#{Issue.table_name}.project_id = #{project.id}" if project
s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker

View File

@@ -82,7 +82,7 @@ class Member < ActiveRecord::Base
protected
def validate
errors.add_on_empty :role if member_roles.empty? && roles.empty?
errors.add_to_base "Role can't be blank" if member_roles.empty? && roles.empty?
end
private

View File

@@ -29,11 +29,6 @@ class News < ActiveRecord::Base
acts_as_activity_provider :find_options => {:include => [:project, :author]},
:author_key => :author_id
named_scope :visible, lambda {|*args| {
:include => :project,
:conditions => Project.allowed_to_condition(args.first || User.current, :view_news)
}}
def visible?(user=User.current)
!user.nil? && user.allowed_to?(:view_news, project)
end

View File

@@ -33,11 +33,7 @@ class Principal < ActiveRecord::Base
}
before_create :set_default_empty_values
def name(formatter = nil)
to_s
end
def <=>(principal)
if self.class.name == principal.class.name
self.to_s.downcase <=> principal.to_s.downcase

View File

@@ -16,15 +16,10 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Project < ActiveRecord::Base
include Redmine::SafeAttributes
# Project statuses
STATUS_ACTIVE = 1
STATUS_ARCHIVED = 9
# Maximum length for project identifiers
IDENTIFIER_MAX_LENGTH = 100
# Specific overidden Activities
has_many :time_entry_activities
has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
@@ -66,14 +61,14 @@ class Project < ActiveRecord::Base
:url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
:author => nil
attr_protected :status
attr_protected :status, :enabled_module_names
validates_presence_of :name, :identifier
validates_uniqueness_of :identifier
validates_uniqueness_of :name, :identifier
validates_associated :repository, :wiki
validates_length_of :name, :maximum => 255
validates_length_of :name, :maximum => 30
validates_length_of :homepage, :maximum => 255
validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
validates_length_of :identifier, :in => 1..20
# donwcase letters, digits, dashes but not digits only
validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
# reserved words
@@ -86,24 +81,6 @@ class Project < ActiveRecord::Base
named_scope :all_public, { :conditions => { :is_public => true } }
named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
def initialize(attributes = nil)
super
initialized = (attributes || {}).stringify_keys
if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
self.identifier = Project.next_identifier
end
if !initialized.key?('is_public')
self.is_public = Setting.default_projects_public?
end
if !initialized.key?('enabled_module_names')
self.enabled_module_names = Setting.default_projects_modules
end
if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
self.trackers = Tracker.all
end
end
def identifier=(identifier)
super unless identifier_frozen?
end
@@ -243,10 +220,6 @@ class Project < ActiveRecord::Base
self.status == STATUS_ACTIVE
end
def archived?
self.status == STATUS_ARCHIVED
end
# Archives the project and its descendants
def archive
# Check that there is no issue of a non descendant project that is assigned
@@ -409,13 +382,12 @@ class Project < ActiveRecord::Base
# Returns the mail adresses of users that should be always notified on project events
def recipients
notified_users.collect {|user| user.mail}
members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
end
# Returns the users that should be notified on project events
def notified_users
# TODO: User part should be extracted to User#notify_about?
members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user}
end
# Returns an array of all custom fields enabled for project issues
@@ -440,54 +412,6 @@ class Project < ActiveRecord::Base
def short_description(length = 255)
description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
end
def css_classes
s = 'project'
s << ' root' if root?
s << ' child' if child?
s << (leaf? ? ' leaf' : ' parent')
s
end
# The earliest start date of a project, based on it's issues and versions
def start_date
[
issues.minimum('start_date'),
shared_versions.collect(&:effective_date),
shared_versions.collect(&:start_date)
].flatten.compact.min
end
# The latest due date of an issue or version
def due_date
[
issues.maximum('due_date'),
shared_versions.collect(&:effective_date),
shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
].flatten.compact.max
end
def overdue?
active? && !due_date.nil? && (due_date < Date.today)
end
# Returns the percent completed for this project, based on the
# progress on it's versions.
def completed_percent(options={:include_subprojects => false})
if options.delete(:include_subprojects)
total = self_and_descendants.collect(&:completed_percent).sum
total / self_and_descendants.count
else
if versions.count > 0
total = versions.collect(&:completed_pourcent).sum
total / versions.count
else
100
end
end
end
# Return true if this project is allowed to do the specified action.
# action can be:
@@ -508,7 +432,7 @@ class Project < ActiveRecord::Base
def enabled_module_names=(module_names)
if module_names && module_names.is_a?(Array)
module_names = module_names.collect(&:to_s).reject(&:blank?)
module_names = module_names.collect(&:to_s)
# remove disabled modules
enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
# add new modules
@@ -518,33 +442,6 @@ class Project < ActiveRecord::Base
end
end
# Returns an array of the enabled modules names
def enabled_module_names
enabled_modules.collect(&:name)
end
safe_attributes 'name',
'description',
'homepage',
'is_public',
'identifier',
'custom_field_values',
'custom_fields',
'tracker_ids',
'issue_custom_field_ids'
safe_attributes 'enabled_module_names',
:if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
# Returns an array of projects that are in this project's hierarchy
#
# Example: parents, children, siblings
def hierarchy
parents = project.self_and_ancestors || []
descendants = project.descendants || []
project_hierarchy = parents | descendants # Set union
end
# Returns an auto-generated project identifier based on the last identifier used
def self.next_identifier
p = Project.find(:first, :order => 'created_on DESC')
@@ -606,18 +503,6 @@ class Project < ActiveRecord::Base
return nil
end
end
# Yields the given block for each project with its level in the tree
def self.project_tree(projects, &block)
ancestors = []
projects.sort_by(&:lft).each do |project|
while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
ancestors.pop
end
yield project, ancestors.size
ancestors << project
end
end
private
@@ -703,20 +588,12 @@ class Project < ActiveRecord::Base
end
self.issues << new_issue
if new_issue.new_record?
logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
else
issues_map[issue.id] = new_issue unless new_issue.new_record?
end
issues_map[issue.id] = new_issue
end
# Relations after in case issues related each other
project.issues.each do |issue|
new_issue = issues_map[issue.id]
unless new_issue
# Issue was not copied
next
end
# Relations
issue.relations_from.each do |source_relation|
@@ -743,12 +620,7 @@ class Project < ActiveRecord::Base
# Copies members from +project+
def copy_members(project)
# Copy users first, then groups to handle members with inherited and given roles
members_to_copy = []
members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
members_to_copy.each do |member|
project.memberships.each do |member|
new_member = Member.new
new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
# only copy non inherited roles

View File

@@ -187,28 +187,14 @@ class Query < ActiveRecord::Base
if project
user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
else
all_projects = Project.visible.all
if all_projects.any?
# members of visible projects
user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort.collect{|s| [s.name, s.id.to_s] }
# project filter
project_values = []
Project.project_tree(all_projects) do |p, level|
prefix = (level > 0 ? ('--' * level + ' ') : '')
project_values << ["#{prefix}#{p.name}", p.id.to_s]
end
@available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
project_ids = User.current.projects.collect(&:id)
if project_ids.any?
# members of the user's projects
user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", project_ids]).sort.collect{|s| [s.name, s.id.to_s] }
end
end
@available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
@available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
@available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
@available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
if User.current.logged?
@available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
@@ -260,10 +246,8 @@ class Query < ActiveRecord::Base
# Add multiple filters using +add_filter+
def add_filters(fields, operators, values)
if fields.is_a?(Array) && operators.is_a?(Hash) && values.is_a?(Hash)
fields.each do |field|
add_filter(field, operators[field], values[field])
end
fields.each do |field|
add_filter(field, operators[field], values[field])
end
end
@@ -378,15 +362,15 @@ class Query < ActiveRecord::Base
# Returns true if the query is a grouped query
def grouped?
!group_by_column.nil?
!group_by.blank?
end
def group_by_column
groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
groupable_columns.detect {|c| c.name.to_s == group_by}
end
def group_by_statement
group_by_column.try(:groupable)
group_by_column.groupable
end
def project_statement
@@ -442,47 +426,6 @@ class Query < ActiveRecord::Base
db_field = 'user_id'
sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
elsif field == "member_of_group" # named field
if operator == '*' # Any group
groups = Group.all
operator = '=' # Override the operator since we want to find by assigned_to
elsif operator == "!*"
groups = Group.all
operator = '!' # Override the operator since we want to find by assigned_to
else
groups = Group.find_all_by_id(v)
end
groups ||= []
members_of_groups = groups.inject([]) {|user_ids, group|
if group && group.user_ids.present?
user_ids << group.user_ids
end
user_ids.flatten.uniq.compact
}.sort.collect(&:to_s)
sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
elsif field == "assigned_to_role" # named field
if operator == "*" # Any Role
roles = Role.givable
operator = '=' # Override the operator since we want to find by assigned_to
elsif operator == "!*" # No role
roles = Role.givable
operator = '!' # Override the operator since we want to find by assigned_to
else
roles = Role.givable.find_all_by_id(v)
end
roles ||= []
members_of_roles = roles.inject([]) {|user_ids, role|
if role && role.members
user_ids << role.members.collect(&:user_id)
end
user_ids.flatten.uniq.compact
}.sort.collect(&:to_s)
sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
else
# regular field
db_table = Issue.table_name
@@ -566,19 +509,9 @@ class Query < ActiveRecord::Base
sql = ''
case operator
when "="
if value.any?
sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
else
# IN an empty set
sql = "1=0"
end
sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
when "!"
if value.any?
sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
else
# NOT IN an empty set
sql = "1=1"
end
sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
when "!*"
sql = "#{db_table}.#{db_field} IS NULL"
sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter

View File

@@ -91,13 +91,12 @@ class Repository < ActiveRecord::Base
def relative_path(path)
path
end
# Finds and returns a revision with a number or the beginning of a hash
def find_changeset_by_name(name)
return nil if name.blank?
changesets.find(:first, :conditions => (name.match(/^\d*$/) ? ["revision = ?", name.to_s] : ["revision LIKE ?", name + '%']))
end
def latest_changeset
@latest_changeset ||= changesets.find(:first)
end

View File

@@ -29,16 +29,6 @@ class Repository::Git < Repository
'Git'
end
# Returns the identifier for the given git changeset
def self.changeset_identifier(changeset)
changeset.scmid
end
# Returns the readable identifier for the given git changeset
def self.format_changeset_identifier(changeset)
changeset.revision[0, 8]
end
def branches
scm.branches
end

View File

@@ -18,9 +18,6 @@
require 'redmine/scm/adapters/mercurial_adapter'
class Repository::Mercurial < Repository
# sort changesets by revision number
has_many :changesets, :order => "#{Changeset.table_name}.id DESC", :foreign_key => 'repository_id'
attr_protected :root_url
validates_presence_of :url
@@ -55,18 +52,6 @@ class Repository::Mercurial < Repository
entries
end
# Returns the latest changesets for +path+; sorted by revision number
def latest_changesets(path, rev, limit=10)
if path.blank?
changesets.find(:all, :include => :user, :limit => limit)
else
changes.find(:all, :include => {:changeset => :user},
:conditions => ["path = ?", path.with_leading_slash],
:order => "#{Changeset.table_name}.id DESC",
:limit => limit).collect(&:changeset)
end
end
def fetch_changesets
scm_info = scm.info
if scm_info

View File

@@ -27,7 +27,7 @@ class TimeEntry < ActiveRecord::Base
acts_as_customizable
acts_as_event :title => Proc.new {|o| "#{l_hours(o.hours)} (#{(o.issue || o.project).event_title})"},
:url => Proc.new {|o| {:controller => 'timelog', :action => 'index', :project_id => o.project, :issue_id => o.issue}},
:url => Proc.new {|o| {:controller => 'timelog', :action => 'details', :project_id => o.project, :issue_id => o.issue}},
:author => :user,
:description => :comments
@@ -66,9 +66,6 @@ class TimeEntry < ActiveRecord::Base
# these attributes make time aggregations easier
def spent_on=(date)
super
if spent_on.is_a?(Time)
self.spent_on = spent_on.to_date
end
self.tyear = spent_on ? spent_on.year : nil
self.tmonth = spent_on ? spent_on.month : nil
self.tweek = spent_on ? Date.civil(spent_on.year, spent_on.month, spent_on.day).cweek : nil
@@ -84,20 +81,4 @@ class TimeEntry < ActiveRecord::Base
yield
end
end
def self.earilest_date_for_project(project=nil)
finder_conditions = ARCondition.new(Project.allowed_to_condition(User.current, :view_time_entries))
if project
finder_conditions << ["project_id IN (?)", project.hierarchy.collect(&:id)]
end
TimeEntry.minimum(:spent_on, :include => :project, :conditions => finder_conditions.conditions)
end
def self.latest_date_for_project(project=nil)
finder_conditions = ARCondition.new(Project.allowed_to_condition(User.current, :view_time_entries))
if project
finder_conditions << ["project_id IN (?)", project.hierarchy.collect(&:id)]
end
TimeEntry.maximum(:spent_on, :include => :project, :conditions => finder_conditions.conditions)
end
end

View File

@@ -18,8 +18,7 @@
require "digest/sha1"
class User < Principal
include Redmine::SafeAttributes
# Account statuses
STATUS_ANONYMOUS = 0
STATUS_ACTIVE = 1
@@ -34,15 +33,6 @@ class User < Principal
:username => '#{login}'
}
MAIL_NOTIFICATION_OPTIONS = [
['all', :label_user_mail_option_all],
['selected', :label_user_mail_option_selected],
['only_my_events', :label_user_mail_option_only_my_events],
['only_assigned', :label_user_mail_option_only_assigned],
['only_owner', :label_user_mail_option_only_owner],
['none', :label_user_mail_option_none]
]
has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
:after_remove => Proc.new {|user, group| group.user_removed(user)}
has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
@@ -60,7 +50,7 @@ class User < Principal
attr_accessor :password, :password_confirmation
attr_accessor :last_before_login_on
# Prevents unauthorized assignments
attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
attr_protected :login, :admin, :password, :password_confirmation, :hashed_password, :group_ids
validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false
@@ -73,10 +63,9 @@ class User < Principal
validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
validates_length_of :mail, :maximum => 60, :allow_nil => true
validates_confirmation_of :password, :allow_nil => true
validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
def before_create
self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
self.mail_notification = false
true
end
@@ -90,10 +79,6 @@ class User < Principal
super
end
def mail=(arg)
write_attribute(:mail, arg.to_s.strip)
end
def identity_url=(url)
if url.blank?
write_attribute(:identity_url, '')
@@ -175,30 +160,6 @@ class User < Principal
self.status == STATUS_LOCKED
end
def activate
self.status = STATUS_ACTIVE
end
def register
self.status = STATUS_REGISTERED
end
def lock
self.status = STATUS_LOCKED
end
def activate!
update_attribute(:status, STATUS_ACTIVE)
end
def register!
update_attribute(:status, STATUS_REGISTERED)
end
def lock!
update_attribute(:status, STATUS_LOCKED)
end
def check_password?(clear_password)
if auth_source_id.present?
auth_source.authenticate(self.login, clear_password)
@@ -261,21 +222,6 @@ class User < Principal
notified_projects_ids
end
def valid_notification_options
self.class.valid_notification_options(self)
end
# Only users that belong to more than 1 project can select projects for which they are notified
def self.valid_notification_options(user=nil)
# Note that @user.membership.size would fail since AR ignores
# :include association option when doing a count
if user.nil? || user.memberships.length < 1
MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
else
MAIL_NOTIFICATION_OPTIONS
end
end
# Find a user account by matching the exact login and then a case-insensitive
# version. Exact matches will be given priority.
def self.find_by_login(login)
@@ -350,35 +296,23 @@ class User < Principal
!roles_for_project(project).detect {|role| role.member?}.nil?
end
# Return true if the user is allowed to do the specified action on a specific context
# Action can be:
# Return true if the user is allowed to do the specified action on project
# action can be:
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
# * a permission Symbol (eg. :edit_project)
# Context can be:
# * a project : returns true if user is allowed to do the specified action on this project
# * a group of projects : returns true if user is allowed on every project
# * nil with options[:global] set : check if user has at least one role allowed for this action,
# or falls back to Non Member / Anonymous permissions depending if the user is logged
def allowed_to?(action, context, options={})
if context && context.is_a?(Project)
def allowed_to?(action, project, options={})
if project
# No action allowed on archived projects
return false unless context.active?
return false unless project.active?
# No action allowed on disabled modules
return false unless context.allows_to?(action)
return false unless project.allows_to?(action)
# Admin users are authorized for anything else
return true if admin?
roles = roles_for_project(context)
roles = roles_for_project(project)
return false unless roles
roles.detect {|role| (context.is_public? || role.member?) && role.allowed_to?(action)}
roles.detect {|role| (project.is_public? || role.member?) && role.allowed_to?(action)}
elsif context && context.is_a?(Array)
# Authorize if user is authorized on every element of the array
context.map do |project|
allowed_to?(action,project,options)
end.inject do |memo,allowed|
memo && allowed
end
elsif options[:global]
# Admin users are always authorized
return true if admin?
@@ -390,69 +324,6 @@ class User < Principal
false
end
end
# Is the user allowed to do the specified action on any project?
# See allowed_to? for the actions and valid options.
def allowed_to_globally?(action, options)
allowed_to?(action, nil, options.reverse_merge(:global => true))
end
safe_attributes 'login',
'firstname',
'lastname',
'mail',
'mail_notification',
'language',
'custom_field_values',
'custom_fields',
'identity_url'
safe_attributes 'status',
'auth_source_id',
:if => lambda {|user, current_user| current_user.admin?}
safe_attributes 'group_ids',
:if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
# Utility method to help check if a user should be notified about an
# event.
#
# TODO: only supports Issue events currently
def notify_about?(object)
case mail_notification
when 'all'
true
when 'selected'
# user receives notifications for created/assigned issues on unselected projects
if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
true
else
false
end
when 'none'
false
when 'only_my_events'
if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
true
else
false
end
when 'only_assigned'
if object.is_a?(Issue) && object.assigned_to == self
true
else
false
end
when 'only_owner'
if object.is_a?(Issue) && object.author == self
true
else
false
end
else
false
end
end
def self.current=(user)
@current_user = user

View File

@@ -43,7 +43,7 @@ class Version < ActiveRecord::Base
end
def start_date
@start_date ||= fixed_issues.minimum('start_date')
effective_date
end
def due_date
@@ -73,17 +73,6 @@ class Version < ActiveRecord::Base
def completed?
effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
end
def behind_schedule?
if completed_pourcent == 100
return false
elsif due_date && start_date
done_date = start_date + ((due_date - start_date+1)* completed_pourcent/100).floor
return done_date <= Date.today
else
false # No issues so it's not late
end
end
# Returns the completion percentage of this version based on the amount of open/closed issues
# and the time spent on the open issues.
@@ -134,10 +123,6 @@ class Version < ActiveRecord::Base
end
def to_s; name end
def to_s_with_project
"#{project} - #{name}"
end
# Versions are sorted by effective_date and "Project Name - Version name"
# Those with no effective_date are at the end, sorted by "Project Name - Version name"

View File

@@ -46,10 +46,10 @@ class Wiki < ActiveRecord::Base
def find_page(title, options = {})
title = start_page if title.blank?
title = Wiki.titleize(title)
page = pages.first(:conditions => ["LOWER(title) LIKE LOWER(?)", title])
page = pages.find_by_title(title)
if !page && !(options[:with_redirect] == false)
# search for a redirect
redirect = redirects.first(:conditions => ["LOWER(title) LIKE LOWER(?)", title])
redirect = redirects.find_by_title(title)
page = find_page(redirect.redirects_to, :with_redirect => false) if redirect
end
page

View File

@@ -54,7 +54,7 @@ class WikiContent < ActiveRecord::Base
:description => :comments,
:datetime => :updated_on,
:type => 'wiki-page',
:url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.page.wiki.project, :id => o.page.title, :version => o.version}}
:url => Proc.new {|o| {:controller => 'wiki', :id => o.page.wiki.project_id, :page => o.page.title, :version => o.version}}
acts_as_activity_provider :type => 'wiki_edits',
:timestamp => "#{WikiContent.versioned_table_name}.updated_on",

View File

@@ -28,7 +28,7 @@ class WikiPage < ActiveRecord::Base
acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
:description => :text,
:datetime => :created_on,
:url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.wiki.project, :id => o.title}}
:url => Proc.new {|o| {:controller => 'wiki', :id => o.wiki.project, :page => o.title}}
acts_as_searchable :columns => ['title', 'text'],
:include => [{:wiki => :project}, :content],
@@ -139,7 +139,7 @@ class WikiPage < ActiveRecord::Base
parent_page = t.blank? ? nil : self.wiki.find_page(t)
self.parent = parent_page
end
protected
def validate

View File

@@ -1,5 +1,20 @@
<div id="admin-menu">
<ul>
<%= render_menu :admin_menu %>
</ul>
<ul>
<li><%= link_to l(:label_project_plural), {:controller => 'admin', :action => 'projects'}, :class => 'projects' %></li>
<li><%= link_to l(:label_user_plural), {:controller => 'users'}, :class => 'users' %></li>
<li><%= link_to l(:label_group_plural), {:controller => 'groups'}, :class => 'groups' %></li>
<li><%= link_to l(:label_ldap_authentication), :controller => 'ldap_auth_sources', :action => 'index' %></li>
<li><%= link_to l(:label_role_and_permissions), {:controller => 'roles'}, :class => 'roles' %></li>
<li><%= link_to l(:label_tracker_plural), {:controller => 'trackers'}, :class => 'trackers' %></li>
<li><%= link_to l(:label_issue_status_plural), {:controller => 'issue_statuses'}, :class => 'issue_statuses' %></li>
<li><%= link_to l(:label_workflow), {:controller => 'workflows', :action => 'edit'}, :class => 'workflows' %></li>
<li><%= link_to l(:label_custom_field_plural), {:controller => 'custom_fields'}, :class => 'custom_fields' %></li>
<li><%= link_to l(:label_enumerations), {:controller => 'enumerations'}, :class => 'enumerations' %></li>
<li><%= link_to l(:label_settings), {:controller => 'settings'}, :class => 'settings' %></li>
<% menu_items_for(:admin_menu) do |item| -%>
<li><%= link_to h(item.caption), item.url, item.html_options %></li>
<% end -%>
<li><%= link_to l(:label_plugins), {:controller => 'admin', :action => 'plugins'}, :class => 'plugins' %></li>
<li><%= link_to l(:label_information_plural), {:controller => 'admin', :action => 'info'}, :class => 'info' %></li>
</ul>
</div>

View File

@@ -1,5 +1,5 @@
<div class="contextual">
<%= link_to l(:label_project_new), {:controller => 'projects', :action => 'new'}, :class => 'icon icon-add' %>
<%= link_to l(:label_project_new), {:controller => 'projects', :action => 'add'}, :class => 'icon icon-add' %>
</div>
<h2><%=l(:label_project_plural)%></h2>
@@ -19,21 +19,23 @@
<table class="list">
<thead><tr>
<th><%=l(:label_project)%></th>
<th><%=l(:field_description)%></th>
<th><%=l(:field_is_public)%></th>
<th><%=l(:field_created_on)%></th>
<th></th>
</tr></thead>
<tbody>
<% project_tree(@projects) do |project, level| %>
<tr class="<%= cycle("odd", "even") %> <%= project.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
<td class="name"><span><%= link_to_project(project, {:action => 'settings'}, :title => project.short_description) %></span></td>
<tr class="<%= cycle("odd", "even") %> <%= css_project_classes(project) %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
<td class="name"><%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %></td>
<td><%= textilizable project.short_description, :project => project %></td>
<td align="center"><%= checked_image project.is_public? %></td>
<td align="center"><%= format_date(project.created_on) %></td>
<td class="buttons">
<%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project, :status => params[:status] }, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-lock') if project.active? %>
<%= link_to(l(:button_unarchive), { :controller => 'projects', :action => 'unarchive', :id => project, :status => params[:status] }, :method => :post, :class => 'icon icon-unlock') if !project.active? && (project.parent.nil? || project.parent.active?) %>
<%= link_to(l(:button_copy), { :controller => 'projects', :action => 'copy', :id => project }, :class => 'icon icon-copy') %>
<%= link_to(l(:button_delete), project_destroy_confirm_path(project), :class => 'icon icon-del') %>
<%= link_to(l(:button_delete), { :controller => 'projects', :action => 'destroy', :id => project }, :class => 'icon icon-del') %>
</td>
</tr>
<% end %>

View File

@@ -1,5 +1,6 @@
<span id="attachments_fields">
<%= file_field_tag 'attachments[1][file]', :size => 30, :id => nil -%><label class="inline"><span id="attachment_description_label_content"><%= l(:label_optional_description) %></span><%= text_field_tag 'attachments[1][description]', '', :size => 60, :id => nil %>
<%= file_field_tag 'attachments[1][file]', :size => 30, :id => nil -%>
<label class="inline"><span id="attachment_description_label_content"><%= l(:label_optional_description) %></span><%= text_field_tag 'attachments[1][description]', '', :size => 60, :id => nil %>
</label>
</span>
<br />

View File

@@ -30,11 +30,11 @@
</table>
<% other_formats_links do |f| %>
<%= f.link_to 'Atom', :url => {:controller => 'activities', :action => 'index', :id => @project, :show_messages => 1, :key => User.current.rss_key} %>
<%= f.link_to 'Atom', :url => {:controller => 'projects', :action => 'activity', :id => @project, :show_messages => 1, :key => User.current.rss_key} %>
<% end %>
<% content_for :header_tags do %>
<%= auto_discovery_link_tag(:atom, {:controller => 'activities', :action => 'index', :id => @project, :format => 'atom', :show_messages => 1, :key => User.current.rss_key}) %>
<%= auto_discovery_link_tag(:atom, {:controller => 'projects', :action => 'activity', :id => @project, :format => 'atom', :show_messages => 1, :key => User.current.rss_key}) %>
<% end %>
<% html_title l(:label_board_plural) %>

View File

@@ -69,5 +69,4 @@
<% content_for :header_tags do %>
<%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@project}: #{@board}") %>
<%= stylesheet_link_tag 'scm' %>
<% end %>

View File

@@ -1,7 +1,6 @@
<h2><%= l(:label_calendar) %></h2>
<% form_tag(calendar_path, :method => :put, :id => 'query_form') do %>
<%= hidden_field_tag('project_id', @project.to_param) if @project%>
<% form_tag({}, :id => 'query_form') do %>
<fieldset id="filters" class="collapsible">
<legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
<div>
@@ -10,7 +9,14 @@
</fieldset>
<p style="float:right;">
<%= link_to_previous_month(@year, @month, :project => @project) %> | <%= link_to_next_month(@year, @month, :project => @project) %>
<%= link_to_remote ('&#171; ' + (@month==1 ? "#{month_name(12)} #{@year-1}" : "#{month_name(@month-1)}")),
{:update => "content", :url => { :year => (@month==1 ? @year-1 : @year), :month =>(@month==1 ? 12 : @month-1) }},
{:href => url_for(:action => 'show', :year => (@month==1 ? @year-1 : @year), :month =>(@month==1 ? 12 : @month-1))}
%> |
<%= link_to_remote ((@month==12 ? "#{month_name(1)} #{@year+1}" : "#{month_name(@month+1)}") + ' &#187;'),
{:update => "content", :url => { :year => (@month==12 ? @year+1 : @year), :month =>(@month==12 ? 1 : @month+1) }},
{:href => url_for(:action => 'show', :year => (@month==12 ? @year+1 : @year), :month =>(@month==12 ? 1 : @month+1))}
%>
</p>
<p class="buttons">
@@ -26,8 +32,7 @@
}, :class => 'icon icon-checked' %>
<%= link_to_remote l(:button_clear),
{ :url => { :project_id => @project, :set_filter => (@query.new_record? ? 1 : nil) },
:method => :put,
{ :url => { :set_filter => (@query.new_record? ? 1 : nil) },
:update => "content",
}, :class => 'icon icon-reload' if @query.new_record? %>
</p>
@@ -38,9 +43,9 @@
<%= render :partial => 'common/calendar', :locals => {:calendar => @calendar} %>
<p class="legend cal">
<span class="starting"><%= l(:text_tip_issue_begin_day) %></span>
<span class="ending"><%= l(:text_tip_issue_end_day) %></span>
<span class="starting ending"><%= l(:text_tip_issue_begin_end_day) %></span>
<span class="starting"><%= l(:text_tip_task_begin_day) %></span>
<span class="ending"><%= l(:text_tip_task_end_day) %></span>
<span class="starting ending"><%= l(:text_tip_task_begin_end_day) %></span>
</p>
<% end %>

View File

@@ -0,0 +1,6 @@
<h2>403</h2>
<p><%= l(:notice_not_authorized) %></p>
<p><a href="javascript:history.back()">Back</a></p>
<% html_title '403' %>

View File

@@ -0,0 +1,6 @@
<h2>404</h2>
<p><%= l(:notice_file_not_found) %></p>
<p><a href="javascript:history.back()">Back</a></p>
<% html_title '404' %>

View File

@@ -1,6 +0,0 @@
<h2><%=h @status %></h2>
<p id="errorExplanation"><%=h @message %></p>
<p><a href="javascript:history.back()">Back</a></p>
<% html_title @status %>

View File

@@ -86,12 +86,10 @@ when "IssueCustomField" %>
<% when "UserCustomField" %>
<p><%= f.check_box :is_required %></p>
<p><%= f.check_box :visible %></p>
<p><%= f.check_box :editable %></p>
<% when "ProjectCustomField" %>
<p><%= f.check_box :is_required %></p>
<p><%= f.check_box :visible %></p>
<p><%= f.check_box :searchable %></p>
<% when "TimeEntryCustomField" %>

View File

@@ -1,8 +1,6 @@
<% @gantt.view = self %>
<h2><%= l(:label_gantt) %></h2>
<% form_tag(gantt_path(:month => params[:month], :year => params[:year], :months => params[:months]), :method => :put, :id => 'query_form') do %>
<%= hidden_field_tag('project_id', @project.to_param) if @project%>
<% form_tag(params.merge(:month => nil, :year => nil, :months => nil), :id => 'query_form') do %>
<fieldset id="filters" class="collapsible">
<legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
<div>
@@ -10,7 +8,7 @@
</div>
</fieldset>
<p class="contextual">
<p style="float:right;">
<%= gantt_zoom_link(@gantt, :in) %>
<%= gantt_zoom_link(@gantt, :out) %>
</p>
@@ -29,8 +27,7 @@
}, :class => 'icon icon-checked' %>
<%= link_to_remote l(:button_clear),
{ :url => { :project_id => @project, :set_filter => (@query.new_record? ? 1 : nil) },
:method => :put,
{ :url => { :set_filter => (@query.new_record? ? 1 : nil) },
:update => "content",
}, :class => 'icon icon-reload' if @query.new_record? %>
</p>
@@ -57,21 +54,11 @@ if @gantt.zoom >1
end
end
# Width of the entire chart
g_width = (@gantt.date_to - @gantt.date_from + 1)*zoom
@gantt.render(:top => headers_height + 8, :zoom => zoom, :g_width => g_width, :subject_width => subject_width)
g_height = [(20 * (@gantt.number_of_rows + 6))+150, 206].max
g_height = [(20 * @gantt.events.length + 6)+150, 206].max
t_height = g_height + headers_height
%>
<% if @gantt.truncated %>
<p class="warning"><%= l(:notice_gantt_chart_truncated, :max => @gantt.max_rows) %></p>
<% end %>
<table width="100%" style="border:0; border-collapse: collapse;">
<tr>
<td style="width:<%= subject_width %>px; padding:0px;">
@@ -79,11 +66,26 @@ t_height = g_height + headers_height
<div style="position:relative;height:<%= t_height + 24 %>px;width:<%= subject_width + 1 %>px;">
<div style="right:-2px;width:<%= subject_width %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr"></div>
<div style="right:-2px;width:<%= subject_width %>px;height:<%= t_height %>px;border-left: 1px solid #c0c0c0;overflow:hidden;" class="gantt_hdr"></div>
<div class="gantt_subjects">
<%= @gantt.subjects %>
</div>
<%
#
# Tasks subjects
#
top = headers_height + 8
@gantt.events.each do |i|
left = 4 + (i.is_a?(Issue) ? i.level * 16 : 0)
%>
<div style="position: absolute;line-height:1.2em;height:16px;top:<%= top %>px;left:<%= left %>px;overflow:hidden;"><small>
<% if i.is_a? Issue %>
<%= h("#{i.project} -") unless @project && @project == i.project %>
<%= link_to_issue i %>
<% else %>
<span class="icon icon-package">
<%= link_to_version i %>
</span>
<% end %>
</small></div>
<% top = top + 20
end %>
</div>
</td>
<td>
@@ -161,7 +163,53 @@ if show_days
end
end %>
<%= @gantt.lines %>
<%
#
# Tasks
#
top = headers_height + 10
@gantt.events.each do |i|
if i.is_a? Issue
i_start_date = (i.start_date >= @gantt.date_from ? i.start_date : @gantt.date_from )
i_end_date = (i.due_before <= @gantt.date_to ? i.due_before : @gantt.date_to )
i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor
i_done_date = (i_done_date <= @gantt.date_from ? @gantt.date_from : i_done_date )
i_done_date = (i_done_date >= @gantt.date_to ? @gantt.date_to : i_done_date )
i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
i_left = ((i_start_date - @gantt.date_from)*zoom).floor
i_width = ((i_end_date - i_start_date + 1)*zoom).floor - 2 # total width of the issue (- 2 for left and right borders)
d_width = ((i_done_date - i_start_date)*zoom).floor - 2 # done width
l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor - 2 : 0 # delay width
css = "task " + (i.leaf? ? 'leaf' : 'parent')
%>
<div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= i_width %>px;" class="<%= css %> task_todo"><div class="left"></div>&nbsp;<div class="right"></div></div>
<% if l_width > 0 %>
<div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= l_width %>px;" class="<%= css %> task_late">&nbsp;</div>
<% end %>
<% if d_width > 0 %>
<div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= d_width %>px;" class="<%= css %> task_done">&nbsp;</div>
<% end %>
<div style="top:<%= top %>px;left:<%= i_left + i_width + 8 %>px;background:#fff;" class="<%= css %>">
<%= i.status.name %>
<%= (i.done_ratio).to_i %>%
</div>
<div class="tooltip" style="position: absolute;top:<%= top %>px;left:<%= i_left %>px;width:<%= i_width %>px;height:12px;">
<span class="tip">
<%= render_issue_tooltip i %>
</span></div>
<% else
i_left = ((i.start_date - @gantt.date_from)*zoom).floor
%>
<div style="top:<%= top %>px;left:<%= i_left %>px;width:15px;" class="task milestone">&nbsp;</div>
<div style="top:<%= top %>px;left:<%= i_left + 12 %>px;background:#fff;" class="task">
<strong><%= format_version_name i %></strong>
</div>
<% end %>
<% top = top + 20
end %>
<%
#
@@ -178,8 +226,8 @@ if Date.today >= @gantt.date_from and Date.today <= @gantt.date_to %>
<table width="100%">
<tr>
<td align="left"><%= link_to_remote ('&#171; ' + l(:label_previous)), {:url => @gantt.params_previous, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'}, {:href => url_for(@gantt.params_previous)} %></td>
<td align="right"><%= link_to_remote (l(:label_next) + ' &#187;'), {:url => @gantt.params_next, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'}, {:href => url_for(@gantt.params_next)} %></td>
<td align="left"><%= link_to_remote ('&#171; ' + l(:label_previous)), {:url => @gantt.params_previous, :update => 'content', :complete => 'window.scrollTo(0,0)'}, {:href => url_for(@gantt.params_previous)} %></td>
<td align="right"><%= link_to_remote (l(:label_next) + ' &#187;'), {:url => @gantt.params_next, :update => 'content', :complete => 'window.scrollTo(0,0)'}, {:href => url_for(@gantt.params_next)} %></td>
</tr>
</table>

View File

@@ -29,7 +29,7 @@
<% remote_form_for(:group, @group, :url => {:controller => 'groups', :action => 'add_users', :id => @group}, :method => :post) do |f| %>
<fieldset><legend><%=l(:label_user_new)%></legend>
<p><%= label_tag "user_search", l(:label_user_search) %><%= text_field_tag 'user_search', nil %></p>
<p><%= text_field_tag 'user_search', nil %></p>
<%= observe_field(:user_search,
:frequency => 0.5,
:update => :users,

View File

@@ -2,5 +2,5 @@
<div class="box">
<p><%= f.text_field :name, :size => 30, :required => true %></p>
<p><%= f.select :assigned_to_id, @project.users.sort.collect{|u| [u.name, u.id]}, :include_blank => true %></p>
<p><%= f.select :assigned_to_id, @project.users.collect{|u| [u.name, u.id]}, :include_blank => true %></p>
</div>

View File

@@ -1,9 +1,7 @@
<%= error_messages_for 'relation' %>
<p><%= f.select :relation_type, collection_for_relation_type_select, {}, :onchange => "setPredecessorFieldsVisibility();" %>
<%= l(:label_issue) %> #<%= f.text_field :issue_to_id, :size => 10 %>
<div id="related_issue_candidates" class="autocomplete"></div>
<%= javascript_tag "observeRelatedIssueField('#{auto_complete_issues_path(:id => @issue, :project_id => @project) }')" %>
<%= l(:label_issue) %> #<%= f.text_field :issue_to_id, :size => 6 %>
<span id="predecessor_fields" style="display:none;">
<%= l(:field_delay) %>: <%= f.text_field :delay, :size => 3 %> <%= l(:label_day_plural) %>
</span>

View File

@@ -1,10 +1,10 @@
<div class="contextual">
<%= link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %>
<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'new', :issue_id => @issue}, :class => 'icon icon-time-add' %>
<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time-add' %>
<% replace_watcher ||= 'watcher' %>
<%= watcher_tag(@issue, User.current, {:id => replace_watcher, :replace => ['watcher','watcher2']}) %>
<%= link_to_if_authorized l(:button_duplicate), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-duplicate' %>
<%= link_to_if_authorized l(:button_copy), {:controller => 'issue_moves', :action => 'new', :id => @issue, :copy_options => {:copy => 't'}}, :class => 'icon icon-copy' %>
<%= link_to_if_authorized l(:button_move), {:controller => 'issue_moves', :action => 'new', :id => @issue}, :class => 'icon icon-move' %>
<%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :confirm => (@issue.leaf? ? l(:text_are_you_sure) : l(:text_are_you_sure_with_children)), :method => :post, :class => 'icon icon-del' %>
<%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'move', :id => @issue, :copy_options => {:copy => 't'} }, :class => 'icon icon-copy' %>
<%= link_to_if_authorized l(:button_move), {:controller => 'issues', :action => 'move', :id => @issue }, :class => 'icon icon-move' %>
<%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
</div>

View File

@@ -23,7 +23,7 @@
<%= prompt_to_remote(image_tag('add.png', :style => 'vertical-align: middle;'),
l(:label_version_new),
'version[name]',
{:controller => 'versions', :action => 'create', :project_id => @project},
{:controller => 'versions', :action => 'new', :project_id => @project},
:title => l(:label_version_new),
:tabindex => 200) if authorize_for('versions', 'new') %>
</p>
@@ -40,6 +40,6 @@
</div>
<div style="clear:both;"> </div>
<%= render :partial => 'issues/form_custom_fields' %>
<%= render :partial => 'form_custom_fields' %>
<% end %>

View File

@@ -1,10 +1,8 @@
<% changesets.each do |changeset| %>
<div class="changeset <%= cycle('odd', 'even') %>">
<p><%= link_to_revision(changeset, changeset.project,
:text => "#{l(:label_revision)} #{changeset.format_identifier}") %><br />
<p><%= link_to("#{l(:label_revision)} #{changeset.revision}",
:controller => 'repositories', :action => 'revision', :id => changeset.project, :rev => changeset.revision) %><br />
<span class="author"><%= authoring(changeset.committed_on, changeset.author) %></span></p>
<div class="changeset-changes">
<%= textilizable(changeset, :comments) %>
</div>
<%= textilizable(changeset, :comments) %>
</div>
<% end %>

View File

@@ -44,7 +44,7 @@
<%= f.hidden_field :lock_version %>
<%= submit_tag l(:button_submit) %>
<%= link_to_remote l(:label_preview),
{ :url => preview_issue_path(:project_id => @project, :id => @issue),
{ :url => { :controller => 'issues', :action => 'preview', :project_id => @project, :id => @issue },
:method => 'post',
:update => 'preview',
:with => 'Form.serialize("issue-form")',

View File

@@ -1,17 +1,18 @@
<%= call_hook(:view_issues_form_details_top, { :issue => @issue, :form => f }) %>
<div id="issue_descr_fields" <%= 'style="display:none"' unless @issue.new_record? || @issue.errors.any? %>>
<p><%= f.select :tracker_id, @project.trackers.collect {|t| [t.name, t.id]}, :required => true %></p>
<%= observe_field :issue_tracker_id, :url => { :action => :new, :project_id => @project, :id => @issue },
<%= observe_field :issue_tracker_id, :url => { :action => :update_form, :project_id => @project, :id => @issue },
:update => :attributes,
:with => "Form.serialize('issue-form')" %>
<p><%= f.text_field :subject, :size => 80, :required => true %></p>
<% if User.current.allowed_to?(:manage_subtasks, @project) %>
<p id="parent_issue"><%= f.text_field :parent_issue_id, :size => 10 %></p>
<% unless (@issue.new_record? && @issue.parent_issue_id.nil?) || !User.current.allowed_to?(:manage_subtasks, @project) %>
<p><%= f.text_field :parent_issue_id, :size => 10 %></p>
<div id="parent_issue_candidates" class="autocomplete"></div>
<%= javascript_tag "observeParentIssueField('#{auto_complete_issues_path(:id => @issue, :project_id => @project) }')" %>
<%= javascript_tag "observeParentIssueField('#{url_for(:controller => :issues,
:action => :auto_complete,
:id => @issue,
:project_id => @project) }')" %>
<% end %>
<p><%= f.text_area :description,
@@ -22,15 +23,15 @@
</div>
<div id="attributes" class="attributes">
<%= render :partial => 'issues/attributes' %>
<%= render :partial => 'attributes' %>
</div>
<% if @issue.new_record? %>
<p id="attachments_form"><%= label_tag('attachments[1][file]', l(:label_attachment_plural))%><%= render :partial => 'attachments/form' %></p>
<p><%= label_tag('attachments[1][file]', l(:label_attachment_plural))%><%= render :partial => 'attachments/form' %></p>
<% end %>
<% if @issue.new_record? && User.current.allowed_to?(:add_issue_watchers, @project) -%>
<p id="watchers_form"><label><%= l(:label_issue_watchers) %></label>
<p><label><%= l(:label_issue_watchers) %></label>
<% @issue.project.users.sort.each do |user| -%>
<label class="floating"><%= check_box_tag 'issue[watcher_user_ids][]', user.id, @issue.watched_by?(user) %> <%=h user %></label>
<% end -%>

View File

@@ -1,7 +1,7 @@
<% reply_links = authorize_for('issues', 'edit') -%>
<% for journal in journals %>
<div id="change-<%= journal.id %>" class="<%= journal.css_classes %>">
<h4><div class="journal-link"><%= link_to "##{journal.indice}", :anchor => "note-#{journal.indice}" %></div>
<div id="change-<%= journal.id %>" class="journal">
<h4><div style="float:right;"><%= link_to "##{journal.indice}", :anchor => "note-#{journal.indice}" %></div>
<%= avatar(journal.user, :size => "24") %>
<%= content_tag('a', '', :name => "note-#{journal.indice}")%>
<%= authoring journal.created_on, journal.user, :label => :label_updated_time_by %></h4>

View File

@@ -3,7 +3,7 @@
<div class="autoscroll">
<table class="list issues">
<thead><tr>
<th class="checkbox hide-when-print"><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;',
<th><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;',
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
</th>
<%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %>
@@ -25,7 +25,7 @@
<% previous_group = group %>
<% end %>
<tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
<td class="checkbox hide-when-print"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
<td class="checkbox"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
<td class="id"><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
<% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %>
</tr>

View File

@@ -14,7 +14,7 @@
<%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;') %>
<%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %>
</td>
<td class="project"><%= link_to_project(issue.project) %></td>
<td class="project"><%= link_to(h(issue.project), :controller => 'projects', :action => 'show', :id => issue.project) %></td>
<td class="tracker"><%=h issue.tracker %></td>
<td class="subject">
<%= link_to h(truncate(issue.subject, :length => 60)), :controller => 'issues', :action => 'show', :id => issue %> (<%=h issue.status %>)

View File

@@ -1,14 +1,14 @@
<div class="contextual">
<% if authorize_for('issue_relations', 'new') %>
<%= toggle_link l(:button_add), 'new-relation-form', {:focus => 'relation_issue_to_id'} %>
<%= toggle_link l(:button_add), 'new-relation-form'%>
<% end %>
</div>
<p><strong><%=l(:label_related_issues)%></strong></p>
<% if @relations.present? %>
<% if @issue.relations.any? %>
<table style="width:100%">
<% @relations.each do |relation| %>
<% @issue.relations.select {|r| r.other_issue(@issue).visible? }.each do |relation| %>
<tr>
<td><%= l(relation.label_for(@issue)) %> <%= "(#{l('datetime.distance_in_words.x_days', :count => relation.delay)})" if relation.delay && relation.delay != 0 %>
<%= h(relation.other_issue(@issue).project) + ' - ' if Setting.cross_project_issue_relations? %>
@@ -28,7 +28,6 @@
<% remote_form_for(:relation, @relation,
:url => {:controller => 'issue_relations', :action => 'new', :issue_id => @issue},
:method => :post,
:complete => "Form.Element.focus('relation_issue_to_id');",
:html => {:id => 'new-relation-form', :style => (@relation ? '' : 'display: none;')}) do |f| %>
<%= render :partial => 'issue_relations/form', :locals => {:f => f}%>
<% end %>

View File

@@ -6,7 +6,7 @@
<%= call_hook(:view_issues_sidebar_issues_bottom) %>
<% if User.current.allowed_to?(:view_calendar, @project, :global => true) %>
<%= link_to(l(:label_calendar), :controller => 'calendars', :action => 'show', :project_id => @project) %><br />
<%= link_to(l(:label_calendar), :controller => 'issues', :action => 'calendar', :project_id => @project) %><br />
<% end %>
<% if User.current.allowed_to?(:view_gantt, @project, :global => true) %>
<%= link_to(l(:label_gantt), :controller => 'gantts', :action => 'show', :project_id => @project) %><br />

View File

@@ -2,7 +2,7 @@
<ul><%= @issues.collect {|i| content_tag('li', link_to(h("#{i.tracker} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n") %></ul>
<% form_tag(:action => 'bulk_update') do %>
<% form_tag() do %>
<%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %>
<div class="box tabular">
<fieldset class="attributes">
@@ -11,7 +11,7 @@
<div class="splitcontentleft">
<p>
<label><%= l(:field_tracker) %></label>
<%= select_tag('issue[tracker_id]', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@trackers, :id, :name)) %>
<%= select_tag('issue[tracker_id]', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@project.trackers, :id, :name)) %>
</p>
<% if @available_statuses.any? %>
<p>
@@ -27,25 +27,20 @@
<label><%= l(:field_assigned_to) %></label>
<%= select_tag('issue[assigned_to_id]', content_tag('option', l(:label_no_change_option), :value => '') +
content_tag('option', l(:label_nobody), :value => 'none') +
options_from_collection_for_select(@assignables, :id, :name)) %>
options_from_collection_for_select(@project.assignable_users, :id, :name)) %>
</p>
<% if @project %>
<p>
<label><%= l(:field_category) %></label>
<%= select_tag('issue[category_id]', content_tag('option', l(:label_no_change_option), :value => '') +
content_tag('option', l(:label_none), :value => 'none') +
options_from_collection_for_select(@project.issue_categories, :id, :name)) %>
</p>
<% end %>
<% #TODO: allow editing versions when multiple projects %>
<% if @project %>
<p>
<label><%= l(:field_fixed_version) %></label>
<%= select_tag('issue[fixed_version_id]', content_tag('option', l(:label_no_change_option), :value => '') +
content_tag('option', l(:label_none), :value => 'none') +
version_options_for_select(@project.shared_versions.open.sort)) %>
version_options_for_select(@project.shared_versions.open)) %>
</p>
<% end %>
<% @custom_fields.each do |custom_field| %>
<p><label><%= h(custom_field.name) %></label> <%= custom_field_tag_for_bulk_edit('issue', custom_field) %></p>

View File

@@ -4,22 +4,19 @@
<% if !@issue.nil? -%>
<li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue},
:class => 'icon-edit', :disabled => !@can[:edit] %></li>
<% else %>
<li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)},
:class => 'icon-edit', :disabled => !@can[:edit] %></li>
<% end %>
<% if @allowed_statuses.present? %>
<li class="folder">
<a href="#" class="submenu" onclick="return false;"><%= l(:field_status) %></a>
<ul>
<% @statuses.each do |s| -%>
<li><%= context_menu_link s.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {:status_id => s}, :back_url => @back}, :method => :post,
:selected => (@issue && s == @issue.status), :disabled => !(@can[:update] && @allowed_statuses.include?(s)) %></li>
<li><%= context_menu_link s.name, {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:status_id => s}, :back_url => @back}, :method => :post,
:selected => (s == @issue.status), :disabled => !(@can[:update] && @allowed_statuses.include?(s)) %></li>
<% end -%>
</ul>
</li>
<% end %>
<% else %>
<li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)},
:class => 'icon-edit', :disabled => !@can[:edit] %></li>
<% end %>
<% unless @trackers.nil? %>
<li class="folder">
@@ -32,18 +29,15 @@
</ul>
</li>
<% end %>
<li class="folder">
<a href="#" class="submenu"><%= l(:field_priority) %></a>
<ul>
<% @priorities.each do |p| -%>
<li><%= context_menu_link p.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {'priority_id' => p}, :back_url => @back}, :method => :post,
:selected => (@issue && p == @issue.priority), :disabled => (!@can[:edit] || @issues.detect {|i| !i.leaf?}) %></li>
:selected => (@issue && p == @issue.priority), :disabled => !@can[:edit] %></li>
<% end -%>
</ul>
</li>
<% #TODO: allow editing versions when multiple projects %>
<% unless @project.nil? || @project.shared_versions.open.empty? -%>
<li class="folder">
<a href="#" class="submenu"><%= l(:field_fixed_version) %></a>
@@ -57,7 +51,7 @@
</ul>
</li>
<% end %>
<% if @assignables.present? -%>
<% unless @assignables.nil? || @assignables.empty? -%>
<li class="folder">
<a href="#" class="submenu"><%= l(:field_assigned_to) %></a>
<ul>
@@ -83,22 +77,20 @@
</ul>
</li>
<% end -%>
<% if Issue.use_field_for_done_ratio? %>
<li class="folder">
<a href="#" class="submenu"><%= l(:field_done_ratio) %></a>
<ul>
<% (0..10).map{|x|x*10}.each do |p| -%>
<li><%= context_menu_link "#{p}%", {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {'done_ratio' => p}, :back_url => @back}, :method => :post,
:selected => (@issue && p == @issue.done_ratio), :disabled => (!@can[:edit] || @issues.detect {|i| !i.leaf?}) %></li>
:selected => (@issue && p == @issue.done_ratio), :disabled => !@can[:edit] %></li>
<% end -%>
</ul>
</li>
<% end %>
<% if !@issue.nil? %>
<% if @can[:log_time] -%>
<li><%= context_menu_link l(:button_log_time), {:controller => 'timelog', :action => 'new', :issue_id => @issue},
<li><%= context_menu_link l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue},
:class => 'icon-time-add' %></li>
<% end %>
<% if User.current.logged? %>
@@ -110,11 +102,11 @@
<li><%= context_menu_link l(:button_duplicate), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue},
:class => 'icon-duplicate', :disabled => !@can[:copy] %></li>
<% end %>
<li><%= context_menu_link l(:button_copy), new_issue_move_path(:ids => @issues.collect(&:id), :copy_options => {:copy => 't'}),
<li><%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'move', :ids => @issues.collect(&:id), :copy_options => {:copy => 't'}},
:class => 'icon-copy', :disabled => !@can[:move] %></li>
<li><%= context_menu_link l(:button_move), new_issue_move_path(:ids => @issues.collect(&:id)),
<li><%= context_menu_link l(:button_move), {:controller => 'issues', :action => 'move', :ids => @issues.collect(&:id)},
:class => 'icon-move', :disabled => !@can[:move] %></li>
<li><%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :ids => @issues.collect(&:id), :back_url => @back},
<li><%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :ids => @issues.collect(&:id)},
:method => :post, :confirm => l(:text_issues_destroy_confirmation), :class => 'icon-del', :disabled => !@can[:delete] %></li>
<%= call_hook(:view_issues_context_menu_end, {:issues => @issues, :can => @can, :back => @back }) %>

View File

@@ -1,28 +0,0 @@
api.array :issues, api_meta(:total_count => @issue_count, :offset => @offset, :limit => @limit) do
@issues.each do |issue|
api.issue do
api.id issue.id
api.project(:id => issue.project_id, :name => issue.project.name) unless issue.project.nil?
api.tracker(:id => issue.tracker_id, :name => issue.tracker.name) unless issue.tracker.nil?
api.status(:id => issue.status_id, :name => issue.status.name) unless issue.status.nil?
api.priority(:id => issue.priority_id, :name => issue.priority.name) unless issue.priority.nil?
api.author(:id => issue.author_id, :name => issue.author.name) unless issue.author.nil?
api.assigned_to(:id => issue.assigned_to_id, :name => issue.assigned_to.name) unless issue.assigned_to.nil?
api.category(:id => issue.category_id, :name => issue.category.name) unless issue.category.nil?
api.fixed_version(:id => issue.fixed_version_id, :name => issue.fixed_version.name) unless issue.fixed_version.nil?
api.parent(:id => issue.parent_id) unless issue.parent.nil?
api.subject issue.subject
api.description issue.description
api.start_date issue.start_date
api.due_date issue.due_date
api.done_ratio issue.done_ratio
api.estimated_hours issue.estimated_hours
render_api_custom_values issue.custom_field_values, api
api.created_on issue.created_on
api.updated_on issue.updated_on
end
end
end

View File

@@ -39,7 +39,6 @@
{ :url => { :set_filter => 1 },
:before => 'selectAllOptions("selected_columns");',
:update => "content",
:complete => "apply_filters_observer()",
:with => "Form.serialize('query_form')"
}, :class => 'icon icon-checked' %>
@@ -79,7 +78,7 @@
<% content_for :header_tags do %>
<%= auto_discovery_link_tag(:atom, {:query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_issue_plural)) %>
<%= auto_discovery_link_tag(:atom, {:controller => 'journals', :action => 'index', :query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_changes_details)) %>
<%= auto_discovery_link_tag(:atom, {:action => 'changes', :query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_changes_details)) %>
<% end %>
<%= context_menu issues_context_menu_path %>
<%= context_menu :controller => 'issues', :action => 'context_menu' %>

Some files were not shown because too many files have changed in this diff Show More