diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 725bde788..32bb528a3 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -169,6 +169,13 @@ 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
diff --git a/app/controllers/context_menus_controller.rb b/app/controllers/context_menus_controller.rb
index 442f85b37..5f4b02ca2 100644
--- a/app/controllers/context_menus_controller.rb
+++ b/app/controllers/context_menus_controller.rb
@@ -6,9 +6,15 @@ class ContextMenusController < ApplicationController
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
+ @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)),
diff --git a/app/controllers/files_controller.rb b/app/controllers/files_controller.rb
index c84ce5f51..72a4a2411 100644
--- a/app/controllers/files_controller.rb
+++ b/app/controllers/files_controller.rb
@@ -1,7 +1,7 @@
class FilesController < ApplicationController
menu_item :files
- before_filter :find_project
+ before_filter :find_project_by_project_id
before_filter :authorize
helper :sort
@@ -19,4 +19,18 @@ class FilesController < ApplicationController
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? && Setting.notified_events.include?('file_added')
+ Mailer.deliver_attachments_added(attachments[:files])
+ end
+ redirect_to project_files_path(@project)
+ end
end
diff --git a/app/controllers/gantts_controller.rb b/app/controllers/gantts_controller.rb
index 6a6071e86..50fd8c13d 100644
--- a/app/controllers/gantts_controller.rb
+++ b/app/controllers/gantts_controller.rb
@@ -4,6 +4,7 @@ class GanttsController < ApplicationController
rescue_from Query::StatementInvalid, :with => :query_statement_invalid
+ helper :gantt
helper :issues
helper :projects
helper :queries
@@ -14,32 +15,17 @@ class GanttsController < ApplicationController
def show
@gantt = Redmine::Helpers::Gantt.new(params)
+ @gantt.project = @project
retrieve_query
@query.group_by = nil
- 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
+ @gantt.query = @query if @query.valid?
basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
respond_to do |format|
format.html { render :action => "show", :layout => !request.xhr? }
- format.png { send_data(@gantt.to_image(@project), :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
- format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
+ 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") }
end
end
diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb
index 0364e307c..385bfa757 100644
--- a/app/controllers/issues_controller.rb
+++ b/app/controllers/issues_controller.rb
@@ -47,6 +47,7 @@ class IssuesController < ApplicationController
include SortHelper
include IssuesHelper
helper :timelog
+ helper :gantt
include Redmine::Export::PDF
verify :method => [:post, :delete],
@@ -133,7 +134,7 @@ 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', :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
+ 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?} } :
{ :action => 'show', :id => @issue })
}
format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
diff --git a/app/controllers/project_enumerations_controller.rb b/app/controllers/project_enumerations_controller.rb
new file mode 100644
index 000000000..0b15887fa
--- /dev/null
+++ b/app/controllers/project_enumerations_controller.rb
@@ -0,0 +1,26 @@
+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
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 05c8d969d..f16349329 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -18,21 +18,23 @@
class ProjectsController < ApplicationController
menu_item :overview
menu_item :roadmap, :only => :roadmap
- menu_item :files, :only => [:add_file]
menu_item :settings, :only => :settings
- before_filter :find_project, :except => [ :index, :list, :add, :copy ]
- before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy]
- before_filter :authorize_global, :only => :add
+ 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 :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
accept_key_auth :index
-
- after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
+
+ after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
if controller.request.post?
controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
end
end
-
+
+ # TODO: convert to PUT only
+ verify :method => [:post, :put], :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
+
helper :sort
include SortHelper
helper :custom_fields
@@ -61,40 +63,45 @@ class ProjectsController < ApplicationController
end
end
- # Add a new project
- def add
+ def new
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
@trackers = Tracker.all
@project = Project.new(params[:project])
- 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
- @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
+
+ @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
+ end
+
+ def create
+ @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
+ @trackers = Tracker.all
+ @project = Project.new(params[: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
- 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 { render :action => 'new' }
+ format.xml { render :xml => @project.errors, :status => :unprocessable_entity }
+ end
+ end
+
end
def copy
@@ -175,28 +182,27 @@ class ProjectsController < ApplicationController
@wiki ||= @project.wiki
end
- # Edit @project
def edit
- if request.get?
+ end
+
+ def update
+ @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
- @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
+ respond_to do |format|
+ format.html {
+ settings
+ render :action => 'settings'
+ }
+ format.xml { render :xml => @project.errors, :status => :unprocessable_entity }
end
end
end
@@ -239,42 +245,6 @@ 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 => 'files', :action => 'index', :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
-
private
def find_optional_project
return true unless params[:id]
diff --git a/app/controllers/timelog_controller.rb b/app/controllers/timelog_controller.rb
index e234848d0..45f41ae5e 100644
--- a/app/controllers/timelog_controller.rb
+++ b/app/controllers/timelog_controller.rb
@@ -260,8 +260,8 @@ private
end
@from, @to = @to, @from if @from && @to && @from > @to
- @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)
+ @from ||= (TimeEntry.earilest_date_for_project(@project) || Date.today)
+ @to ||= (TimeEntry.latest_date_for_project(@project) || Date.today)
end
def load_available_criterias
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 0354d165d..b854850a3 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -95,7 +95,9 @@ class UsersController < ApplicationController
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
+ if params[:password].present? && (@user.auth_source_id.nil? || params[:user][:auth_source_id].blank?)
+ @user.password, @user.password_confirmation = params[:password], params[:password_confirmation]
+ end
@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)
diff --git a/app/controllers/versions_controller.rb b/app/controllers/versions_controller.rb
index dd01da95b..48612c7b8 100644
--- a/app/controllers/versions_controller.rb
+++ b/app/controllers/versions_controller.rb
@@ -18,9 +18,9 @@
class VersionsController < ApplicationController
menu_item :roadmap
model_object Version
- before_filter :find_model_object, :except => [:index, :new, :close_completed]
- before_filter :find_project_from_association, :except => [:index, :new, :close_completed]
- before_filter :find_project, :only => [:index, :new, :close_completed]
+ 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 :authorize
helper :custom_fields
@@ -63,6 +63,17 @@ 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|
@@ -79,7 +90,7 @@ class VersionsController < ApplicationController
end
else
respond_to do |format|
- format.html
+ format.html { render :action => 'new' }
format.js do
render(:update) {|page| page.alert(@version.errors.full_messages.join('\n')) }
end
@@ -87,9 +98,12 @@ class VersionsController < ApplicationController
end
end
end
-
+
def edit
- if request.post? && params[:version]
+ end
+
+ def update
+ if request.put? && params[:version]
attributes = params[:version].dup
attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing'])
if @version.update_attributes(attributes)
@@ -100,7 +114,7 @@ class VersionsController < ApplicationController
end
def close_completed
- if request.post?
+ if request.put?
@project.close_completed_versions
end
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb
index b49a5674c..8f81f66ba 100644
--- a/app/helpers/admin_helper.rb
+++ b/app/helpers/admin_helper.rb
@@ -20,12 +20,4 @@ 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
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 19dd654db..eecbc5b91 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -32,8 +32,27 @@ module ApplicationHelper
end
# Display a link if user is authorized
+ #
+ # @param [String] name Anchor text (passed to link_to)
+ # @param [Hash, String] options Hash params or url for the link target (passed to link_to).
+ # 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])
+ if options.is_a?(String)
+ begin
+ route = ActionController::Routing::Routes.recognize_path(options.gsub(/\?.*/,''), :method => options[:method] || :get)
+ link_controller = route[:controller]
+ link_action = route[:action]
+ rescue ActionController::RoutingError # Parse failed, not a route
+ link_controller, link_action = nil, nil
+ end
+ else
+ link_controller = options[:controller] || params[:controller]
+ link_action = options[:action]
+ end
+
+ link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(link_controller, link_action)
end
# Display a link to remote if user is authorized
@@ -102,6 +121,11 @@ module ApplicationHelper
link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
end
+
+ def link_to_project(project, options={})
+ options[:class] ||= 'project'
+ link_to(h(project), {:controller => 'projects', :action => 'show', :id => project}, :class => options[:class])
+ end
# Generates a link to a project if active
# Examples:
@@ -302,7 +326,7 @@ module ApplicationHelper
def time_tag(time)
text = distance_of_time_in_words(Time.now, time)
if @project
- link_to(text, {:controller => 'projects', :action => 'activity', :id => @project, :from => time.to_date}, :title => format_time(time))
+ link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
else
content_tag('acronym', text, :title => format_time(time))
end
@@ -813,6 +837,8 @@ module ApplicationHelper
email = $1
end
return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
+ else
+ ''
end
end
diff --git a/app/helpers/gantt_helper.rb b/app/helpers/gantt_helper.rb
new file mode 100644
index 000000000..38f3765e9
--- /dev/null
+++ b/app/helpers/gantt_helper.rb
@@ -0,0 +1,24 @@
+# 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 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
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 617822986..f1ddcfcbf 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -35,8 +35,10 @@ module IssuesHelper
@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) + "
" +
+ "#{@cached_label_project}: #{link_to_project(issue.project)}
" +
"#{@cached_label_status}: #{issue.status.name}
" +
"#{@cached_label_start_date}: #{format_date(issue.start_date)}
" +
"#{@cached_label_due_date}: #{format_date(issue.due_date)}
" +
@@ -243,7 +245,7 @@ module IssuesHelper
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'},
+ {:url => gantt.params.merge(:zoom => (gantt.zoom+1)), :method => :get, :update => 'content'},
{:href => url_for(gantt.params.merge(:zoom => (gantt.zoom+1)))})
else
l(:text_zoom_in) +
@@ -253,7 +255,7 @@ module IssuesHelper
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'},
+ {:url => gantt.params.merge(:zoom => (gantt.zoom-1)), :method => :get, :update => 'content'},
{:href => url_for(gantt.params.merge(:zoom => (gantt.zoom-1)))})
else
l(:text_zoom_out) +
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 7d0682df1..2b7362589 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -62,10 +62,28 @@ class Issue < ActiveRecord::Base
named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
- named_scope :recently_updated, :order => "#{self.table_name}.updated_on DESC"
+ named_scope :recently_updated, :order => "#{Issue.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],
+ :order => "#{Issue.table_name}.due_date ASC, #{Issue.table_name}.start_date ASC, #{Issue.table_name}.id ASC"
+ }
+ }
+
+ 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 :reschedule_following_issues, :close_duplicates, :update_done_ratio_from_issue_status
@@ -357,6 +375,13 @@ 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
# Users the issue can be assigned to
def assignable_users
@@ -821,7 +846,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} as j
+ #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j
where
i.status_id=s.id
and #{where}
diff --git a/app/models/journal.rb b/app/models/journal.rb
index a0e1ae877..3e846aeb8 100644
--- a/app/models/journal.rb
+++ b/app/models/journal.rb
@@ -65,4 +65,12 @@ 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
diff --git a/app/models/principal.rb b/app/models/principal.rb
index 58c3f0497..b3e07dda5 100644
--- a/app/models/principal.rb
+++ b/app/models/principal.rb
@@ -33,7 +33,11 @@ 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
diff --git a/app/models/project.rb b/app/models/project.rb
index 931f89b55..4b0236b37 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -412,6 +412,58 @@ 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
+ if module_enabled?(:issue_tracking)
+ [
+ issues.minimum('start_date'),
+ shared_versions.collect(&:effective_date),
+ shared_versions.collect {|v| v.fixed_issues.minimum('start_date')}
+ ].flatten.compact.min
+ end
+ end
+
+ # The latest due date of an issue or version
+ def due_date
+ if module_enabled?(:issue_tracking)
+ [
+ issues.maximum('due_date'),
+ shared_versions.collect(&:effective_date),
+ shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
+ ].flatten.compact.max
+ end
+ 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:
@@ -441,6 +493,15 @@ class Project < ActiveRecord::Base
enabled_modules.clear
end
end
+
+ # 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
diff --git a/app/models/query.rb b/app/models/query.rb
index b1f784528..59131afcd 100644
--- a/app/models/query.rb
+++ b/app/models/query.rb
@@ -195,6 +195,12 @@ class Query < ActiveRecord::Base
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] }
+ @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] }
+ @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"]] }
@@ -432,6 +438,47 @@ 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
diff --git a/app/models/time_entry.rb b/app/models/time_entry.rb
index 73f39f949..9bf970891 100644
--- a/app/models/time_entry.rb
+++ b/app/models/time_entry.rb
@@ -81,4 +81,20 @@ 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
diff --git a/app/models/version.rb b/app/models/version.rb
index 07e66434d..95e6ad5f6 100644
--- a/app/models/version.rb
+++ b/app/models/version.rb
@@ -73,6 +73,18 @@ 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 && fixed_issues.present? && fixed_issues.minimum('start_date') # TODO: should use #start_date but that method is wrong...
+ start_date = fixed_issues.minimum('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.
@@ -123,6 +135,10 @@ 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"
diff --git a/app/views/admin/projects.rhtml b/app/views/admin/projects.rhtml
index 46b68e4cc..47a2d0583 100644
--- a/app/views/admin/projects.rhtml
+++ b/app/views/admin/projects.rhtml
@@ -1,5 +1,5 @@
diff --git a/app/views/gantts/show.html.erb b/app/views/gantts/show.html.erb index 5d4ef0dbf..5a64054af 100644 --- a/app/views/gantts/show.html.erb +++ b/app/views/gantts/show.html.erb @@ -1,3 +1,4 @@ +<% @gantt.view = self %>
|
@@ -67,26 +69,10 @@ t_height = g_height + headers_height
-<%
-#
-# Tasks subjects
-#
-top = headers_height + 8
-@gantt.events.each do |i|
-left = 4 + (i.is_a?(Issue) ? i.level * 16 : 0)
- %>
-
- <% top = top + 20
-end %>
+<% top = headers_height + 8 %>
+
+<%= @gantt.subjects(:headers_height => headers_height, :top => top, :g_width => g_width) %>
+
|
@@ -164,53 +150,9 @@ if show_days
end
end %>
-<%
-#
-# 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')
- %>
-
- <%= i.status.name %>
- <%= (i.done_ratio).to_i %>%
-
-
-
- <%= render_issue_tooltip i %>
-
-<% else
- i_left = ((i.start_date - @gantt.date_from)*zoom).floor
- %>
-
- <%= format_version_name i %>
-
-<% end %>
- <% top = top + 20
-end %>
+<% top = headers_height + 10 %>
+
+<%= @gantt.lines(:top => top, :zoom => zoom, :g_width => g_width ) %>
<%
#
@@ -227,8 +169,8 @@ if Date.today >= @gantt.date_from and Date.today <= @gantt.date_to %>
- <%= render :partial => 'attributes' %>
+ <%= render :partial => 'issues/attributes' %>
<% if @issue.new_record? %>
diff --git a/app/views/issues/_history.rhtml b/app/views/issues/_history.rhtml
index a95cbf81c..4851e5f22 100644
--- a/app/views/issues/_history.rhtml
+++ b/app/views/issues/_history.rhtml
@@ -1,6 +1,6 @@
<% reply_links = authorize_for('issues', 'edit') -%>
<% for journal in journals %>
-
+
|
| <%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;', + | <%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %> @@ -25,7 +25,7 @@ <% previous_group = group %> <% end %>|
|---|---|
| <%= check_box_tag("ids[]", issue.id, false, :id => nil) %> | +<%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %> | <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %>