##// END OF EJS Templates
Queries can be marked as 'For all projects'. Such queries will be available on all projects and on the global issue list (#897, closes #671)....
Jean-Philippe Lang -
r1296:287d86e36325
parent child
Show More
@@ -0,0 +1,185
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'queries_controller'
20
21 # Re-raise errors caught by the controller.
22 class QueriesController; def rescue_action(e) raise e end; end
23
24 class QueriesControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :members, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries
26
27 def setup
28 @controller = QueriesController.new
29 @request = ActionController::TestRequest.new
30 @response = ActionController::TestResponse.new
31 User.current = nil
32 end
33
34 def test_get_new
35 @request.session[:user_id] = 2
36 get :new, :project_id => 1
37 assert_response :success
38 assert_template 'new'
39 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
40 :name => 'query[is_public]',
41 :checked => nil }
42 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
43 :name => 'query_is_for_all',
44 :checked => nil,
45 :disabled => nil }
46 end
47
48 def test_new_project_public_query
49 @request.session[:user_id] = 2
50 post :new,
51 :project_id => 'ecookbook',
52 :confirm => '1',
53 :default_columns => '1',
54 :fields => ["status_id", "assigned_to_id"],
55 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
56 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
57 :query => {"name" => "test_new_project_public_query", "is_public" => "1"},
58 :column_names => ["", "tracker", "status", "priority", "subject", "updated_on", "category"]
59
60 q = Query.find_by_name('test_new_project_public_query')
61 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => q
62 assert q.is_public?
63 assert q.has_default_columns?
64 assert q.valid?
65 end
66
67 def test_new_project_private_query
68 @request.session[:user_id] = 3
69 post :new,
70 :project_id => 'ecookbook',
71 :confirm => '1',
72 :default_columns => '1',
73 :fields => ["status_id", "assigned_to_id"],
74 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
75 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
76 :query => {"name" => "test_new_project_private_query", "is_public" => "1"},
77 :column_names => ["", "tracker", "status", "priority", "subject", "updated_on", "category"]
78
79 q = Query.find_by_name('test_new_project_private_query')
80 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => q
81 assert !q.is_public?
82 assert q.has_default_columns?
83 assert q.valid?
84 end
85
86 def test_get_edit_global_public_query
87 @request.session[:user_id] = 1
88 get :edit, :id => 4
89 assert_response :success
90 assert_template 'edit'
91 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
92 :name => 'query[is_public]',
93 :checked => 'checked' }
94 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
95 :name => 'query_is_for_all',
96 :checked => 'checked',
97 :disabled => 'disabled' }
98 end
99
100 def test_edit_global_public_query
101 @request.session[:user_id] = 1
102 post :edit,
103 :id => 4,
104 :confirm => '1',
105 :default_columns => '1',
106 :fields => ["status_id", "assigned_to_id"],
107 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
108 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
109 :query => {"name" => "test_edit_global_public_query", "is_public" => "1"},
110 :column_names => ["", "tracker", "status", "priority", "subject", "updated_on", "category"]
111
112 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 4
113 q = Query.find_by_name('test_edit_global_public_query')
114 assert q.is_public?
115 assert q.has_default_columns?
116 assert q.valid?
117 end
118
119 def test_get_edit_global_private_query
120 @request.session[:user_id] = 3
121 get :edit, :id => 3
122 assert_response :success
123 assert_template 'edit'
124 assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
125 :name => 'query[is_public]' }
126 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
127 :name => 'query_is_for_all',
128 :checked => 'checked',
129 :disabled => 'disabled' }
130 end
131
132 def test_edit_global_private_query
133 @request.session[:user_id] = 3
134 post :edit,
135 :id => 3,
136 :confirm => '1',
137 :default_columns => '1',
138 :fields => ["status_id", "assigned_to_id"],
139 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
140 :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
141 :query => {"name" => "test_edit_global_private_query", "is_public" => "1"},
142 :column_names => ["", "tracker", "status", "priority", "subject", "updated_on", "category"]
143
144 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 3
145 q = Query.find_by_name('test_edit_global_private_query')
146 assert !q.is_public?
147 assert q.has_default_columns?
148 assert q.valid?
149 end
150
151 def test_get_edit_project_private_query
152 @request.session[:user_id] = 3
153 get :edit, :id => 2
154 assert_response :success
155 assert_template 'edit'
156 assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
157 :name => 'query[is_public]' }
158 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
159 :name => 'query_is_for_all',
160 :checked => nil,
161 :disabled => nil }
162 end
163
164 def test_get_edit_project_public_query
165 @request.session[:user_id] = 2
166 get :edit, :id => 1
167 assert_response :success
168 assert_template 'edit'
169 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
170 :name => 'query[is_public]',
171 :checked => 'checked'
172 }
173 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
174 :name => 'query_is_for_all',
175 :checked => nil,
176 :disabled => 'disabled' }
177 end
178
179 def test_destroy
180 @request.session[:user_id] = 2
181 post :destroy, :id => 1
182 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :set_filter => 1, :query_id => nil
183 assert_nil Query.find_by_id(1)
184 end
185 end
@@ -1,410 +1,417
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 layout 'base'
19 layout 'base'
20 menu_item :new_issue, :only => :new
20 menu_item :new_issue, :only => :new
21
21
22 before_filter :find_issue, :only => [:show, :edit, :destroy_attachment]
22 before_filter :find_issue, :only => [:show, :edit, :destroy_attachment]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
24 before_filter :find_project, :only => [:new, :update_form, :preview]
24 before_filter :find_project, :only => [:new, :update_form, :preview]
25 before_filter :authorize, :except => [:index, :changes, :preview, :update_form, :context_menu]
25 before_filter :authorize, :except => [:index, :changes, :preview, :update_form, :context_menu]
26 before_filter :find_optional_project, :only => [:index, :changes]
26 before_filter :find_optional_project, :only => [:index, :changes]
27 accept_key_auth :index, :changes
27 accept_key_auth :index, :changes
28
28
29 helper :journals
29 helper :journals
30 helper :projects
30 helper :projects
31 include ProjectsHelper
31 include ProjectsHelper
32 helper :custom_fields
32 helper :custom_fields
33 include CustomFieldsHelper
33 include CustomFieldsHelper
34 helper :ifpdf
34 helper :ifpdf
35 include IfpdfHelper
35 include IfpdfHelper
36 helper :issue_relations
36 helper :issue_relations
37 include IssueRelationsHelper
37 include IssueRelationsHelper
38 helper :watchers
38 helper :watchers
39 include WatchersHelper
39 include WatchersHelper
40 helper :attachments
40 helper :attachments
41 include AttachmentsHelper
41 include AttachmentsHelper
42 helper :queries
42 helper :queries
43 helper :sort
43 helper :sort
44 include SortHelper
44 include SortHelper
45 include IssuesHelper
45 include IssuesHelper
46
46
47 def index
47 def index
48 sort_init "#{Issue.table_name}.id", "desc"
48 sort_init "#{Issue.table_name}.id", "desc"
49 sort_update
49 sort_update
50 retrieve_query
50 retrieve_query
51 if @query.valid?
51 if @query.valid?
52 limit = per_page_option
52 limit = per_page_option
53 respond_to do |format|
53 respond_to do |format|
54 format.html { }
54 format.html { }
55 format.atom { }
55 format.atom { }
56 format.csv { limit = Setting.issues_export_limit.to_i }
56 format.csv { limit = Setting.issues_export_limit.to_i }
57 format.pdf { limit = Setting.issues_export_limit.to_i }
57 format.pdf { limit = Setting.issues_export_limit.to_i }
58 end
58 end
59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
61 @issues = Issue.find :all, :order => sort_clause,
61 @issues = Issue.find :all, :order => sort_clause,
62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
63 :conditions => @query.statement,
63 :conditions => @query.statement,
64 :limit => limit,
64 :limit => limit,
65 :offset => @issue_pages.current.offset
65 :offset => @issue_pages.current.offset
66 respond_to do |format|
66 respond_to do |format|
67 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
67 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
68 format.atom { render_feed(@issues, :title => l(:label_issue_plural)) }
68 format.atom { render_feed(@issues, :title => l(:label_issue_plural)) }
69 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
69 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
70 format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') }
70 format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') }
71 end
71 end
72 else
72 else
73 # Send html if the query is not valid
73 # Send html if the query is not valid
74 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
74 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
75 end
75 end
76 rescue ActiveRecord::RecordNotFound
77 render_404
76 end
78 end
77
79
78 def changes
80 def changes
79 sort_init "#{Issue.table_name}.id", "desc"
81 sort_init "#{Issue.table_name}.id", "desc"
80 sort_update
82 sort_update
81 retrieve_query
83 retrieve_query
82 if @query.valid?
84 if @query.valid?
83 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
85 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
84 :conditions => @query.statement,
86 :conditions => @query.statement,
85 :limit => 25,
87 :limit => 25,
86 :order => "#{Journal.table_name}.created_on DESC"
88 :order => "#{Journal.table_name}.created_on DESC"
87 end
89 end
88 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
90 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
89 render :layout => false, :content_type => 'application/atom+xml'
91 render :layout => false, :content_type => 'application/atom+xml'
92 rescue ActiveRecord::RecordNotFound
93 render_404
90 end
94 end
91
95
92 def show
96 def show
93 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) }
97 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) }
94 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
98 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
95 @journals.each_with_index {|j,i| j.indice = i+1}
99 @journals.each_with_index {|j,i| j.indice = i+1}
96 @journals.reverse! if User.current.wants_comments_in_reverse_order?
100 @journals.reverse! if User.current.wants_comments_in_reverse_order?
97 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
101 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
98 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
102 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
99 @activities = Enumeration::get_values('ACTI')
103 @activities = Enumeration::get_values('ACTI')
100 @priorities = Enumeration::get_values('IPRI')
104 @priorities = Enumeration::get_values('IPRI')
101 respond_to do |format|
105 respond_to do |format|
102 format.html { render :template => 'issues/show.rhtml' }
106 format.html { render :template => 'issues/show.rhtml' }
103 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
107 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
104 format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
108 format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
105 end
109 end
106 end
110 end
107
111
108 # Add a new issue
112 # Add a new issue
109 # The new issue will be created from an existing one if copy_from parameter is given
113 # The new issue will be created from an existing one if copy_from parameter is given
110 def new
114 def new
111 @issue = params[:copy_from] ? Issue.new.copy_from(params[:copy_from]) : Issue.new(params[:issue])
115 @issue = params[:copy_from] ? Issue.new.copy_from(params[:copy_from]) : Issue.new(params[:issue])
112 @issue.project = @project
116 @issue.project = @project
113 @issue.author = User.current
117 @issue.author = User.current
114 @issue.tracker ||= @project.trackers.find(params[:tracker_id] ? params[:tracker_id] : :first)
118 @issue.tracker ||= @project.trackers.find(params[:tracker_id] ? params[:tracker_id] : :first)
115 if @issue.tracker.nil?
119 if @issue.tracker.nil?
116 flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
120 flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
117 render :nothing => true, :layout => true
121 render :nothing => true, :layout => true
118 return
122 return
119 end
123 end
120
124
121 default_status = IssueStatus.default
125 default_status = IssueStatus.default
122 unless default_status
126 unless default_status
123 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
127 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
124 render :nothing => true, :layout => true
128 render :nothing => true, :layout => true
125 return
129 return
126 end
130 end
127 @issue.status = default_status
131 @issue.status = default_status
128 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
132 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
129
133
130 if request.get? || request.xhr?
134 if request.get? || request.xhr?
131 @issue.start_date ||= Date.today
135 @issue.start_date ||= Date.today
132 @custom_values = @issue.custom_values.empty? ?
136 @custom_values = @issue.custom_values.empty? ?
133 @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue) } :
137 @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue) } :
134 @issue.custom_values
138 @issue.custom_values
135 else
139 else
136 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
140 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
137 # Check that the user is allowed to apply the requested status
141 # Check that the user is allowed to apply the requested status
138 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
142 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
139 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
143 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
140 @issue.custom_values = @custom_values
144 @issue.custom_values = @custom_values
141 if @issue.save
145 if @issue.save
142 attach_files(@issue, params[:attachments])
146 attach_files(@issue, params[:attachments])
143 flash[:notice] = l(:notice_successful_create)
147 flash[:notice] = l(:notice_successful_create)
144 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
148 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
145 redirect_to :controller => 'issues', :action => 'show', :id => @issue, :project_id => @project
149 redirect_to :controller => 'issues', :action => 'show', :id => @issue, :project_id => @project
146 return
150 return
147 end
151 end
148 end
152 end
149 @priorities = Enumeration::get_values('IPRI')
153 @priorities = Enumeration::get_values('IPRI')
150 render :layout => !request.xhr?
154 render :layout => !request.xhr?
151 end
155 end
152
156
153 # Attributes that can be updated on workflow transition (without :edit permission)
157 # Attributes that can be updated on workflow transition (without :edit permission)
154 # TODO: make it configurable (at least per role)
158 # TODO: make it configurable (at least per role)
155 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
159 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
156
160
157 def edit
161 def edit
158 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
162 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
159 @activities = Enumeration::get_values('ACTI')
163 @activities = Enumeration::get_values('ACTI')
160 @priorities = Enumeration::get_values('IPRI')
164 @priorities = Enumeration::get_values('IPRI')
161 @custom_values = []
165 @custom_values = []
162 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
166 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
163
167
164 @notes = params[:notes]
168 @notes = params[:notes]
165 journal = @issue.init_journal(User.current, @notes)
169 journal = @issue.init_journal(User.current, @notes)
166 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
170 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
167 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
171 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
168 attrs = params[:issue].dup
172 attrs = params[:issue].dup
169 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
173 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
170 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
174 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
171 @issue.attributes = attrs
175 @issue.attributes = attrs
172 end
176 end
173
177
174 if request.get?
178 if request.get?
175 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) }
179 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) }
176 else
180 else
177 # Update custom fields if user has :edit permission
181 # Update custom fields if user has :edit permission
178 if @edit_allowed && params[:custom_fields]
182 if @edit_allowed && params[:custom_fields]
179 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
183 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
180 @issue.custom_values = @custom_values
184 @issue.custom_values = @custom_values
181 end
185 end
182 attachments = attach_files(@issue, params[:attachments])
186 attachments = attach_files(@issue, params[:attachments])
183 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
187 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
184 if @issue.save
188 if @issue.save
185 # Log spend time
189 # Log spend time
186 if current_role.allowed_to?(:log_time)
190 if current_role.allowed_to?(:log_time)
187 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
191 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
188 @time_entry.attributes = params[:time_entry]
192 @time_entry.attributes = params[:time_entry]
189 @time_entry.save
193 @time_entry.save
190 end
194 end
191 if !journal.new_record?
195 if !journal.new_record?
192 # Only send notification if something was actually changed
196 # Only send notification if something was actually changed
193 flash[:notice] = l(:notice_successful_update)
197 flash[:notice] = l(:notice_successful_update)
194 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
198 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
195 end
199 end
196 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
200 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
197 end
201 end
198 end
202 end
199 rescue ActiveRecord::StaleObjectError
203 rescue ActiveRecord::StaleObjectError
200 # Optimistic locking exception
204 # Optimistic locking exception
201 flash.now[:error] = l(:notice_locking_conflict)
205 flash.now[:error] = l(:notice_locking_conflict)
202 end
206 end
203
207
204 # Bulk edit a set of issues
208 # Bulk edit a set of issues
205 def bulk_edit
209 def bulk_edit
206 if request.post?
210 if request.post?
207 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
211 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
208 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
212 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
209 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
213 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
210 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
214 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
211 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
215 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
212
216
213 unsaved_issue_ids = []
217 unsaved_issue_ids = []
214 @issues.each do |issue|
218 @issues.each do |issue|
215 journal = issue.init_journal(User.current, params[:notes])
219 journal = issue.init_journal(User.current, params[:notes])
216 issue.priority = priority if priority
220 issue.priority = priority if priority
217 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
221 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
218 issue.category = category if category || params[:category_id] == 'none'
222 issue.category = category if category || params[:category_id] == 'none'
219 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
223 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
220 issue.start_date = params[:start_date] unless params[:start_date].blank?
224 issue.start_date = params[:start_date] unless params[:start_date].blank?
221 issue.due_date = params[:due_date] unless params[:due_date].blank?
225 issue.due_date = params[:due_date] unless params[:due_date].blank?
222 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
226 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
223 # Don't save any change to the issue if the user is not authorized to apply the requested status
227 # Don't save any change to the issue if the user is not authorized to apply the requested status
224 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
228 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
225 # Send notification for each issue (if changed)
229 # Send notification for each issue (if changed)
226 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
230 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
227 else
231 else
228 # Keep unsaved issue ids to display them in flash error
232 # Keep unsaved issue ids to display them in flash error
229 unsaved_issue_ids << issue.id
233 unsaved_issue_ids << issue.id
230 end
234 end
231 end
235 end
232 if unsaved_issue_ids.empty?
236 if unsaved_issue_ids.empty?
233 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
237 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
234 else
238 else
235 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
239 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
236 end
240 end
237 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
241 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
238 return
242 return
239 end
243 end
240 # Find potential statuses the user could be allowed to switch issues to
244 # Find potential statuses the user could be allowed to switch issues to
241 @available_statuses = Workflow.find(:all, :include => :new_status,
245 @available_statuses = Workflow.find(:all, :include => :new_status,
242 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq
246 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq
243 end
247 end
244
248
245 def move
249 def move
246 @allowed_projects = []
250 @allowed_projects = []
247 # find projects to which the user is allowed to move the issue
251 # find projects to which the user is allowed to move the issue
248 if User.current.admin?
252 if User.current.admin?
249 # admin is allowed to move issues to any active (visible) project
253 # admin is allowed to move issues to any active (visible) project
250 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
254 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
251 else
255 else
252 User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
256 User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
253 end
257 end
254 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
258 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
255 @target_project ||= @project
259 @target_project ||= @project
256 @trackers = @target_project.trackers
260 @trackers = @target_project.trackers
257 if request.post?
261 if request.post?
258 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
262 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
259 unsaved_issue_ids = []
263 unsaved_issue_ids = []
260 @issues.each do |issue|
264 @issues.each do |issue|
261 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
265 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
262 end
266 end
263 if unsaved_issue_ids.empty?
267 if unsaved_issue_ids.empty?
264 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
268 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
265 else
269 else
266 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
270 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
267 end
271 end
268 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
272 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
269 return
273 return
270 end
274 end
271 render :layout => false if request.xhr?
275 render :layout => false if request.xhr?
272 end
276 end
273
277
274 def destroy
278 def destroy
275 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
279 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
276 if @hours > 0
280 if @hours > 0
277 case params[:todo]
281 case params[:todo]
278 when 'destroy'
282 when 'destroy'
279 # nothing to do
283 # nothing to do
280 when 'nullify'
284 when 'nullify'
281 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
285 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
282 when 'reassign'
286 when 'reassign'
283 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
287 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
284 if reassign_to.nil?
288 if reassign_to.nil?
285 flash.now[:error] = l(:error_issue_not_found_in_project)
289 flash.now[:error] = l(:error_issue_not_found_in_project)
286 return
290 return
287 else
291 else
288 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
292 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
289 end
293 end
290 else
294 else
291 # display the destroy form
295 # display the destroy form
292 return
296 return
293 end
297 end
294 end
298 end
295 @issues.each(&:destroy)
299 @issues.each(&:destroy)
296 redirect_to :action => 'index', :project_id => @project
300 redirect_to :action => 'index', :project_id => @project
297 end
301 end
298
302
299 def destroy_attachment
303 def destroy_attachment
300 a = @issue.attachments.find(params[:attachment_id])
304 a = @issue.attachments.find(params[:attachment_id])
301 a.destroy
305 a.destroy
302 journal = @issue.init_journal(User.current)
306 journal = @issue.init_journal(User.current)
303 journal.details << JournalDetail.new(:property => 'attachment',
307 journal.details << JournalDetail.new(:property => 'attachment',
304 :prop_key => a.id,
308 :prop_key => a.id,
305 :old_value => a.filename)
309 :old_value => a.filename)
306 journal.save
310 journal.save
307 redirect_to :action => 'show', :id => @issue
311 redirect_to :action => 'show', :id => @issue
308 end
312 end
309
313
310 def context_menu
314 def context_menu
311 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
315 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
312 if (@issues.size == 1)
316 if (@issues.size == 1)
313 @issue = @issues.first
317 @issue = @issues.first
314 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
318 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
315 @assignables = @issue.assignable_users
319 @assignables = @issue.assignable_users
316 @assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
320 @assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
317 end
321 end
318 projects = @issues.collect(&:project).compact.uniq
322 projects = @issues.collect(&:project).compact.uniq
319 @project = projects.first if projects.size == 1
323 @project = projects.first if projects.size == 1
320
324
321 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
325 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
322 :update => (@issue && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && !@allowed_statuses.empty?))),
326 :update => (@issue && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && !@allowed_statuses.empty?))),
323 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
327 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
324 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
328 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
325 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
329 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
326 }
330 }
327
331
328 @priorities = Enumeration.get_values('IPRI').reverse
332 @priorities = Enumeration.get_values('IPRI').reverse
329 @statuses = IssueStatus.find(:all, :order => 'position')
333 @statuses = IssueStatus.find(:all, :order => 'position')
330 @back = request.env['HTTP_REFERER']
334 @back = request.env['HTTP_REFERER']
331
335
332 render :layout => false
336 render :layout => false
333 end
337 end
334
338
335 def update_form
339 def update_form
336 @issue = Issue.new(params[:issue])
340 @issue = Issue.new(params[:issue])
337 render :action => :new, :layout => false
341 render :action => :new, :layout => false
338 end
342 end
339
343
340 def preview
344 def preview
341 issue = @project.issues.find_by_id(params[:id])
345 issue = @project.issues.find_by_id(params[:id])
342 @attachements = issue.attachments if issue
346 @attachements = issue.attachments if issue
343 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
347 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
344 render :partial => 'common/preview'
348 render :partial => 'common/preview'
345 end
349 end
346
350
347 private
351 private
348 def find_issue
352 def find_issue
349 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
353 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
350 @project = @issue.project
354 @project = @issue.project
351 rescue ActiveRecord::RecordNotFound
355 rescue ActiveRecord::RecordNotFound
352 render_404
356 render_404
353 end
357 end
354
358
355 # Filter for bulk operations
359 # Filter for bulk operations
356 def find_issues
360 def find_issues
357 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
361 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
358 raise ActiveRecord::RecordNotFound if @issues.empty?
362 raise ActiveRecord::RecordNotFound if @issues.empty?
359 projects = @issues.collect(&:project).compact.uniq
363 projects = @issues.collect(&:project).compact.uniq
360 if projects.size == 1
364 if projects.size == 1
361 @project = projects.first
365 @project = projects.first
362 else
366 else
363 # TODO: let users bulk edit/move/destroy issues from different projects
367 # TODO: let users bulk edit/move/destroy issues from different projects
364 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
368 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
365 end
369 end
366 rescue ActiveRecord::RecordNotFound
370 rescue ActiveRecord::RecordNotFound
367 render_404
371 render_404
368 end
372 end
369
373
370 def find_project
374 def find_project
371 @project = Project.find(params[:project_id])
375 @project = Project.find(params[:project_id])
372 rescue ActiveRecord::RecordNotFound
376 rescue ActiveRecord::RecordNotFound
373 render_404
377 render_404
374 end
378 end
375
379
376 def find_optional_project
380 def find_optional_project
377 return true unless params[:project_id]
381 return true unless params[:project_id]
378 @project = Project.find(params[:project_id])
382 @project = Project.find(params[:project_id])
379 authorize
383 authorize
380 rescue ActiveRecord::RecordNotFound
384 rescue ActiveRecord::RecordNotFound
381 render_404
385 render_404
382 end
386 end
383
387
384 # Retrieve query from session or build a new query
388 # Retrieve query from session or build a new query
385 def retrieve_query
389 def retrieve_query
386 if !params[:query_id].blank?
390 if !params[:query_id].blank?
387 @query = Query.find(params[:query_id], :conditions => {:project_id => (@project ? @project.id : nil)})
391 cond = "project_id IS NULL"
392 cond << " OR project_id = #{@project.id}" if @project
393 @query = Query.find(params[:query_id], :conditions => cond)
394 @query.project = @project
388 session[:query] = {:id => @query.id, :project_id => @query.project_id}
395 session[:query] = {:id => @query.id, :project_id => @query.project_id}
389 else
396 else
390 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
397 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
391 # Give it a name, required to be valid
398 # Give it a name, required to be valid
392 @query = Query.new(:name => "_")
399 @query = Query.new(:name => "_")
393 @query.project = @project
400 @query.project = @project
394 if params[:fields] and params[:fields].is_a? Array
401 if params[:fields] and params[:fields].is_a? Array
395 params[:fields].each do |field|
402 params[:fields].each do |field|
396 @query.add_filter(field, params[:operators][field], params[:values][field])
403 @query.add_filter(field, params[:operators][field], params[:values][field])
397 end
404 end
398 else
405 else
399 @query.available_filters.keys.each do |field|
406 @query.available_filters.keys.each do |field|
400 @query.add_short_filter(field, params[field]) if params[field]
407 @query.add_short_filter(field, params[field]) if params[field]
401 end
408 end
402 end
409 end
403 session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
410 session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
404 else
411 else
405 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
412 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
406 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
413 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
407 end
414 end
408 end
415 end
409 end
416 end
410 end
417 end
@@ -1,82 +1,80
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class QueriesController < ApplicationController
18 class QueriesController < ApplicationController
19 layout 'base'
19 layout 'base'
20 menu_item :issues
20 menu_item :issues
21 before_filter :find_project, :authorize
21 before_filter :find_query, :except => :new
22
22 before_filter :find_project, :authorize, :only => :new
23 def index
24 @queries = @project.queries.find(:all,
25 :order => "name ASC",
26 :conditions => ["is_public = ? or user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
27 end
28
23
29 def new
24 def new
30 @query = Query.new(params[:query])
25 @query = Query.new(params[:query])
31 @query.project = @project
26 @query.project = params[:query_is_for_all] ? nil : @project
32 @query.user = User.current
27 @query.user = User.current
33 @query.is_public = false unless current_role.allowed_to?(:manage_public_queries)
28 @query.is_public = false unless (@query.project && current_role.allowed_to?(:manage_public_queries)) || User.current.admin?
34 @query.column_names = nil if params[:default_columns]
29 @query.column_names = nil if params[:default_columns]
35
30
36 params[:fields].each do |field|
31 params[:fields].each do |field|
37 @query.add_filter(field, params[:operators][field], params[:values][field])
32 @query.add_filter(field, params[:operators][field], params[:values][field])
38 end if params[:fields]
33 end if params[:fields]
39
34
40 if request.post? && params[:confirm] && @query.save
35 if request.post? && params[:confirm] && @query.save
41 flash[:notice] = l(:notice_successful_create)
36 flash[:notice] = l(:notice_successful_create)
42 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
37 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
43 return
38 return
44 end
39 end
45 render :layout => false if request.xhr?
40 render :layout => false if request.xhr?
46 end
41 end
47
42
48 def edit
43 def edit
49 if request.post?
44 if request.post?
50 @query.filters = {}
45 @query.filters = {}
51 params[:fields].each do |field|
46 params[:fields].each do |field|
52 @query.add_filter(field, params[:operators][field], params[:values][field])
47 @query.add_filter(field, params[:operators][field], params[:values][field])
53 end if params[:fields]
48 end if params[:fields]
54 @query.attributes = params[:query]
49 @query.attributes = params[:query]
55 @query.is_public = false unless current_role.allowed_to?(:manage_public_queries)
50 @query.project = nil if params[:query_is_for_all]
51 @query.is_public = false unless (@query.project && current_role.allowed_to?(:manage_public_queries)) || User.current.admin?
56 @query.column_names = nil if params[:default_columns]
52 @query.column_names = nil if params[:default_columns]
57
53
58 if @query.save
54 if @query.save
59 flash[:notice] = l(:notice_successful_update)
55 flash[:notice] = l(:notice_successful_update)
60 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
56 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
61 end
57 end
62 end
58 end
63 end
59 end
64
60
65 def destroy
61 def destroy
66 @query.destroy if request.post?
62 @query.destroy if request.post?
67 redirect_to :controller => 'queries', :project_id => @project
63 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1
68 end
64 end
69
65
70 private
66 private
67 def find_query
68 @query = Query.find(params[:id])
69 @project = @query.project
70 render_403 unless @query.editable_by?(User.current)
71 rescue ActiveRecord::RecordNotFound
72 render_404
73 end
74
71 def find_project
75 def find_project
72 if params[:id]
76 @project = Project.find(params[:project_id])
73 @query = Query.find(params[:id])
74 @project = @query.project
75 render_403 unless @query.editable_by?(User.current)
76 else
77 @project = Project.find(params[:project_id])
78 end
79 rescue ActiveRecord::RecordNotFound
77 rescue ActiveRecord::RecordNotFound
80 render_404
78 render_404
81 end
79 end
82 end
80 end
@@ -1,164 +1,177
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'csv'
18 require 'csv'
19
19
20 module IssuesHelper
20 module IssuesHelper
21 include ApplicationHelper
21 include ApplicationHelper
22
22
23 def render_issue_tooltip(issue)
23 def render_issue_tooltip(issue)
24 @cached_label_start_date ||= l(:field_start_date)
24 @cached_label_start_date ||= l(:field_start_date)
25 @cached_label_due_date ||= l(:field_due_date)
25 @cached_label_due_date ||= l(:field_due_date)
26 @cached_label_assigned_to ||= l(:field_assigned_to)
26 @cached_label_assigned_to ||= l(:field_assigned_to)
27 @cached_label_priority ||= l(:field_priority)
27 @cached_label_priority ||= l(:field_priority)
28
28
29 link_to_issue(issue) + ": #{h(issue.subject)}<br /><br />" +
29 link_to_issue(issue) + ": #{h(issue.subject)}<br /><br />" +
30 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
30 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
31 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
31 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
32 "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
32 "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
33 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
33 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
34 end
34 end
35
36 def sidebar_queries
37 unless @sidebar_queries
38 # User can see public queries and his own queries
39 visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
40 # Project specific queries and global queries
41 visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
42 @sidebar_queries = Query.find(:all,
43 :order => "name ASC",
44 :conditions => visible.conditions)
45 end
46 @sidebar_queries
47 end
35
48
36 def show_detail(detail, no_html=false)
49 def show_detail(detail, no_html=false)
37 case detail.property
50 case detail.property
38 when 'attr'
51 when 'attr'
39 label = l(("field_" + detail.prop_key.to_s.gsub(/\_id$/, "")).to_sym)
52 label = l(("field_" + detail.prop_key.to_s.gsub(/\_id$/, "")).to_sym)
40 case detail.prop_key
53 case detail.prop_key
41 when 'due_date', 'start_date'
54 when 'due_date', 'start_date'
42 value = format_date(detail.value.to_date) if detail.value
55 value = format_date(detail.value.to_date) if detail.value
43 old_value = format_date(detail.old_value.to_date) if detail.old_value
56 old_value = format_date(detail.old_value.to_date) if detail.old_value
44 when 'status_id'
57 when 'status_id'
45 s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value
58 s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value
46 s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value
59 s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value
47 when 'assigned_to_id'
60 when 'assigned_to_id'
48 u = User.find_by_id(detail.value) and value = u.name if detail.value
61 u = User.find_by_id(detail.value) and value = u.name if detail.value
49 u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value
62 u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value
50 when 'priority_id'
63 when 'priority_id'
51 e = Enumeration.find_by_id(detail.value) and value = e.name if detail.value
64 e = Enumeration.find_by_id(detail.value) and value = e.name if detail.value
52 e = Enumeration.find_by_id(detail.old_value) and old_value = e.name if detail.old_value
65 e = Enumeration.find_by_id(detail.old_value) and old_value = e.name if detail.old_value
53 when 'category_id'
66 when 'category_id'
54 c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
67 c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
55 c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
68 c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
56 when 'fixed_version_id'
69 when 'fixed_version_id'
57 v = Version.find_by_id(detail.value) and value = v.name if detail.value
70 v = Version.find_by_id(detail.value) and value = v.name if detail.value
58 v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value
71 v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value
59 end
72 end
60 when 'cf'
73 when 'cf'
61 custom_field = CustomField.find_by_id(detail.prop_key)
74 custom_field = CustomField.find_by_id(detail.prop_key)
62 if custom_field
75 if custom_field
63 label = custom_field.name
76 label = custom_field.name
64 value = format_value(detail.value, custom_field.field_format) if detail.value
77 value = format_value(detail.value, custom_field.field_format) if detail.value
65 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
78 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
66 end
79 end
67 when 'attachment'
80 when 'attachment'
68 label = l(:label_attachment)
81 label = l(:label_attachment)
69 end
82 end
70
83
71 label ||= detail.prop_key
84 label ||= detail.prop_key
72 value ||= detail.value
85 value ||= detail.value
73 old_value ||= detail.old_value
86 old_value ||= detail.old_value
74
87
75 unless no_html
88 unless no_html
76 label = content_tag('strong', label)
89 label = content_tag('strong', label)
77 old_value = content_tag("i", h(old_value)) if detail.old_value
90 old_value = content_tag("i", h(old_value)) if detail.old_value
78 old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
91 old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
79 if detail.property == 'attachment' && !value.blank? && Attachment.find_by_id(detail.prop_key)
92 if detail.property == 'attachment' && !value.blank? && Attachment.find_by_id(detail.prop_key)
80 # Link to the attachment if it has not been removed
93 # Link to the attachment if it has not been removed
81 value = link_to(value, :controller => 'attachments', :action => 'download', :id => detail.prop_key)
94 value = link_to(value, :controller => 'attachments', :action => 'download', :id => detail.prop_key)
82 else
95 else
83 value = content_tag("i", h(value)) if value
96 value = content_tag("i", h(value)) if value
84 end
97 end
85 end
98 end
86
99
87 if !detail.value.blank?
100 if !detail.value.blank?
88 case detail.property
101 case detail.property
89 when 'attr', 'cf'
102 when 'attr', 'cf'
90 if !detail.old_value.blank?
103 if !detail.old_value.blank?
91 label + " " + l(:text_journal_changed, old_value, value)
104 label + " " + l(:text_journal_changed, old_value, value)
92 else
105 else
93 label + " " + l(:text_journal_set_to, value)
106 label + " " + l(:text_journal_set_to, value)
94 end
107 end
95 when 'attachment'
108 when 'attachment'
96 "#{label} #{value} #{l(:label_added)}"
109 "#{label} #{value} #{l(:label_added)}"
97 end
110 end
98 else
111 else
99 case detail.property
112 case detail.property
100 when 'attr', 'cf'
113 when 'attr', 'cf'
101 label + " " + l(:text_journal_deleted) + " (#{old_value})"
114 label + " " + l(:text_journal_deleted) + " (#{old_value})"
102 when 'attachment'
115 when 'attachment'
103 "#{label} #{old_value} #{l(:label_deleted)}"
116 "#{label} #{old_value} #{l(:label_deleted)}"
104 end
117 end
105 end
118 end
106 end
119 end
107
120
108 def issues_to_csv(issues, project = nil)
121 def issues_to_csv(issues, project = nil)
109 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
122 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
110 export = StringIO.new
123 export = StringIO.new
111 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
124 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
112 # csv header fields
125 # csv header fields
113 headers = [ "#",
126 headers = [ "#",
114 l(:field_status),
127 l(:field_status),
115 l(:field_project),
128 l(:field_project),
116 l(:field_tracker),
129 l(:field_tracker),
117 l(:field_priority),
130 l(:field_priority),
118 l(:field_subject),
131 l(:field_subject),
119 l(:field_assigned_to),
132 l(:field_assigned_to),
120 l(:field_category),
133 l(:field_category),
121 l(:field_fixed_version),
134 l(:field_fixed_version),
122 l(:field_author),
135 l(:field_author),
123 l(:field_start_date),
136 l(:field_start_date),
124 l(:field_due_date),
137 l(:field_due_date),
125 l(:field_done_ratio),
138 l(:field_done_ratio),
126 l(:field_estimated_hours),
139 l(:field_estimated_hours),
127 l(:field_created_on),
140 l(:field_created_on),
128 l(:field_updated_on)
141 l(:field_updated_on)
129 ]
142 ]
130 # Export project custom fields if project is given
143 # Export project custom fields if project is given
131 # otherwise export custom fields marked as "For all projects"
144 # otherwise export custom fields marked as "For all projects"
132 custom_fields = project.nil? ? IssueCustomField.for_all : project.all_custom_fields
145 custom_fields = project.nil? ? IssueCustomField.for_all : project.all_custom_fields
133 custom_fields.each {|f| headers << f.name}
146 custom_fields.each {|f| headers << f.name}
134 # Description in the last column
147 # Description in the last column
135 headers << l(:field_description)
148 headers << l(:field_description)
136 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
149 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
137 # csv lines
150 # csv lines
138 issues.each do |issue|
151 issues.each do |issue|
139 fields = [issue.id,
152 fields = [issue.id,
140 issue.status.name,
153 issue.status.name,
141 issue.project.name,
154 issue.project.name,
142 issue.tracker.name,
155 issue.tracker.name,
143 issue.priority.name,
156 issue.priority.name,
144 issue.subject,
157 issue.subject,
145 issue.assigned_to,
158 issue.assigned_to,
146 issue.category,
159 issue.category,
147 issue.fixed_version,
160 issue.fixed_version,
148 issue.author.name,
161 issue.author.name,
149 format_date(issue.start_date),
162 format_date(issue.start_date),
150 format_date(issue.due_date),
163 format_date(issue.due_date),
151 issue.done_ratio,
164 issue.done_ratio,
152 issue.estimated_hours,
165 issue.estimated_hours,
153 format_time(issue.created_on),
166 format_time(issue.created_on),
154 format_time(issue.updated_on)
167 format_time(issue.updated_on)
155 ]
168 ]
156 custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
169 custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
157 fields << issue.description
170 fields << issue.description
158 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
171 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
159 end
172 end
160 end
173 end
161 export.rewind
174 export.rewind
162 export
175 export
163 end
176 end
164 end
177 end
@@ -1,361 +1,368
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :default_order
19 attr_accessor :name, :sortable, :default_order
20 include GLoc
20 include GLoc
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.default_order = options[:default_order]
25 self.default_order = options[:default_order]
26 end
26 end
27
27
28 def caption
28 def caption
29 set_language_if_valid(User.current.language)
29 set_language_if_valid(User.current.language)
30 l("field_#{name}")
30 l("field_#{name}")
31 end
31 end
32 end
32 end
33
33
34 class QueryCustomFieldColumn < QueryColumn
34 class QueryCustomFieldColumn < QueryColumn
35
35
36 def initialize(custom_field)
36 def initialize(custom_field)
37 self.name = "cf_#{custom_field.id}".to_sym
37 self.name = "cf_#{custom_field.id}".to_sym
38 self.sortable = false
38 self.sortable = false
39 @cf = custom_field
39 @cf = custom_field
40 end
40 end
41
41
42 def caption
42 def caption
43 @cf.name
43 @cf.name
44 end
44 end
45
45
46 def custom_field
46 def custom_field
47 @cf
47 @cf
48 end
48 end
49 end
49 end
50
50
51 class Query < ActiveRecord::Base
51 class Query < ActiveRecord::Base
52 belongs_to :project
52 belongs_to :project
53 belongs_to :user
53 belongs_to :user
54 serialize :filters
54 serialize :filters
55 serialize :column_names
55 serialize :column_names
56
56
57 attr_protected :project_id, :user_id
57 attr_protected :project_id, :user_id
58
58
59 validates_presence_of :name, :on => :save
59 validates_presence_of :name, :on => :save
60 validates_length_of :name, :maximum => 255
60 validates_length_of :name, :maximum => 255
61
61
62 @@operators = { "=" => :label_equals,
62 @@operators = { "=" => :label_equals,
63 "!" => :label_not_equals,
63 "!" => :label_not_equals,
64 "o" => :label_open_issues,
64 "o" => :label_open_issues,
65 "c" => :label_closed_issues,
65 "c" => :label_closed_issues,
66 "!*" => :label_none,
66 "!*" => :label_none,
67 "*" => :label_all,
67 "*" => :label_all,
68 ">=" => '>=',
68 ">=" => '>=',
69 "<=" => '<=',
69 "<=" => '<=',
70 "<t+" => :label_in_less_than,
70 "<t+" => :label_in_less_than,
71 ">t+" => :label_in_more_than,
71 ">t+" => :label_in_more_than,
72 "t+" => :label_in,
72 "t+" => :label_in,
73 "t" => :label_today,
73 "t" => :label_today,
74 "w" => :label_this_week,
74 "w" => :label_this_week,
75 ">t-" => :label_less_than_ago,
75 ">t-" => :label_less_than_ago,
76 "<t-" => :label_more_than_ago,
76 "<t-" => :label_more_than_ago,
77 "t-" => :label_ago,
77 "t-" => :label_ago,
78 "~" => :label_contains,
78 "~" => :label_contains,
79 "!~" => :label_not_contains }
79 "!~" => :label_not_contains }
80
80
81 cattr_reader :operators
81 cattr_reader :operators
82
82
83 @@operators_by_filter_type = { :list => [ "=", "!" ],
83 @@operators_by_filter_type = { :list => [ "=", "!" ],
84 :list_status => [ "o", "=", "!", "c", "*" ],
84 :list_status => [ "o", "=", "!", "c", "*" ],
85 :list_optional => [ "=", "!", "!*", "*" ],
85 :list_optional => [ "=", "!", "!*", "*" ],
86 :list_subprojects => [ "*", "!*", "=" ],
86 :list_subprojects => [ "*", "!*", "=" ],
87 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
87 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
88 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
88 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
89 :string => [ "=", "~", "!", "!~" ],
89 :string => [ "=", "~", "!", "!~" ],
90 :text => [ "~", "!~" ],
90 :text => [ "~", "!~" ],
91 :integer => [ "=", ">=", "<=" ] }
91 :integer => [ "=", ">=", "<=" ] }
92
92
93 cattr_reader :operators_by_filter_type
93 cattr_reader :operators_by_filter_type
94
94
95 @@available_columns = [
95 @@available_columns = [
96 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"),
96 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"),
97 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"),
97 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"),
98 QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'),
98 QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'),
99 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
99 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
100 QueryColumn.new(:author),
100 QueryColumn.new(:author),
101 QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"),
101 QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"),
102 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
102 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
103 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"),
103 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"),
104 QueryColumn.new(:fixed_version, :sortable => "#{Version.table_name}.effective_date", :default_order => 'desc'),
104 QueryColumn.new(:fixed_version, :sortable => "#{Version.table_name}.effective_date", :default_order => 'desc'),
105 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
105 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
106 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
106 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
107 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
107 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
108 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"),
108 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"),
109 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
109 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
110 ]
110 ]
111 cattr_reader :available_columns
111 cattr_reader :available_columns
112
112
113 def initialize(attributes = nil)
113 def initialize(attributes = nil)
114 super attributes
114 super attributes
115 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
115 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
116 set_language_if_valid(User.current.language)
116 set_language_if_valid(User.current.language)
117 end
117 end
118
118
119 def after_initialize
120 # Store the fact that project is nil (used in #editable_by?)
121 @is_for_all = project.nil?
122 end
123
119 def validate
124 def validate
120 filters.each_key do |field|
125 filters.each_key do |field|
121 errors.add label_for(field), :activerecord_error_blank unless
126 errors.add label_for(field), :activerecord_error_blank unless
122 # filter requires one or more values
127 # filter requires one or more values
123 (values_for(field) and !values_for(field).first.empty?) or
128 (values_for(field) and !values_for(field).first.empty?) or
124 # filter doesn't require any value
129 # filter doesn't require any value
125 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
130 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
126 end if filters
131 end if filters
127 end
132 end
128
133
129 def editable_by?(user)
134 def editable_by?(user)
130 return false unless user
135 return false unless user
131 return true if !is_public && self.user_id == user.id
136 # Admin can edit them all and regular users can edit their private queries
132 is_public && user.allowed_to?(:manage_public_queries, project)
137 return true if user.admin? || (!is_public && self.user_id == user.id)
138 # Members can not edit public queries that are for all project (only admin is allowed to)
139 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
133 end
140 end
134
141
135 def available_filters
142 def available_filters
136 return @available_filters if @available_filters
143 return @available_filters if @available_filters
137
144
138 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
145 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
139
146
140 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
147 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
141 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
148 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
142 "priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI'], :order => 'position').collect{|s| [s.name, s.id.to_s] } },
149 "priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI'], :order => 'position').collect{|s| [s.name, s.id.to_s] } },
143 "subject" => { :type => :text, :order => 8 },
150 "subject" => { :type => :text, :order => 8 },
144 "created_on" => { :type => :date_past, :order => 9 },
151 "created_on" => { :type => :date_past, :order => 9 },
145 "updated_on" => { :type => :date_past, :order => 10 },
152 "updated_on" => { :type => :date_past, :order => 10 },
146 "start_date" => { :type => :date, :order => 11 },
153 "start_date" => { :type => :date, :order => 11 },
147 "due_date" => { :type => :date, :order => 12 },
154 "due_date" => { :type => :date, :order => 12 },
148 "done_ratio" => { :type => :integer, :order => 13 }}
155 "done_ratio" => { :type => :integer, :order => 13 }}
149
156
150 user_values = []
157 user_values = []
151 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
158 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
152 if project
159 if project
153 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
160 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
154 else
161 else
155 # members of the user's projects
162 # members of the user's projects
156 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
163 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
157 end
164 end
158 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
165 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
159 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
166 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
160
167
161 if project
168 if project
162 # project specific filters
169 # project specific filters
163 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
170 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
164 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
171 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
165 unless @project.active_children.empty?
172 unless @project.active_children.empty?
166 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } }
173 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } }
167 end
174 end
168 @project.all_custom_fields.select(&:is_filter?).each do |field|
175 @project.all_custom_fields.select(&:is_filter?).each do |field|
169 case field.field_format
176 case field.field_format
170 when "text"
177 when "text"
171 options = { :type => :text, :order => 20 }
178 options = { :type => :text, :order => 20 }
172 when "list"
179 when "list"
173 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
180 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
174 when "date"
181 when "date"
175 options = { :type => :date, :order => 20 }
182 options = { :type => :date, :order => 20 }
176 when "bool"
183 when "bool"
177 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
184 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
178 else
185 else
179 options = { :type => :string, :order => 20 }
186 options = { :type => :string, :order => 20 }
180 end
187 end
181 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
188 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
182 end
189 end
183 # remove category filter if no category defined
190 # remove category filter if no category defined
184 @available_filters.delete "category_id" if @available_filters["category_id"][:values].empty?
191 @available_filters.delete "category_id" if @available_filters["category_id"][:values].empty?
185 end
192 end
186 @available_filters
193 @available_filters
187 end
194 end
188
195
189 def add_filter(field, operator, values)
196 def add_filter(field, operator, values)
190 # values must be an array
197 # values must be an array
191 return unless values and values.is_a? Array # and !values.first.empty?
198 return unless values and values.is_a? Array # and !values.first.empty?
192 # check if field is defined as an available filter
199 # check if field is defined as an available filter
193 if available_filters.has_key? field
200 if available_filters.has_key? field
194 filter_options = available_filters[field]
201 filter_options = available_filters[field]
195 # check if operator is allowed for that filter
202 # check if operator is allowed for that filter
196 #if @@operators_by_filter_type[filter_options[:type]].include? operator
203 #if @@operators_by_filter_type[filter_options[:type]].include? operator
197 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
204 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
198 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
205 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
199 #end
206 #end
200 filters[field] = {:operator => operator, :values => values }
207 filters[field] = {:operator => operator, :values => values }
201 end
208 end
202 end
209 end
203
210
204 def add_short_filter(field, expression)
211 def add_short_filter(field, expression)
205 return unless expression
212 return unless expression
206 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
213 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
207 add_filter field, (parms[0] || "="), [parms[1] || ""]
214 add_filter field, (parms[0] || "="), [parms[1] || ""]
208 end
215 end
209
216
210 def has_filter?(field)
217 def has_filter?(field)
211 filters and filters[field]
218 filters and filters[field]
212 end
219 end
213
220
214 def operator_for(field)
221 def operator_for(field)
215 has_filter?(field) ? filters[field][:operator] : nil
222 has_filter?(field) ? filters[field][:operator] : nil
216 end
223 end
217
224
218 def values_for(field)
225 def values_for(field)
219 has_filter?(field) ? filters[field][:values] : nil
226 has_filter?(field) ? filters[field][:values] : nil
220 end
227 end
221
228
222 def label_for(field)
229 def label_for(field)
223 label = @available_filters[field][:name] if @available_filters.has_key?(field)
230 label = @available_filters[field][:name] if @available_filters.has_key?(field)
224 label ||= field.gsub(/\_id$/, "")
231 label ||= field.gsub(/\_id$/, "")
225 end
232 end
226
233
227 def available_columns
234 def available_columns
228 return @available_columns if @available_columns
235 return @available_columns if @available_columns
229 @available_columns = Query.available_columns
236 @available_columns = Query.available_columns
230 @available_columns += (project ?
237 @available_columns += (project ?
231 project.all_custom_fields :
238 project.all_custom_fields :
232 IssueCustomField.find(:all, :conditions => {:is_for_all => true})
239 IssueCustomField.find(:all, :conditions => {:is_for_all => true})
233 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
240 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
234 end
241 end
235
242
236 def columns
243 def columns
237 if has_default_columns?
244 if has_default_columns?
238 available_columns.select {|c| Setting.issue_list_default_columns.include?(c.name.to_s) }
245 available_columns.select {|c| Setting.issue_list_default_columns.include?(c.name.to_s) }
239 else
246 else
240 # preserve the column_names order
247 # preserve the column_names order
241 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
248 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
242 end
249 end
243 end
250 end
244
251
245 def column_names=(names)
252 def column_names=(names)
246 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
253 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
247 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
254 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
248 write_attribute(:column_names, names)
255 write_attribute(:column_names, names)
249 end
256 end
250
257
251 def has_column?(column)
258 def has_column?(column)
252 column_names && column_names.include?(column.name)
259 column_names && column_names.include?(column.name)
253 end
260 end
254
261
255 def has_default_columns?
262 def has_default_columns?
256 column_names.nil? || column_names.empty?
263 column_names.nil? || column_names.empty?
257 end
264 end
258
265
259 def statement
266 def statement
260 # project/subprojects clause
267 # project/subprojects clause
261 clause = ''
268 clause = ''
262 if project && !@project.active_children.empty?
269 if project && !@project.active_children.empty?
263 ids = [project.id]
270 ids = [project.id]
264 if has_filter?("subproject_id")
271 if has_filter?("subproject_id")
265 case operator_for("subproject_id")
272 case operator_for("subproject_id")
266 when '='
273 when '='
267 # include the selected subprojects
274 # include the selected subprojects
268 ids += values_for("subproject_id").each(&:to_i)
275 ids += values_for("subproject_id").each(&:to_i)
269 when '!*'
276 when '!*'
270 # main project only
277 # main project only
271 else
278 else
272 # all subprojects
279 # all subprojects
273 ids += project.active_children.collect{|p| p.id}
280 ids += project.active_children.collect{|p| p.id}
274 end
281 end
275 elsif Setting.display_subprojects_issues?
282 elsif Setting.display_subprojects_issues?
276 ids += project.active_children.collect{|p| p.id}
283 ids += project.active_children.collect{|p| p.id}
277 end
284 end
278 clause << "#{Issue.table_name}.project_id IN (%s)" % ids.join(',')
285 clause << "#{Issue.table_name}.project_id IN (%s)" % ids.join(',')
279 elsif project
286 elsif project
280 clause << "#{Issue.table_name}.project_id = %d" % project.id
287 clause << "#{Issue.table_name}.project_id = %d" % project.id
281 else
288 else
282 clause << Project.visible_by(User.current)
289 clause << Project.visible_by(User.current)
283 end
290 end
284
291
285 # filters clauses
292 # filters clauses
286 filters_clauses = []
293 filters_clauses = []
287 filters.each_key do |field|
294 filters.each_key do |field|
288 next if field == "subproject_id"
295 next if field == "subproject_id"
289 v = values_for(field).clone
296 v = values_for(field).clone
290 next unless v and !v.empty?
297 next unless v and !v.empty?
291
298
292 sql = ''
299 sql = ''
293 if field =~ /^cf_(\d+)$/
300 if field =~ /^cf_(\d+)$/
294 # custom field
301 # custom field
295 db_table = CustomValue.table_name
302 db_table = CustomValue.table_name
296 db_field = 'value'
303 db_field = 'value'
297 sql << "#{Issue.table_name}.id IN (SELECT #{db_table}.customized_id FROM #{db_table} where #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} AND "
304 sql << "#{Issue.table_name}.id IN (SELECT #{db_table}.customized_id FROM #{db_table} where #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} AND "
298 else
305 else
299 # regular field
306 # regular field
300 db_table = Issue.table_name
307 db_table = Issue.table_name
301 db_field = field
308 db_field = field
302 sql << '('
309 sql << '('
303 end
310 end
304
311
305 # "me" value subsitution
312 # "me" value subsitution
306 if %w(assigned_to_id author_id).include?(field)
313 if %w(assigned_to_id author_id).include?(field)
307 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
314 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
308 end
315 end
309
316
310 case operator_for field
317 case operator_for field
311 when "="
318 when "="
312 sql = sql + "#{db_table}.#{db_field} IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
319 sql = sql + "#{db_table}.#{db_field} IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
313 when "!"
320 when "!"
314 sql = sql + "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
321 sql = sql + "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
315 when "!*"
322 when "!*"
316 sql = sql + "#{db_table}.#{db_field} IS NULL"
323 sql = sql + "#{db_table}.#{db_field} IS NULL"
317 when "*"
324 when "*"
318 sql = sql + "#{db_table}.#{db_field} IS NOT NULL"
325 sql = sql + "#{db_table}.#{db_field} IS NOT NULL"
319 when ">="
326 when ">="
320 sql = sql + "#{db_table}.#{db_field} >= #{v.first.to_i}"
327 sql = sql + "#{db_table}.#{db_field} >= #{v.first.to_i}"
321 when "<="
328 when "<="
322 sql = sql + "#{db_table}.#{db_field} <= #{v.first.to_i}"
329 sql = sql + "#{db_table}.#{db_field} <= #{v.first.to_i}"
323 when "o"
330 when "o"
324 sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
331 sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
325 when "c"
332 when "c"
326 sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
333 sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
327 when ">t-"
334 when ">t-"
328 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today + 1).to_time)]
335 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today + 1).to_time)]
329 when "<t-"
336 when "<t-"
330 sql = sql + "#{db_table}.#{db_field} <= '%s'" % connection.quoted_date((Date.today - v.first.to_i).to_time)
337 sql = sql + "#{db_table}.#{db_field} <= '%s'" % connection.quoted_date((Date.today - v.first.to_i).to_time)
331 when "t-"
338 when "t-"
332 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today - v.first.to_i + 1).to_time)]
339 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today - v.first.to_i + 1).to_time)]
333 when ">t+"
340 when ">t+"
334 sql = sql + "#{db_table}.#{db_field} >= '%s'" % connection.quoted_date((Date.today + v.first.to_i).to_time)
341 sql = sql + "#{db_table}.#{db_field} >= '%s'" % connection.quoted_date((Date.today + v.first.to_i).to_time)
335 when "<t+"
342 when "<t+"
336 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)]
343 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)]
337 when "t+"
344 when "t+"
338 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today + v.first.to_i).to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)]
345 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today + v.first.to_i).to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)]
339 when "t"
346 when "t"
340 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today+1).to_time)]
347 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today+1).to_time)]
341 when "w"
348 when "w"
342 from = l(:general_first_day_of_week) == '7' ?
349 from = l(:general_first_day_of_week) == '7' ?
343 # week starts on sunday
350 # week starts on sunday
344 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
351 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
345 # week starts on monday (Rails default)
352 # week starts on monday (Rails default)
346 Time.now.at_beginning_of_week
353 Time.now.at_beginning_of_week
347 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
354 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
348 when "~"
355 when "~"
349 sql = sql + "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(v.first)}%'"
356 sql = sql + "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(v.first)}%'"
350 when "!~"
357 when "!~"
351 sql = sql + "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(v.first)}%'"
358 sql = sql + "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(v.first)}%'"
352 end
359 end
353 sql << ')'
360 sql << ')'
354 filters_clauses << sql
361 filters_clauses << sql
355 end if filters and valid?
362 end if filters and valid?
356
363
357 clause << ' AND ' unless clause.empty?
364 clause << ' AND ' unless clause.empty?
358 clause << filters_clauses.join(' AND ') unless filters_clauses.empty?
365 clause << filters_clauses.join(' AND ') unless filters_clauses.empty?
359 clause
366 clause
360 end
367 end
361 end
368 end
@@ -1,13 +1,14
1 <% if @project %>
1 <h3><%= l(:label_issue_plural) %></h3>
2 <h3><%= l(:label_issue_plural) %></h3>
2 <%= link_to l(:label_issue_view_all), { :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 } %><br />
3 <%= link_to l(:label_issue_view_all), { :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 } %><br />
3 <%= link_to l(:field_summary), :controller => 'reports', :action => 'issue_report', :id => @project %><br />
4 <%= link_to l(:field_summary), :controller => 'reports', :action => 'issue_report', :id => @project %><br />
4 <%= link_to l(:label_change_log), :controller => 'projects', :action => 'changelog', :id => @project %>
5 <%= link_to l(:label_change_log), :controller => 'projects', :action => 'changelog', :id => @project %>
6 <% end %>
5
7
8 <% unless sidebar_queries.empty? -%>
6 <h3><%= l(:label_query_plural) %></h3>
9 <h3><%= l(:label_query_plural) %></h3>
7
10
8 <% queries = @project.queries.find(:all,
11 <% sidebar_queries.each do |query| -%>
9 :order => "name ASC",
10 :conditions => ["is_public = ? or user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
11 queries.each do |query| %>
12 <%= link_to query.name, :controller => 'issues', :action => 'index', :project_id => @project, :query_id => query %><br />
12 <%= link_to query.name, :controller => 'issues', :action => 'index', :project_id => @project, :query_id => query %><br />
13 <% end %>
13 <% end -%>
14 <% end -%>
@@ -1,67 +1,67
1 <% if @query.new_record? %>
1 <% if @query.new_record? %>
2 <h2><%=l(:label_issue_plural)%></h2>
2 <h2><%=l(:label_issue_plural)%></h2>
3 <% html_title(l(:label_issue_plural)) %>
3 <% html_title(l(:label_issue_plural)) %>
4
4
5 <% form_tag({ :controller => 'queries', :action => 'new' }, :id => 'query_form') do %>
5 <% form_tag({ :controller => 'queries', :action => 'new' }, :id => 'query_form') do %>
6 <%= hidden_field_tag('project_id', @project.id) if @project %>
6 <%= hidden_field_tag('project_id', @project.id) if @project %>
7 <fieldset id="filters"><legend><%= l(:label_filter_plural) %></legend>
7 <fieldset id="filters"><legend><%= l(:label_filter_plural) %></legend>
8 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
8 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
9 <p class="buttons">
9 <p class="buttons">
10 <%= link_to_remote l(:button_apply),
10 <%= link_to_remote l(:button_apply),
11 { :url => { :set_filter => 1 },
11 { :url => { :set_filter => 1 },
12 :update => "content",
12 :update => "content",
13 :with => "Form.serialize('query_form')"
13 :with => "Form.serialize('query_form')"
14 }, :class => 'icon icon-checked' %>
14 }, :class => 'icon icon-checked' %>
15
15
16 <%= link_to_remote l(:button_clear),
16 <%= link_to_remote l(:button_clear),
17 { :url => { :set_filter => 1 },
17 { :url => { :set_filter => 1 },
18 :update => "content",
18 :update => "content",
19 }, :class => 'icon icon-reload' %>
19 }, :class => 'icon icon-reload' %>
20
20
21 <% if current_role && current_role.allowed_to?(:save_queries) %>
21 <% if current_role && current_role.allowed_to?(:save_queries) %>
22 <%= link_to l(:button_save), {}, :onclick => "$('query_form').submit(); return false;", :class => 'icon icon-save' %>
22 <%= link_to l(:button_save), {}, :onclick => "$('query_form').submit(); return false;", :class => 'icon icon-save' %>
23 <% end %>
23 <% end %>
24 </p>
24 </p>
25 </fieldset>
25 </fieldset>
26 <% end %>
26 <% end %>
27 <% else %>
27 <% else %>
28 <div class="contextual">
28 <div class="contextual">
29 <% if @query.editable_by?(User.current) %>
29 <% if @query.editable_by?(User.current) %>
30 <%= link_to l(:button_edit), {:controller => 'queries', :action => 'edit', :id => @query}, :class => 'icon icon-edit' %>
30 <%= link_to l(:button_edit), {:controller => 'queries', :action => 'edit', :id => @query}, :class => 'icon icon-edit' %>
31 <%= link_to l(:button_delete), {:controller => 'queries', :action => 'destroy', :id => @query}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
31 <%= link_to l(:button_delete), {:controller => 'queries', :action => 'destroy', :id => @query}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
32 <% end %>
32 <% end %>
33 </div>
33 </div>
34 <h2><%=h @query.name %></h2>
34 <h2><%=h @query.name %></h2>
35 <div id="query_form"></div>
35 <div id="query_form"></div>
36 <% html_title @query.name %>
36 <% html_title @query.name %>
37 <% end %>
37 <% end %>
38 <%= error_messages_for 'query' %>
38 <%= error_messages_for 'query' %>
39 <% if @query.valid? %>
39 <% if @query.valid? %>
40 <% if @issues.empty? %>
40 <% if @issues.empty? %>
41 <p class="nodata"><%= l(:label_no_data) %></p>
41 <p class="nodata"><%= l(:label_no_data) %></p>
42 <% else %>
42 <% else %>
43 <%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %>
43 <%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %>
44 <p class="pagination"><%= pagination_links_full @issue_pages, @issue_count %></p>
44 <p class="pagination"><%= pagination_links_full @issue_pages, @issue_count %></p>
45
45
46 <p class="other-formats">
46 <p class="other-formats">
47 <%= l(:label_export_to) %>
47 <%= l(:label_export_to) %>
48 <span><%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span>
48 <span><%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span>
49 <span><%= link_to 'CSV', {:format => 'csv'}, :class => 'csv' %></span>
49 <span><%= link_to 'CSV', {:format => 'csv'}, :class => 'csv' %></span>
50 <span><%= link_to 'PDF', {:format => 'pdf'}, :class => 'pdf' %></span>
50 <span><%= link_to 'PDF', {:format => 'pdf'}, :class => 'pdf' %></span>
51 </p>
51 </p>
52 <% end %>
52 <% end %>
53 <% end %>
53 <% end %>
54
54
55 <% content_for :sidebar do %>
55 <% content_for :sidebar do %>
56 <%= render :partial => 'issues/sidebar' %>
56 <%= render :partial => 'issues/sidebar' %>
57 <% end if @project%>
57 <% end %>
58
58
59 <% content_for :header_tags do %>
59 <% content_for :header_tags do %>
60 <%= auto_discovery_link_tag(:atom, {:query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_issue_plural)) %>
60 <%= auto_discovery_link_tag(:atom, {:query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_issue_plural)) %>
61 <%= auto_discovery_link_tag(:atom, {:action => 'changes', :query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_changes_details)) %>
61 <%= auto_discovery_link_tag(:atom, {:action => 'changes', :query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_changes_details)) %>
62 <%= javascript_include_tag 'context_menu' %>
62 <%= javascript_include_tag 'context_menu' %>
63 <%= stylesheet_link_tag 'context_menu' %>
63 <%= stylesheet_link_tag 'context_menu' %>
64 <% end %>
64 <% end %>
65
65
66 <div id="context-menu" style="display: none;"></div>
66 <div id="context-menu" style="display: none;"></div>
67 <%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %>
67 <%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %>
@@ -1,24 +1,29
1 <%= error_messages_for 'query' %>
1 <%= error_messages_for 'query' %>
2 <%= hidden_field_tag 'confirm', 1 %>
2 <%= hidden_field_tag 'confirm', 1 %>
3
3
4 <div class="box">
4 <div class="box">
5 <div class="tabular">
5 <div class="tabular">
6 <p><label for="query_name"><%=l(:field_name)%></label>
6 <p><label for="query_name"><%=l(:field_name)%></label>
7 <%= text_field 'query', 'name', :size => 80 %></p>
7 <%= text_field 'query', 'name', :size => 80 %></p>
8
8
9 <% if current_role.allowed_to?(:manage_public_queries) %>
9 <% if User.current.admin? || (@project && current_role.allowed_to?(:manage_public_queries)) %>
10 <p><label for="query_is_public"><%=l(:field_is_public)%></label>
10 <p><label for="query_is_public"><%=l(:field_is_public)%></label>
11 <%= check_box 'query', 'is_public' %></p>
11 <%= check_box 'query', 'is_public',
12 :onchange => (User.current.admin? ? nil : 'if (this.checked) {$("query_is_for_all").checked = false; $("query_is_for_all").disabled = true;} else {$("query_is_for_all").disabled = false;}') %></p>
12 <% end %>
13 <% end %>
13
14
15 <p><label for="query_is_for_all"><%=l(:field_is_for_all)%></label>
16 <%= check_box_tag 'query_is_for_all', 1, @query.project.nil?,
17 :disabled => (!@query.new_record? && (@query.project.nil? || (@query.is_public? && !User.current.admin?))) %></p>
18
14 <p><label for="query_default_columns"><%=l(:label_default_columns)%></label>
19 <p><label for="query_default_columns"><%=l(:label_default_columns)%></label>
15 <%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns',
20 <%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns',
16 :onclick => 'if (this.checked) {Element.hide("columns")} else {Element.show("columns")}' %></p>
21 :onclick => 'if (this.checked) {Element.hide("columns")} else {Element.show("columns")}' %></p>
17 </div>
22 </div>
18
23
19 <fieldset><legend><%= l(:label_filter_plural) %></legend>
24 <fieldset><legend><%= l(:label_filter_plural) %></legend>
20 <%= render :partial => 'queries/filters', :locals => {:query => query}%>
25 <%= render :partial => 'queries/filters', :locals => {:query => query}%>
21 </fieldset>
26 </fieldset>
22
27
23 <%= render :partial => 'queries/columns', :locals => {:query => query}%>
28 <%= render :partial => 'queries/columns', :locals => {:query => query}%>
24 </div>
29 </div>
@@ -1,22 +1,69
1 ---
1 ---
2 queries_001:
2 queries_001:
3 name: Multiple custom fields query
3 id: 1
4 project_id: 1
4 project_id: 1
5 is_public: true
6 name: Multiple custom fields query
5 filters: |
7 filters: |
6 ---
8 ---
7 cf_1:
9 cf_1:
8 :values:
10 :values:
9 - MySQL
11 - MySQL
10 :operator: "="
12 :operator: "="
11 status_id:
13 status_id:
12 :values:
14 :values:
13 - "1"
15 - "1"
14 :operator: o
16 :operator: o
15 cf_2:
17 cf_2:
16 :values:
18 :values:
17 - "125"
19 - "125"
18 :operator: "="
20 :operator: "="
19
21
20 id: 1
21 is_public: true
22 user_id: 1
22 user_id: 1
23 column_names:
24 queries_002:
25 id: 2
26 project_id: 1
27 is_public: false
28 name: Private query for cookbook
29 filters: |
30 ---
31 tracker_id:
32 :values:
33 - "3"
34 :operator: "="
35 status_id:
36 :values:
37 - "1"
38 :operator: o
39
40 user_id: 3
41 column_names:
42 queries_003:
43 id: 3
44 project_id:
45 is_public: false
46 name: Private query for all projects
47 filters: |
48 ---
49 tracker_id:
50 :values:
51 - "3"
52 :operator: "="
53
54 user_id: 3
55 column_names:
56 queries_004:
57 id: 4
58 project_id:
59 is_public: true
60 name: Public query for all projects
61 filters: |
62 ---
63 tracker_id:
64 :values:
65 - "3"
66 :operator: "="
67
68 user_id: 2
69 column_names:
@@ -1,163 +1,161
1 ---
1 ---
2 roles_001:
2 roles_001:
3 name: Manager
3 name: Manager
4 id: 1
4 id: 1
5 builtin: 0
5 builtin: 0
6 permissions: |
6 permissions: |
7 ---
7 ---
8 - :edit_project
8 - :edit_project
9 - :manage_members
9 - :manage_members
10 - :manage_versions
10 - :manage_versions
11 - :manage_categories
11 - :manage_categories
12 - :add_issues
12 - :add_issues
13 - :edit_issues
13 - :edit_issues
14 - :manage_issue_relations
14 - :manage_issue_relations
15 - :add_issue_notes
15 - :add_issue_notes
16 - :move_issues
16 - :move_issues
17 - :delete_issues
17 - :delete_issues
18 - :manage_public_queries
18 - :manage_public_queries
19 - :save_queries
19 - :save_queries
20 - :view_gantt
20 - :view_gantt
21 - :view_calendar
21 - :view_calendar
22 - :log_time
22 - :log_time
23 - :view_time_entries
23 - :view_time_entries
24 - :edit_time_entries
24 - :edit_time_entries
25 - :delete_time_entries
25 - :delete_time_entries
26 - :manage_news
26 - :manage_news
27 - :comment_news
27 - :comment_news
28 - :view_documents
28 - :view_documents
29 - :manage_documents
29 - :manage_documents
30 - :view_wiki_pages
30 - :view_wiki_pages
31 - :edit_wiki_pages
31 - :edit_wiki_pages
32 - :delete_wiki_pages
32 - :delete_wiki_pages
33 - :rename_wiki_pages
33 - :rename_wiki_pages
34 - :add_messages
34 - :add_messages
35 - :edit_messages
35 - :edit_messages
36 - :delete_messages
36 - :delete_messages
37 - :manage_boards
37 - :manage_boards
38 - :view_files
38 - :view_files
39 - :manage_files
39 - :manage_files
40 - :browse_repository
40 - :browse_repository
41 - :view_changesets
41 - :view_changesets
42
42
43 position: 1
43 position: 1
44 roles_002:
44 roles_002:
45 name: Developer
45 name: Developer
46 id: 2
46 id: 2
47 builtin: 0
47 builtin: 0
48 permissions: |
48 permissions: |
49 ---
49 ---
50 - :edit_project
50 - :edit_project
51 - :manage_members
51 - :manage_members
52 - :manage_versions
52 - :manage_versions
53 - :manage_categories
53 - :manage_categories
54 - :add_issues
54 - :add_issues
55 - :edit_issues
55 - :edit_issues
56 - :manage_issue_relations
56 - :manage_issue_relations
57 - :add_issue_notes
57 - :add_issue_notes
58 - :move_issues
58 - :move_issues
59 - :delete_issues
59 - :delete_issues
60 - :manage_public_queries
61 - :save_queries
60 - :save_queries
62 - :view_gantt
61 - :view_gantt
63 - :view_calendar
62 - :view_calendar
64 - :log_time
63 - :log_time
65 - :view_time_entries
64 - :view_time_entries
66 - :edit_own_time_entries
65 - :edit_own_time_entries
67 - :manage_news
66 - :manage_news
68 - :comment_news
67 - :comment_news
69 - :view_documents
68 - :view_documents
70 - :manage_documents
69 - :manage_documents
71 - :view_wiki_pages
70 - :view_wiki_pages
72 - :edit_wiki_pages
71 - :edit_wiki_pages
73 - :delete_wiki_pages
72 - :delete_wiki_pages
74 - :add_messages
73 - :add_messages
75 - :manage_boards
74 - :manage_boards
76 - :view_files
75 - :view_files
77 - :manage_files
76 - :manage_files
78 - :browse_repository
77 - :browse_repository
79 - :view_changesets
78 - :view_changesets
80
79
81 position: 2
80 position: 2
82 roles_003:
81 roles_003:
83 name: Reporter
82 name: Reporter
84 id: 3
83 id: 3
85 builtin: 0
84 builtin: 0
86 permissions: |
85 permissions: |
87 ---
86 ---
88 - :edit_project
87 - :edit_project
89 - :manage_members
88 - :manage_members
90 - :manage_versions
89 - :manage_versions
91 - :manage_categories
90 - :manage_categories
92 - :add_issues
91 - :add_issues
93 - :edit_issues
92 - :edit_issues
94 - :manage_issue_relations
93 - :manage_issue_relations
95 - :add_issue_notes
94 - :add_issue_notes
96 - :move_issues
95 - :move_issues
97 - :manage_public_queries
98 - :save_queries
96 - :save_queries
99 - :view_gantt
97 - :view_gantt
100 - :view_calendar
98 - :view_calendar
101 - :log_time
99 - :log_time
102 - :view_time_entries
100 - :view_time_entries
103 - :manage_news
101 - :manage_news
104 - :comment_news
102 - :comment_news
105 - :view_documents
103 - :view_documents
106 - :manage_documents
104 - :manage_documents
107 - :view_wiki_pages
105 - :view_wiki_pages
108 - :edit_wiki_pages
106 - :edit_wiki_pages
109 - :delete_wiki_pages
107 - :delete_wiki_pages
110 - :add_messages
108 - :add_messages
111 - :manage_boards
109 - :manage_boards
112 - :view_files
110 - :view_files
113 - :manage_files
111 - :manage_files
114 - :browse_repository
112 - :browse_repository
115 - :view_changesets
113 - :view_changesets
116
114
117 position: 3
115 position: 3
118 roles_004:
116 roles_004:
119 name: Non member
117 name: Non member
120 id: 4
118 id: 4
121 builtin: 1
119 builtin: 1
122 permissions: |
120 permissions: |
123 ---
121 ---
124 - :add_issues
122 - :add_issues
125 - :edit_issues
123 - :edit_issues
126 - :manage_issue_relations
124 - :manage_issue_relations
127 - :add_issue_notes
125 - :add_issue_notes
128 - :move_issues
126 - :move_issues
129 - :save_queries
127 - :save_queries
130 - :view_gantt
128 - :view_gantt
131 - :view_calendar
129 - :view_calendar
132 - :log_time
130 - :log_time
133 - :view_time_entries
131 - :view_time_entries
134 - :comment_news
132 - :comment_news
135 - :view_documents
133 - :view_documents
136 - :manage_documents
134 - :manage_documents
137 - :view_wiki_pages
135 - :view_wiki_pages
138 - :edit_wiki_pages
136 - :edit_wiki_pages
139 - :add_messages
137 - :add_messages
140 - :view_files
138 - :view_files
141 - :manage_files
139 - :manage_files
142 - :browse_repository
140 - :browse_repository
143 - :view_changesets
141 - :view_changesets
144
142
145 position: 4
143 position: 4
146 roles_005:
144 roles_005:
147 name: Anonymous
145 name: Anonymous
148 id: 5
146 id: 5
149 builtin: 2
147 builtin: 2
150 permissions: |
148 permissions: |
151 ---
149 ---
152 - :add_issue_notes
150 - :add_issue_notes
153 - :view_gantt
151 - :view_gantt
154 - :view_calendar
152 - :view_calendar
155 - :view_time_entries
153 - :view_time_entries
156 - :view_documents
154 - :view_documents
157 - :view_wiki_pages
155 - :view_wiki_pages
158 - :view_files
156 - :view_files
159 - :browse_repository
157 - :browse_repository
160 - :view_changesets
158 - :view_changesets
161
159
162 position: 5
160 position: 5
163 No newline at end of file
161
@@ -1,44 +1,74
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class QueryTest < Test::Unit::TestCase
20 class QueryTest < Test::Unit::TestCase
21 fixtures :projects, :users, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries
21 fixtures :projects, :users, :members, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries
22
22
23 def test_query_with_multiple_custom_fields
23 def test_query_with_multiple_custom_fields
24 query = Query.find(1)
24 query = Query.find(1)
25 assert query.valid?
25 assert query.valid?
26 assert query.statement.include?("custom_values.value IN ('MySQL')")
26 assert query.statement.include?("custom_values.value IN ('MySQL')")
27 issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement
27 issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement
28 assert_equal 1, issues.length
28 assert_equal 1, issues.length
29 assert_equal Issue.find(3), issues.first
29 assert_equal Issue.find(3), issues.first
30 end
30 end
31
31
32 def test_default_columns
32 def test_default_columns
33 q = Query.new
33 q = Query.new
34 assert !q.columns.empty?
34 assert !q.columns.empty?
35 end
35 end
36
36
37 def test_set_column_names
37 def test_set_column_names
38 q = Query.new
38 q = Query.new
39 q.column_names = ['tracker', :subject, '', 'unknonw_column']
39 q.column_names = ['tracker', :subject, '', 'unknonw_column']
40 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
40 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
41 c = q.columns.first
41 c = q.columns.first
42 assert q.has_column?(c)
42 assert q.has_column?(c)
43 end
43 end
44
45 def test_editable_by
46 admin = User.find(1)
47 manager = User.find(2)
48 developer = User.find(3)
49
50 # Public query on project 1
51 q = Query.find(1)
52 assert q.editable_by?(admin)
53 assert q.editable_by?(manager)
54 assert !q.editable_by?(developer)
55
56 # Private query on project 1
57 q = Query.find(2)
58 assert q.editable_by?(admin)
59 assert !q.editable_by?(manager)
60 assert q.editable_by?(developer)
61
62 # Private query for all projects
63 q = Query.find(3)
64 assert q.editable_by?(admin)
65 assert !q.editable_by?(manager)
66 assert q.editable_by?(developer)
67
68 # Public query for all projects
69 q = Query.find(4)
70 assert q.editable_by?(admin)
71 assert !q.editable_by?(manager)
72 assert !q.editable_by?(developer)
73 end
44 end
74 end
General Comments 0
You need to be logged in to leave comments. Login now