##// END OF EJS Templates
Remove special behaviour for listing issue time entries, use a filter for that....
Jean-Philippe Lang -
r15262:03dbf8abb881
parent child
Show More
@@ -1,277 +1,280
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 TimelogController < ApplicationController
18 class TimelogController < ApplicationController
19 menu_item :issues
19 menu_item :issues
20
20
21 before_filter :find_time_entry, :only => [:show, :edit, :update]
21 before_filter :find_time_entry, :only => [:show, :edit, :update]
22 before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
22 before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
23 before_filter :authorize, :only => [:show, :edit, :update, :bulk_edit, :bulk_update, :destroy]
23 before_filter :authorize, :only => [:show, :edit, :update, :bulk_edit, :bulk_update, :destroy]
24
24
25 before_filter :find_optional_project, :only => [:new, :create, :index, :report]
25 before_filter :find_optional_issue, :only => [:new, :create]
26 before_filter :find_optional_project, :only => [:index, :report]
26 before_filter :authorize_global, :only => [:new, :create, :index, :report]
27 before_filter :authorize_global, :only => [:new, :create, :index, :report]
27
28
28 accept_rss_auth :index
29 accept_rss_auth :index
29 accept_api_auth :index, :show, :create, :update, :destroy
30 accept_api_auth :index, :show, :create, :update, :destroy
30
31
31 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
32 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
32
33
33 helper :sort
34 helper :sort
34 include SortHelper
35 include SortHelper
35 helper :issues
36 helper :issues
36 include TimelogHelper
37 include TimelogHelper
37 helper :custom_fields
38 helper :custom_fields
38 include CustomFieldsHelper
39 include CustomFieldsHelper
39 helper :queries
40 helper :queries
40 include QueriesHelper
41 include QueriesHelper
41
42
42 def index
43 def index
43 retrieve_time_entry_query
44 retrieve_time_entry_query
44 sort_init(@query.sort_criteria.empty? ? [['spent_on', 'desc']] : @query.sort_criteria)
45 sort_init(@query.sort_criteria.empty? ? [['spent_on', 'desc']] : @query.sort_criteria)
45 sort_update(@query.sortable_columns)
46 sort_update(@query.sortable_columns)
46 scope = time_entry_scope(:order => sort_clause).
47 scope = time_entry_scope(:order => sort_clause).
47 includes(:project, :user, :issue).
48 includes(:project, :user, :issue).
48 preload(:issue => [:project, :tracker, :status, :assigned_to, :priority])
49 preload(:issue => [:project, :tracker, :status, :assigned_to, :priority])
49
50
50 respond_to do |format|
51 respond_to do |format|
51 format.html {
52 format.html {
52 @entry_count = scope.count
53 @entry_count = scope.count
53 @entry_pages = Paginator.new @entry_count, per_page_option, params['page']
54 @entry_pages = Paginator.new @entry_count, per_page_option, params['page']
54 @entries = scope.offset(@entry_pages.offset).limit(@entry_pages.per_page).to_a
55 @entries = scope.offset(@entry_pages.offset).limit(@entry_pages.per_page).to_a
55 @total_hours = scope.sum(:hours).to_f
56 @total_hours = scope.sum(:hours).to_f
56
57
57 render :layout => !request.xhr?
58 render :layout => !request.xhr?
58 }
59 }
59 format.api {
60 format.api {
60 @entry_count = scope.count
61 @entry_count = scope.count
61 @offset, @limit = api_offset_and_limit
62 @offset, @limit = api_offset_and_limit
62 @entries = scope.offset(@offset).limit(@limit).preload(:custom_values => :custom_field).to_a
63 @entries = scope.offset(@offset).limit(@limit).preload(:custom_values => :custom_field).to_a
63 }
64 }
64 format.atom {
65 format.atom {
65 entries = scope.limit(Setting.feeds_limit.to_i).reorder("#{TimeEntry.table_name}.created_on DESC").to_a
66 entries = scope.limit(Setting.feeds_limit.to_i).reorder("#{TimeEntry.table_name}.created_on DESC").to_a
66 render_feed(entries, :title => l(:label_spent_time))
67 render_feed(entries, :title => l(:label_spent_time))
67 }
68 }
68 format.csv {
69 format.csv {
69 # Export all entries
70 # Export all entries
70 @entries = scope.to_a
71 @entries = scope.to_a
71 send_data(query_to_csv(@entries, @query, params), :type => 'text/csv; header=present', :filename => 'timelog.csv')
72 send_data(query_to_csv(@entries, @query, params), :type => 'text/csv; header=present', :filename => 'timelog.csv')
72 }
73 }
73 end
74 end
74 end
75 end
75
76
76 def report
77 def report
77 retrieve_time_entry_query
78 retrieve_time_entry_query
78 scope = time_entry_scope
79 scope = time_entry_scope
79
80
80 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], scope)
81 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], scope)
81
82
82 respond_to do |format|
83 respond_to do |format|
83 format.html { render :layout => !request.xhr? }
84 format.html { render :layout => !request.xhr? }
84 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
85 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
85 end
86 end
86 end
87 end
87
88
88 def show
89 def show
89 respond_to do |format|
90 respond_to do |format|
90 # TODO: Implement html response
91 # TODO: Implement html response
91 format.html { render :nothing => true, :status => 406 }
92 format.html { render :nothing => true, :status => 406 }
92 format.api
93 format.api
93 end
94 end
94 end
95 end
95
96
96 def new
97 def new
97 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
98 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
98 @time_entry.safe_attributes = params[:time_entry]
99 @time_entry.safe_attributes = params[:time_entry]
99 end
100 end
100
101
101 def create
102 def create
102 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
103 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
103 @time_entry.safe_attributes = params[:time_entry]
104 @time_entry.safe_attributes = params[:time_entry]
104 if @time_entry.project && !User.current.allowed_to?(:log_time, @time_entry.project)
105 if @time_entry.project && !User.current.allowed_to?(:log_time, @time_entry.project)
105 render_403
106 render_403
106 return
107 return
107 end
108 end
108
109
109 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
110 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
110
111
111 if @time_entry.save
112 if @time_entry.save
112 respond_to do |format|
113 respond_to do |format|
113 format.html {
114 format.html {
114 flash[:notice] = l(:notice_successful_create)
115 flash[:notice] = l(:notice_successful_create)
115 if params[:continue]
116 if params[:continue]
116 options = {
117 options = {
117 :time_entry => {
118 :time_entry => {
118 :project_id => params[:time_entry][:project_id],
119 :project_id => params[:time_entry][:project_id],
119 :issue_id => @time_entry.issue_id,
120 :issue_id => @time_entry.issue_id,
120 :activity_id => @time_entry.activity_id
121 :activity_id => @time_entry.activity_id
121 },
122 },
122 :back_url => params[:back_url]
123 :back_url => params[:back_url]
123 }
124 }
124 if params[:project_id] && @time_entry.project
125 if params[:project_id] && @time_entry.project
125 redirect_to new_project_time_entry_path(@time_entry.project, options)
126 redirect_to new_project_time_entry_path(@time_entry.project, options)
126 elsif params[:issue_id] && @time_entry.issue
127 elsif params[:issue_id] && @time_entry.issue
127 redirect_to new_issue_time_entry_path(@time_entry.issue, options)
128 redirect_to new_issue_time_entry_path(@time_entry.issue, options)
128 else
129 else
129 redirect_to new_time_entry_path(options)
130 redirect_to new_time_entry_path(options)
130 end
131 end
131 else
132 else
132 redirect_back_or_default project_time_entries_path(@time_entry.project)
133 redirect_back_or_default project_time_entries_path(@time_entry.project)
133 end
134 end
134 }
135 }
135 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
136 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
136 end
137 end
137 else
138 else
138 respond_to do |format|
139 respond_to do |format|
139 format.html { render :action => 'new' }
140 format.html { render :action => 'new' }
140 format.api { render_validation_errors(@time_entry) }
141 format.api { render_validation_errors(@time_entry) }
141 end
142 end
142 end
143 end
143 end
144 end
144
145
145 def edit
146 def edit
146 @time_entry.safe_attributes = params[:time_entry]
147 @time_entry.safe_attributes = params[:time_entry]
147 end
148 end
148
149
149 def update
150 def update
150 @time_entry.safe_attributes = params[:time_entry]
151 @time_entry.safe_attributes = params[:time_entry]
151
152
152 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
153 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
153
154
154 if @time_entry.save
155 if @time_entry.save
155 respond_to do |format|
156 respond_to do |format|
156 format.html {
157 format.html {
157 flash[:notice] = l(:notice_successful_update)
158 flash[:notice] = l(:notice_successful_update)
158 redirect_back_or_default project_time_entries_path(@time_entry.project)
159 redirect_back_or_default project_time_entries_path(@time_entry.project)
159 }
160 }
160 format.api { render_api_ok }
161 format.api { render_api_ok }
161 end
162 end
162 else
163 else
163 respond_to do |format|
164 respond_to do |format|
164 format.html { render :action => 'edit' }
165 format.html { render :action => 'edit' }
165 format.api { render_validation_errors(@time_entry) }
166 format.api { render_validation_errors(@time_entry) }
166 end
167 end
167 end
168 end
168 end
169 end
169
170
170 def bulk_edit
171 def bulk_edit
171 @available_activities = TimeEntryActivity.shared.active
172 @available_activities = TimeEntryActivity.shared.active
172 @custom_fields = TimeEntry.first.available_custom_fields
173 @custom_fields = TimeEntry.first.available_custom_fields
173 end
174 end
174
175
175 def bulk_update
176 def bulk_update
176 attributes = parse_params_for_bulk_update(params[:time_entry])
177 attributes = parse_params_for_bulk_update(params[:time_entry])
177
178
178 unsaved_time_entry_ids = []
179 unsaved_time_entry_ids = []
179 @time_entries.each do |time_entry|
180 @time_entries.each do |time_entry|
180 time_entry.reload
181 time_entry.reload
181 time_entry.safe_attributes = attributes
182 time_entry.safe_attributes = attributes
182 call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
183 call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
183 unless time_entry.save
184 unless time_entry.save
184 logger.info "time entry could not be updated: #{time_entry.errors.full_messages}" if logger && logger.info?
185 logger.info "time entry could not be updated: #{time_entry.errors.full_messages}" if logger && logger.info?
185 # Keep unsaved time_entry ids to display them in flash error
186 # Keep unsaved time_entry ids to display them in flash error
186 unsaved_time_entry_ids << time_entry.id
187 unsaved_time_entry_ids << time_entry.id
187 end
188 end
188 end
189 end
189 set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
190 set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
190 redirect_back_or_default project_time_entries_path(@projects.first)
191 redirect_back_or_default project_time_entries_path(@projects.first)
191 end
192 end
192
193
193 def destroy
194 def destroy
194 destroyed = TimeEntry.transaction do
195 destroyed = TimeEntry.transaction do
195 @time_entries.each do |t|
196 @time_entries.each do |t|
196 unless t.destroy && t.destroyed?
197 unless t.destroy && t.destroyed?
197 raise ActiveRecord::Rollback
198 raise ActiveRecord::Rollback
198 end
199 end
199 end
200 end
200 end
201 end
201
202
202 respond_to do |format|
203 respond_to do |format|
203 format.html {
204 format.html {
204 if destroyed
205 if destroyed
205 flash[:notice] = l(:notice_successful_delete)
206 flash[:notice] = l(:notice_successful_delete)
206 else
207 else
207 flash[:error] = l(:notice_unable_delete_time_entry)
208 flash[:error] = l(:notice_unable_delete_time_entry)
208 end
209 end
209 redirect_back_or_default project_time_entries_path(@projects.first)
210 redirect_back_or_default project_time_entries_path(@projects.first)
210 }
211 }
211 format.api {
212 format.api {
212 if destroyed
213 if destroyed
213 render_api_ok
214 render_api_ok
214 else
215 else
215 render_validation_errors(@time_entries)
216 render_validation_errors(@time_entries)
216 end
217 end
217 }
218 }
218 end
219 end
219 end
220 end
220
221
221 private
222 private
222 def find_time_entry
223 def find_time_entry
223 @time_entry = TimeEntry.find(params[:id])
224 @time_entry = TimeEntry.find(params[:id])
224 unless @time_entry.editable_by?(User.current)
225 unless @time_entry.editable_by?(User.current)
225 render_403
226 render_403
226 return false
227 return false
227 end
228 end
228 @project = @time_entry.project
229 @project = @time_entry.project
229 rescue ActiveRecord::RecordNotFound
230 rescue ActiveRecord::RecordNotFound
230 render_404
231 render_404
231 end
232 end
232
233
233 def find_time_entries
234 def find_time_entries
234 @time_entries = TimeEntry.where(:id => params[:id] || params[:ids]).to_a
235 @time_entries = TimeEntry.where(:id => params[:id] || params[:ids]).to_a
235 raise ActiveRecord::RecordNotFound if @time_entries.empty?
236 raise ActiveRecord::RecordNotFound if @time_entries.empty?
236 raise Unauthorized unless @time_entries.all? {|t| t.editable_by?(User.current)}
237 raise Unauthorized unless @time_entries.all? {|t| t.editable_by?(User.current)}
237 @projects = @time_entries.collect(&:project).compact.uniq
238 @projects = @time_entries.collect(&:project).compact.uniq
238 @project = @projects.first if @projects.size == 1
239 @project = @projects.first if @projects.size == 1
239 rescue ActiveRecord::RecordNotFound
240 rescue ActiveRecord::RecordNotFound
240 render_404
241 render_404
241 end
242 end
242
243
243 def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
244 def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
244 if unsaved_time_entry_ids.empty?
245 if unsaved_time_entry_ids.empty?
245 flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
246 flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
246 else
247 else
247 flash[:error] = l(:notice_failed_to_save_time_entries,
248 flash[:error] = l(:notice_failed_to_save_time_entries,
248 :count => unsaved_time_entry_ids.size,
249 :count => unsaved_time_entry_ids.size,
249 :total => time_entries.size,
250 :total => time_entries.size,
250 :ids => '#' + unsaved_time_entry_ids.join(', #'))
251 :ids => '#' + unsaved_time_entry_ids.join(', #'))
251 end
252 end
252 end
253 end
253
254
254 def find_optional_project
255 def find_optional_issue
255 if params[:issue_id].present?
256 if params[:issue_id].present?
256 @issue = Issue.find(params[:issue_id])
257 @issue = Issue.find(params[:issue_id])
257 @project = @issue.project
258 @project = @issue.project
258 elsif params[:project_id].present?
259 else
260 find_optional_project
261 end
262 end
263
264 def find_optional_project
265 if params[:project_id].present?
259 @project = Project.find(params[:project_id])
266 @project = Project.find(params[:project_id])
260 end
267 end
261 rescue ActiveRecord::RecordNotFound
268 rescue ActiveRecord::RecordNotFound
262 render_404
269 render_404
263 end
270 end
264
271
265 # Returns the TimeEntry scope for index and report actions
272 # Returns the TimeEntry scope for index and report actions
266 def time_entry_scope(options={})
273 def time_entry_scope(options={})
267 scope = @query.results_scope(options)
274 @query.results_scope(options)
268 if @issue
269 scope = scope.on_issue(@issue)
270 end
271 scope
272 end
275 end
273
276
274 def retrieve_time_entry_query
277 def retrieve_time_entry_query
275 retrieve_query(TimeEntryQuery, false)
278 retrieve_query(TimeEntryQuery, false)
276 end
279 end
277 end
280 end
@@ -1,492 +1,494
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module IssuesHelper
20 module IssuesHelper
21 include ApplicationHelper
21 include ApplicationHelper
22 include Redmine::Export::PDF::IssuesPdfHelper
22 include Redmine::Export::PDF::IssuesPdfHelper
23
23
24 def issue_list(issues, &block)
24 def issue_list(issues, &block)
25 ancestors = []
25 ancestors = []
26 issues.each do |issue|
26 issues.each do |issue|
27 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
27 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
28 ancestors.pop
28 ancestors.pop
29 end
29 end
30 yield issue, ancestors.size
30 yield issue, ancestors.size
31 ancestors << issue unless issue.leaf?
31 ancestors << issue unless issue.leaf?
32 end
32 end
33 end
33 end
34
34
35 def grouped_issue_list(issues, query, issue_count_by_group, &block)
35 def grouped_issue_list(issues, query, issue_count_by_group, &block)
36 previous_group, first = false, true
36 previous_group, first = false, true
37 totals_by_group = query.totalable_columns.inject({}) do |h, column|
37 totals_by_group = query.totalable_columns.inject({}) do |h, column|
38 h[column] = query.total_by_group_for(column)
38 h[column] = query.total_by_group_for(column)
39 h
39 h
40 end
40 end
41 issue_list(issues) do |issue, level|
41 issue_list(issues) do |issue, level|
42 group_name = group_count = nil
42 group_name = group_count = nil
43 if query.grouped?
43 if query.grouped?
44 group = query.group_by_column.value(issue)
44 group = query.group_by_column.value(issue)
45 if first || group != previous_group
45 if first || group != previous_group
46 if group.blank? && group != false
46 if group.blank? && group != false
47 group_name = "(#{l(:label_blank_value)})"
47 group_name = "(#{l(:label_blank_value)})"
48 else
48 else
49 group_name = format_object(group)
49 group_name = format_object(group)
50 end
50 end
51 group_name ||= ""
51 group_name ||= ""
52 group_count = issue_count_by_group[group]
52 group_count = issue_count_by_group[group]
53 group_totals = totals_by_group.map {|column, t| total_tag(column, t[group] || 0)}.join(" ").html_safe
53 group_totals = totals_by_group.map {|column, t| total_tag(column, t[group] || 0)}.join(" ").html_safe
54 end
54 end
55 end
55 end
56 yield issue, level, group_name, group_count, group_totals
56 yield issue, level, group_name, group_count, group_totals
57 previous_group, first = group, false
57 previous_group, first = group, false
58 end
58 end
59 end
59 end
60
60
61 # Renders a HTML/CSS tooltip
61 # Renders a HTML/CSS tooltip
62 #
62 #
63 # To use, a trigger div is needed. This is a div with the class of "tooltip"
63 # To use, a trigger div is needed. This is a div with the class of "tooltip"
64 # that contains this method wrapped in a span with the class of "tip"
64 # that contains this method wrapped in a span with the class of "tip"
65 #
65 #
66 # <div class="tooltip"><%= link_to_issue(issue) %>
66 # <div class="tooltip"><%= link_to_issue(issue) %>
67 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
67 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
68 # </div>
68 # </div>
69 #
69 #
70 def render_issue_tooltip(issue)
70 def render_issue_tooltip(issue)
71 @cached_label_status ||= l(:field_status)
71 @cached_label_status ||= l(:field_status)
72 @cached_label_start_date ||= l(:field_start_date)
72 @cached_label_start_date ||= l(:field_start_date)
73 @cached_label_due_date ||= l(:field_due_date)
73 @cached_label_due_date ||= l(:field_due_date)
74 @cached_label_assigned_to ||= l(:field_assigned_to)
74 @cached_label_assigned_to ||= l(:field_assigned_to)
75 @cached_label_priority ||= l(:field_priority)
75 @cached_label_priority ||= l(:field_priority)
76 @cached_label_project ||= l(:field_project)
76 @cached_label_project ||= l(:field_project)
77
77
78 link_to_issue(issue) + "<br /><br />".html_safe +
78 link_to_issue(issue) + "<br /><br />".html_safe +
79 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
79 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
80 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
80 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
81 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
81 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
82 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
82 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
83 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
83 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
84 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
84 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
85 end
85 end
86
86
87 def issue_heading(issue)
87 def issue_heading(issue)
88 h("#{issue.tracker} ##{issue.id}")
88 h("#{issue.tracker} ##{issue.id}")
89 end
89 end
90
90
91 def render_issue_subject_with_tree(issue)
91 def render_issue_subject_with_tree(issue)
92 s = ''
92 s = ''
93 ancestors = issue.root? ? [] : issue.ancestors.visible.to_a
93 ancestors = issue.root? ? [] : issue.ancestors.visible.to_a
94 ancestors.each do |ancestor|
94 ancestors.each do |ancestor|
95 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
95 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
96 end
96 end
97 s << '<div>'
97 s << '<div>'
98 subject = h(issue.subject)
98 subject = h(issue.subject)
99 if issue.is_private?
99 if issue.is_private?
100 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
100 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
101 end
101 end
102 s << content_tag('h3', subject)
102 s << content_tag('h3', subject)
103 s << '</div>' * (ancestors.size + 1)
103 s << '</div>' * (ancestors.size + 1)
104 s.html_safe
104 s.html_safe
105 end
105 end
106
106
107 def render_descendants_tree(issue)
107 def render_descendants_tree(issue)
108 s = '<form><table class="list issues">'
108 s = '<form><table class="list issues">'
109 issue_list(issue.descendants.visible.preload(:status, :priority, :tracker, :assigned_to).sort_by(&:lft)) do |child, level|
109 issue_list(issue.descendants.visible.preload(:status, :priority, :tracker, :assigned_to).sort_by(&:lft)) do |child, level|
110 css = "issue issue-#{child.id} hascontextmenu #{issue.css_classes}"
110 css = "issue issue-#{child.id} hascontextmenu #{issue.css_classes}"
111 css << " idnt idnt-#{level}" if level > 0
111 css << " idnt idnt-#{level}" if level > 0
112 s << content_tag('tr',
112 s << content_tag('tr',
113 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
113 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
114 content_tag('td', link_to_issue(child, :project => (issue.project_id != child.project_id)), :class => 'subject', :style => 'width: 50%') +
114 content_tag('td', link_to_issue(child, :project => (issue.project_id != child.project_id)), :class => 'subject', :style => 'width: 50%') +
115 content_tag('td', h(child.status), :class => 'status') +
115 content_tag('td', h(child.status), :class => 'status') +
116 content_tag('td', link_to_user(child.assigned_to), :class => 'assigned_to') +
116 content_tag('td', link_to_user(child.assigned_to), :class => 'assigned_to') +
117 content_tag('td', child.disabled_core_fields.include?('done_ratio') ? '' : progress_bar(child.done_ratio), :class=> 'done_ratio'),
117 content_tag('td', child.disabled_core_fields.include?('done_ratio') ? '' : progress_bar(child.done_ratio), :class=> 'done_ratio'),
118 :class => css)
118 :class => css)
119 end
119 end
120 s << '</table></form>'
120 s << '</table></form>'
121 s.html_safe
121 s.html_safe
122 end
122 end
123
123
124 def issue_estimated_hours_details(issue)
124 def issue_estimated_hours_details(issue)
125 if issue.total_estimated_hours.present?
125 if issue.total_estimated_hours.present?
126 if issue.total_estimated_hours == issue.estimated_hours
126 if issue.total_estimated_hours == issue.estimated_hours
127 l_hours_short(issue.estimated_hours)
127 l_hours_short(issue.estimated_hours)
128 else
128 else
129 s = issue.estimated_hours.present? ? l_hours_short(issue.estimated_hours) : ""
129 s = issue.estimated_hours.present? ? l_hours_short(issue.estimated_hours) : ""
130 s << " (#{l(:label_total)}: #{l_hours_short(issue.total_estimated_hours)})"
130 s << " (#{l(:label_total)}: #{l_hours_short(issue.total_estimated_hours)})"
131 s.html_safe
131 s.html_safe
132 end
132 end
133 end
133 end
134 end
134 end
135
135
136 def issue_spent_hours_details(issue)
136 def issue_spent_hours_details(issue)
137 if issue.total_spent_hours > 0
137 if issue.total_spent_hours > 0
138 path = project_time_entries_path(issue.project, :issue_id => "~#{issue.id}")
139
138 if issue.total_spent_hours == issue.spent_hours
140 if issue.total_spent_hours == issue.spent_hours
139 link_to(l_hours_short(issue.spent_hours), issue_time_entries_path(issue))
141 link_to(l_hours_short(issue.spent_hours), path)
140 else
142 else
141 s = issue.spent_hours > 0 ? l_hours_short(issue.spent_hours) : ""
143 s = issue.spent_hours > 0 ? l_hours_short(issue.spent_hours) : ""
142 s << " (#{l(:label_total)}: #{link_to l_hours_short(issue.total_spent_hours), issue_time_entries_path(issue)})"
144 s << " (#{l(:label_total)}: #{link_to l_hours_short(issue.total_spent_hours), path})"
143 s.html_safe
145 s.html_safe
144 end
146 end
145 end
147 end
146 end
148 end
147
149
148 # Returns an array of error messages for bulk edited issues
150 # Returns an array of error messages for bulk edited issues
149 def bulk_edit_error_messages(issues)
151 def bulk_edit_error_messages(issues)
150 messages = {}
152 messages = {}
151 issues.each do |issue|
153 issues.each do |issue|
152 issue.errors.full_messages.each do |message|
154 issue.errors.full_messages.each do |message|
153 messages[message] ||= []
155 messages[message] ||= []
154 messages[message] << issue
156 messages[message] << issue
155 end
157 end
156 end
158 end
157 messages.map { |message, issues|
159 messages.map { |message, issues|
158 "#{message}: " + issues.map {|i| "##{i.id}"}.join(', ')
160 "#{message}: " + issues.map {|i| "##{i.id}"}.join(', ')
159 }
161 }
160 end
162 end
161
163
162 # Returns a link for adding a new subtask to the given issue
164 # Returns a link for adding a new subtask to the given issue
163 def link_to_new_subtask(issue)
165 def link_to_new_subtask(issue)
164 attrs = {
166 attrs = {
165 :parent_issue_id => issue
167 :parent_issue_id => issue
166 }
168 }
167 attrs[:tracker_id] = issue.tracker unless issue.tracker.disabled_core_fields.include?('parent_issue_id')
169 attrs[:tracker_id] = issue.tracker unless issue.tracker.disabled_core_fields.include?('parent_issue_id')
168 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
170 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
169 end
171 end
170
172
171 def trackers_options_for_select(issue)
173 def trackers_options_for_select(issue)
172 trackers = issue.allowed_target_trackers
174 trackers = issue.allowed_target_trackers
173 if issue.new_record? && issue.parent_issue_id.present?
175 if issue.new_record? && issue.parent_issue_id.present?
174 trackers = trackers.reject do |tracker|
176 trackers = trackers.reject do |tracker|
175 issue.tracker_id != tracker.id && tracker.disabled_core_fields.include?('parent_issue_id')
177 issue.tracker_id != tracker.id && tracker.disabled_core_fields.include?('parent_issue_id')
176 end
178 end
177 end
179 end
178 trackers.collect {|t| [t.name, t.id]}
180 trackers.collect {|t| [t.name, t.id]}
179 end
181 end
180
182
181 class IssueFieldsRows
183 class IssueFieldsRows
182 include ActionView::Helpers::TagHelper
184 include ActionView::Helpers::TagHelper
183
185
184 def initialize
186 def initialize
185 @left = []
187 @left = []
186 @right = []
188 @right = []
187 end
189 end
188
190
189 def left(*args)
191 def left(*args)
190 args.any? ? @left << cells(*args) : @left
192 args.any? ? @left << cells(*args) : @left
191 end
193 end
192
194
193 def right(*args)
195 def right(*args)
194 args.any? ? @right << cells(*args) : @right
196 args.any? ? @right << cells(*args) : @right
195 end
197 end
196
198
197 def size
199 def size
198 @left.size > @right.size ? @left.size : @right.size
200 @left.size > @right.size ? @left.size : @right.size
199 end
201 end
200
202
201 def to_html
203 def to_html
202 content =
204 content =
203 content_tag('div', @left.reduce(&:+), :class => 'splitcontentleft') +
205 content_tag('div', @left.reduce(&:+), :class => 'splitcontentleft') +
204 content_tag('div', @right.reduce(&:+), :class => 'splitcontentleft')
206 content_tag('div', @right.reduce(&:+), :class => 'splitcontentleft')
205
207
206 content_tag('div', content, :class => 'splitcontent')
208 content_tag('div', content, :class => 'splitcontent')
207 end
209 end
208
210
209 def cells(label, text, options={})
211 def cells(label, text, options={})
210 options[:class] = [options[:class] || "", 'attribute'].join(' ')
212 options[:class] = [options[:class] || "", 'attribute'].join(' ')
211 content_tag 'div',
213 content_tag 'div',
212 content_tag('div', label + ":", :class => 'label') + content_tag('div', text, :class => 'value'),
214 content_tag('div', label + ":", :class => 'label') + content_tag('div', text, :class => 'value'),
213 options
215 options
214 end
216 end
215 end
217 end
216
218
217 def issue_fields_rows
219 def issue_fields_rows
218 r = IssueFieldsRows.new
220 r = IssueFieldsRows.new
219 yield r
221 yield r
220 r.to_html
222 r.to_html
221 end
223 end
222
224
223 def render_custom_fields_rows(issue)
225 def render_custom_fields_rows(issue)
224 values = issue.visible_custom_field_values
226 values = issue.visible_custom_field_values
225 return if values.empty?
227 return if values.empty?
226 half = (values.size / 2.0).ceil
228 half = (values.size / 2.0).ceil
227 issue_fields_rows do |rows|
229 issue_fields_rows do |rows|
228 values.each_with_index do |value, i|
230 values.each_with_index do |value, i|
229 css = "cf_#{value.custom_field.id}"
231 css = "cf_#{value.custom_field.id}"
230 m = (i < half ? :left : :right)
232 m = (i < half ? :left : :right)
231 rows.send m, custom_field_name_tag(value.custom_field), show_value(value), :class => css
233 rows.send m, custom_field_name_tag(value.custom_field), show_value(value), :class => css
232 end
234 end
233 end
235 end
234 end
236 end
235
237
236 # Returns the path for updating the issue form
238 # Returns the path for updating the issue form
237 # with project as the current project
239 # with project as the current project
238 def update_issue_form_path(project, issue)
240 def update_issue_form_path(project, issue)
239 options = {:format => 'js'}
241 options = {:format => 'js'}
240 if issue.new_record?
242 if issue.new_record?
241 if project
243 if project
242 new_project_issue_path(project, options)
244 new_project_issue_path(project, options)
243 else
245 else
244 new_issue_path(options)
246 new_issue_path(options)
245 end
247 end
246 else
248 else
247 edit_issue_path(issue, options)
249 edit_issue_path(issue, options)
248 end
250 end
249 end
251 end
250
252
251 # Returns the number of descendants for an array of issues
253 # Returns the number of descendants for an array of issues
252 def issues_descendant_count(issues)
254 def issues_descendant_count(issues)
253 ids = issues.reject(&:leaf?).map {|issue| issue.descendants.ids}.flatten.uniq
255 ids = issues.reject(&:leaf?).map {|issue| issue.descendants.ids}.flatten.uniq
254 ids -= issues.map(&:id)
256 ids -= issues.map(&:id)
255 ids.size
257 ids.size
256 end
258 end
257
259
258 def issues_destroy_confirmation_message(issues)
260 def issues_destroy_confirmation_message(issues)
259 issues = [issues] unless issues.is_a?(Array)
261 issues = [issues] unless issues.is_a?(Array)
260 message = l(:text_issues_destroy_confirmation)
262 message = l(:text_issues_destroy_confirmation)
261
263
262 descendant_count = issues_descendant_count(issues)
264 descendant_count = issues_descendant_count(issues)
263 if descendant_count > 0
265 if descendant_count > 0
264 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
266 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
265 end
267 end
266 message
268 message
267 end
269 end
268
270
269 # Returns an array of users that are proposed as watchers
271 # Returns an array of users that are proposed as watchers
270 # on the new issue form
272 # on the new issue form
271 def users_for_new_issue_watchers(issue)
273 def users_for_new_issue_watchers(issue)
272 users = issue.watcher_users
274 users = issue.watcher_users
273 if issue.project.users.count <= 20
275 if issue.project.users.count <= 20
274 users = (users + issue.project.users.sort).uniq
276 users = (users + issue.project.users.sort).uniq
275 end
277 end
276 users
278 users
277 end
279 end
278
280
279 def email_issue_attributes(issue, user)
281 def email_issue_attributes(issue, user)
280 items = []
282 items = []
281 %w(author status priority assigned_to category fixed_version).each do |attribute|
283 %w(author status priority assigned_to category fixed_version).each do |attribute|
282 unless issue.disabled_core_fields.include?(attribute+"_id")
284 unless issue.disabled_core_fields.include?(attribute+"_id")
283 items << "#{l("field_#{attribute}")}: #{issue.send attribute}"
285 items << "#{l("field_#{attribute}")}: #{issue.send attribute}"
284 end
286 end
285 end
287 end
286 issue.visible_custom_field_values(user).each do |value|
288 issue.visible_custom_field_values(user).each do |value|
287 items << "#{value.custom_field.name}: #{show_value(value, false)}"
289 items << "#{value.custom_field.name}: #{show_value(value, false)}"
288 end
290 end
289 items
291 items
290 end
292 end
291
293
292 def render_email_issue_attributes(issue, user, html=false)
294 def render_email_issue_attributes(issue, user, html=false)
293 items = email_issue_attributes(issue, user)
295 items = email_issue_attributes(issue, user)
294 if html
296 if html
295 content_tag('ul', items.map{|s| content_tag('li', s)}.join("\n").html_safe)
297 content_tag('ul', items.map{|s| content_tag('li', s)}.join("\n").html_safe)
296 else
298 else
297 items.map{|s| "* #{s}"}.join("\n")
299 items.map{|s| "* #{s}"}.join("\n")
298 end
300 end
299 end
301 end
300
302
301 # Returns the textual representation of a journal details
303 # Returns the textual representation of a journal details
302 # as an array of strings
304 # as an array of strings
303 def details_to_strings(details, no_html=false, options={})
305 def details_to_strings(details, no_html=false, options={})
304 options[:only_path] = (options[:only_path] == false ? false : true)
306 options[:only_path] = (options[:only_path] == false ? false : true)
305 strings = []
307 strings = []
306 values_by_field = {}
308 values_by_field = {}
307 details.each do |detail|
309 details.each do |detail|
308 if detail.property == 'cf'
310 if detail.property == 'cf'
309 field = detail.custom_field
311 field = detail.custom_field
310 if field && field.multiple?
312 if field && field.multiple?
311 values_by_field[field] ||= {:added => [], :deleted => []}
313 values_by_field[field] ||= {:added => [], :deleted => []}
312 if detail.old_value
314 if detail.old_value
313 values_by_field[field][:deleted] << detail.old_value
315 values_by_field[field][:deleted] << detail.old_value
314 end
316 end
315 if detail.value
317 if detail.value
316 values_by_field[field][:added] << detail.value
318 values_by_field[field][:added] << detail.value
317 end
319 end
318 next
320 next
319 end
321 end
320 end
322 end
321 strings << show_detail(detail, no_html, options)
323 strings << show_detail(detail, no_html, options)
322 end
324 end
323 if values_by_field.present?
325 if values_by_field.present?
324 multiple_values_detail = Struct.new(:property, :prop_key, :custom_field, :old_value, :value)
326 multiple_values_detail = Struct.new(:property, :prop_key, :custom_field, :old_value, :value)
325 values_by_field.each do |field, changes|
327 values_by_field.each do |field, changes|
326 if changes[:added].any?
328 if changes[:added].any?
327 detail = multiple_values_detail.new('cf', field.id.to_s, field)
329 detail = multiple_values_detail.new('cf', field.id.to_s, field)
328 detail.value = changes[:added]
330 detail.value = changes[:added]
329 strings << show_detail(detail, no_html, options)
331 strings << show_detail(detail, no_html, options)
330 end
332 end
331 if changes[:deleted].any?
333 if changes[:deleted].any?
332 detail = multiple_values_detail.new('cf', field.id.to_s, field)
334 detail = multiple_values_detail.new('cf', field.id.to_s, field)
333 detail.old_value = changes[:deleted]
335 detail.old_value = changes[:deleted]
334 strings << show_detail(detail, no_html, options)
336 strings << show_detail(detail, no_html, options)
335 end
337 end
336 end
338 end
337 end
339 end
338 strings
340 strings
339 end
341 end
340
342
341 # Returns the textual representation of a single journal detail
343 # Returns the textual representation of a single journal detail
342 def show_detail(detail, no_html=false, options={})
344 def show_detail(detail, no_html=false, options={})
343 multiple = false
345 multiple = false
344 show_diff = false
346 show_diff = false
345
347
346 case detail.property
348 case detail.property
347 when 'attr'
349 when 'attr'
348 field = detail.prop_key.to_s.gsub(/\_id$/, "")
350 field = detail.prop_key.to_s.gsub(/\_id$/, "")
349 label = l(("field_" + field).to_sym)
351 label = l(("field_" + field).to_sym)
350 case detail.prop_key
352 case detail.prop_key
351 when 'due_date', 'start_date'
353 when 'due_date', 'start_date'
352 value = format_date(detail.value.to_date) if detail.value
354 value = format_date(detail.value.to_date) if detail.value
353 old_value = format_date(detail.old_value.to_date) if detail.old_value
355 old_value = format_date(detail.old_value.to_date) if detail.old_value
354
356
355 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
357 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
356 'priority_id', 'category_id', 'fixed_version_id'
358 'priority_id', 'category_id', 'fixed_version_id'
357 value = find_name_by_reflection(field, detail.value)
359 value = find_name_by_reflection(field, detail.value)
358 old_value = find_name_by_reflection(field, detail.old_value)
360 old_value = find_name_by_reflection(field, detail.old_value)
359
361
360 when 'estimated_hours'
362 when 'estimated_hours'
361 value = l_hours_short(detail.value.to_f) unless detail.value.blank?
363 value = l_hours_short(detail.value.to_f) unless detail.value.blank?
362 old_value = l_hours_short(detail.old_value.to_f) unless detail.old_value.blank?
364 old_value = l_hours_short(detail.old_value.to_f) unless detail.old_value.blank?
363
365
364 when 'parent_id'
366 when 'parent_id'
365 label = l(:field_parent_issue)
367 label = l(:field_parent_issue)
366 value = "##{detail.value}" unless detail.value.blank?
368 value = "##{detail.value}" unless detail.value.blank?
367 old_value = "##{detail.old_value}" unless detail.old_value.blank?
369 old_value = "##{detail.old_value}" unless detail.old_value.blank?
368
370
369 when 'is_private'
371 when 'is_private'
370 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
372 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
371 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
373 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
372
374
373 when 'description'
375 when 'description'
374 show_diff = true
376 show_diff = true
375 end
377 end
376 when 'cf'
378 when 'cf'
377 custom_field = detail.custom_field
379 custom_field = detail.custom_field
378 if custom_field
380 if custom_field
379 label = custom_field.name
381 label = custom_field.name
380 if custom_field.format.class.change_as_diff
382 if custom_field.format.class.change_as_diff
381 show_diff = true
383 show_diff = true
382 else
384 else
383 multiple = custom_field.multiple?
385 multiple = custom_field.multiple?
384 value = format_value(detail.value, custom_field) if detail.value
386 value = format_value(detail.value, custom_field) if detail.value
385 old_value = format_value(detail.old_value, custom_field) if detail.old_value
387 old_value = format_value(detail.old_value, custom_field) if detail.old_value
386 end
388 end
387 end
389 end
388 when 'attachment'
390 when 'attachment'
389 label = l(:label_attachment)
391 label = l(:label_attachment)
390 when 'relation'
392 when 'relation'
391 if detail.value && !detail.old_value
393 if detail.value && !detail.old_value
392 rel_issue = Issue.visible.find_by_id(detail.value)
394 rel_issue = Issue.visible.find_by_id(detail.value)
393 value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.value}" :
395 value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.value}" :
394 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
396 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
395 elsif detail.old_value && !detail.value
397 elsif detail.old_value && !detail.value
396 rel_issue = Issue.visible.find_by_id(detail.old_value)
398 rel_issue = Issue.visible.find_by_id(detail.old_value)
397 old_value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.old_value}" :
399 old_value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.old_value}" :
398 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
400 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
399 end
401 end
400 relation_type = IssueRelation::TYPES[detail.prop_key]
402 relation_type = IssueRelation::TYPES[detail.prop_key]
401 label = l(relation_type[:name]) if relation_type
403 label = l(relation_type[:name]) if relation_type
402 end
404 end
403 call_hook(:helper_issues_show_detail_after_setting,
405 call_hook(:helper_issues_show_detail_after_setting,
404 {:detail => detail, :label => label, :value => value, :old_value => old_value })
406 {:detail => detail, :label => label, :value => value, :old_value => old_value })
405
407
406 label ||= detail.prop_key
408 label ||= detail.prop_key
407 value ||= detail.value
409 value ||= detail.value
408 old_value ||= detail.old_value
410 old_value ||= detail.old_value
409
411
410 unless no_html
412 unless no_html
411 label = content_tag('strong', label)
413 label = content_tag('strong', label)
412 old_value = content_tag("i", h(old_value)) if detail.old_value
414 old_value = content_tag("i", h(old_value)) if detail.old_value
413 if detail.old_value && detail.value.blank? && detail.property != 'relation'
415 if detail.old_value && detail.value.blank? && detail.property != 'relation'
414 old_value = content_tag("del", old_value)
416 old_value = content_tag("del", old_value)
415 end
417 end
416 if detail.property == 'attachment' && value.present? &&
418 if detail.property == 'attachment' && value.present? &&
417 atta = detail.journal.journalized.attachments.detect {|a| a.id == detail.prop_key.to_i}
419 atta = detail.journal.journalized.attachments.detect {|a| a.id == detail.prop_key.to_i}
418 # Link to the attachment if it has not been removed
420 # Link to the attachment if it has not been removed
419 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
421 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
420 if options[:only_path] != false && (atta.is_text? || atta.is_image?)
422 if options[:only_path] != false && (atta.is_text? || atta.is_image?)
421 value += ' '
423 value += ' '
422 value += link_to(l(:button_view),
424 value += link_to(l(:button_view),
423 { :controller => 'attachments', :action => 'show',
425 { :controller => 'attachments', :action => 'show',
424 :id => atta, :filename => atta.filename },
426 :id => atta, :filename => atta.filename },
425 :class => 'icon-only icon-magnifier',
427 :class => 'icon-only icon-magnifier',
426 :title => l(:button_view))
428 :title => l(:button_view))
427 end
429 end
428 else
430 else
429 value = content_tag("i", h(value)) if value
431 value = content_tag("i", h(value)) if value
430 end
432 end
431 end
433 end
432
434
433 if show_diff
435 if show_diff
434 s = l(:text_journal_changed_no_detail, :label => label)
436 s = l(:text_journal_changed_no_detail, :label => label)
435 unless no_html
437 unless no_html
436 diff_link = link_to 'diff',
438 diff_link = link_to 'diff',
437 diff_journal_url(detail.journal_id, :detail_id => detail.id, :only_path => options[:only_path]),
439 diff_journal_url(detail.journal_id, :detail_id => detail.id, :only_path => options[:only_path]),
438 :title => l(:label_view_diff)
440 :title => l(:label_view_diff)
439 s << " (#{ diff_link })"
441 s << " (#{ diff_link })"
440 end
442 end
441 s.html_safe
443 s.html_safe
442 elsif detail.value.present?
444 elsif detail.value.present?
443 case detail.property
445 case detail.property
444 when 'attr', 'cf'
446 when 'attr', 'cf'
445 if detail.old_value.present?
447 if detail.old_value.present?
446 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
448 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
447 elsif multiple
449 elsif multiple
448 l(:text_journal_added, :label => label, :value => value).html_safe
450 l(:text_journal_added, :label => label, :value => value).html_safe
449 else
451 else
450 l(:text_journal_set_to, :label => label, :value => value).html_safe
452 l(:text_journal_set_to, :label => label, :value => value).html_safe
451 end
453 end
452 when 'attachment', 'relation'
454 when 'attachment', 'relation'
453 l(:text_journal_added, :label => label, :value => value).html_safe
455 l(:text_journal_added, :label => label, :value => value).html_safe
454 end
456 end
455 else
457 else
456 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
458 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
457 end
459 end
458 end
460 end
459
461
460 # Find the name of an associated record stored in the field attribute
462 # Find the name of an associated record stored in the field attribute
461 def find_name_by_reflection(field, id)
463 def find_name_by_reflection(field, id)
462 unless id.present?
464 unless id.present?
463 return nil
465 return nil
464 end
466 end
465 @detail_value_name_by_reflection ||= Hash.new do |hash, key|
467 @detail_value_name_by_reflection ||= Hash.new do |hash, key|
466 association = Issue.reflect_on_association(key.first.to_sym)
468 association = Issue.reflect_on_association(key.first.to_sym)
467 name = nil
469 name = nil
468 if association
470 if association
469 record = association.klass.find_by_id(key.last)
471 record = association.klass.find_by_id(key.last)
470 if record
472 if record
471 name = record.name.force_encoding('UTF-8')
473 name = record.name.force_encoding('UTF-8')
472 end
474 end
473 end
475 end
474 hash[key] = name
476 hash[key] = name
475 end
477 end
476 @detail_value_name_by_reflection[[field, id]]
478 @detail_value_name_by_reflection[[field, id]]
477 end
479 end
478
480
479 # Renders issue children recursively
481 # Renders issue children recursively
480 def render_api_issue_children(issue, api)
482 def render_api_issue_children(issue, api)
481 return if issue.leaf?
483 return if issue.leaf?
482 api.array :children do
484 api.array :children do
483 issue.children.each do |child|
485 issue.children.each do |child|
484 api.issue(:id => child.id) do
486 api.issue(:id => child.id) do
485 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
487 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
486 api.subject child.subject
488 api.subject child.subject
487 render_api_issue_children(child, api)
489 render_api_issue_children(child, api)
488 end
490 end
489 end
491 end
490 end
492 end
491 end
493 end
492 end
494 end
@@ -1,312 +1,314
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module QueriesHelper
20 module QueriesHelper
21 include ApplicationHelper
21 include ApplicationHelper
22
22
23 def filters_options_for_select(query)
23 def filters_options_for_select(query)
24 ungrouped = []
24 ungrouped = []
25 grouped = {}
25 grouped = {}
26 query.available_filters.map do |field, field_options|
26 query.available_filters.map do |field, field_options|
27 if [:tree, :relation].include?(field_options[:type])
27 if field_options[:type] == :relation
28 group = :label_relations
28 group = :label_relations
29 elsif field_options[:type] == :tree
30 group = query.is_a?(IssueQuery) ? :label_relations : nil
29 elsif field =~ /^(.+)\./
31 elsif field =~ /^(.+)\./
30 # association filters
32 # association filters
31 group = "field_#{$1}"
33 group = "field_#{$1}"
32 elsif %w(member_of_group assigned_to_role).include?(field)
34 elsif %w(member_of_group assigned_to_role).include?(field)
33 group = :field_assigned_to
35 group = :field_assigned_to
34 elsif field_options[:type] == :date_past || field_options[:type] == :date
36 elsif field_options[:type] == :date_past || field_options[:type] == :date
35 group = :label_date
37 group = :label_date
36 end
38 end
37 if group
39 if group
38 (grouped[group] ||= []) << [field_options[:name], field]
40 (grouped[group] ||= []) << [field_options[:name], field]
39 else
41 else
40 ungrouped << [field_options[:name], field]
42 ungrouped << [field_options[:name], field]
41 end
43 end
42 end
44 end
43 # Don't group dates if there's only one (eg. time entries filters)
45 # Don't group dates if there's only one (eg. time entries filters)
44 if grouped[:label_date].try(:size) == 1
46 if grouped[:label_date].try(:size) == 1
45 ungrouped << grouped.delete(:label_date).first
47 ungrouped << grouped.delete(:label_date).first
46 end
48 end
47 s = options_for_select([[]] + ungrouped)
49 s = options_for_select([[]] + ungrouped)
48 if grouped.present?
50 if grouped.present?
49 localized_grouped = grouped.map {|k,v| [l(k), v]}
51 localized_grouped = grouped.map {|k,v| [l(k), v]}
50 s << grouped_options_for_select(localized_grouped)
52 s << grouped_options_for_select(localized_grouped)
51 end
53 end
52 s
54 s
53 end
55 end
54
56
55 def query_filters_hidden_tags(query)
57 def query_filters_hidden_tags(query)
56 tags = ''.html_safe
58 tags = ''.html_safe
57 query.filters.each do |field, options|
59 query.filters.each do |field, options|
58 tags << hidden_field_tag("f[]", field, :id => nil)
60 tags << hidden_field_tag("f[]", field, :id => nil)
59 tags << hidden_field_tag("op[#{field}]", options[:operator], :id => nil)
61 tags << hidden_field_tag("op[#{field}]", options[:operator], :id => nil)
60 options[:values].each do |value|
62 options[:values].each do |value|
61 tags << hidden_field_tag("v[#{field}][]", value, :id => nil)
63 tags << hidden_field_tag("v[#{field}][]", value, :id => nil)
62 end
64 end
63 end
65 end
64 tags
66 tags
65 end
67 end
66
68
67 def query_columns_hidden_tags(query)
69 def query_columns_hidden_tags(query)
68 tags = ''.html_safe
70 tags = ''.html_safe
69 query.columns.each do |column|
71 query.columns.each do |column|
70 tags << hidden_field_tag("c[]", column.name, :id => nil)
72 tags << hidden_field_tag("c[]", column.name, :id => nil)
71 end
73 end
72 tags
74 tags
73 end
75 end
74
76
75 def query_hidden_tags(query)
77 def query_hidden_tags(query)
76 query_filters_hidden_tags(query) + query_columns_hidden_tags(query)
78 query_filters_hidden_tags(query) + query_columns_hidden_tags(query)
77 end
79 end
78
80
79 def available_block_columns_tags(query)
81 def available_block_columns_tags(query)
80 tags = ''.html_safe
82 tags = ''.html_safe
81 query.available_block_columns.each do |column|
83 query.available_block_columns.each do |column|
82 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column), :id => nil) + " #{column.caption}", :class => 'inline')
84 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column), :id => nil) + " #{column.caption}", :class => 'inline')
83 end
85 end
84 tags
86 tags
85 end
87 end
86
88
87 def available_totalable_columns_tags(query)
89 def available_totalable_columns_tags(query)
88 tags = ''.html_safe
90 tags = ''.html_safe
89 query.available_totalable_columns.each do |column|
91 query.available_totalable_columns.each do |column|
90 tags << content_tag('label', check_box_tag('t[]', column.name.to_s, query.totalable_columns.include?(column), :id => nil) + " #{column.caption}", :class => 'inline')
92 tags << content_tag('label', check_box_tag('t[]', column.name.to_s, query.totalable_columns.include?(column), :id => nil) + " #{column.caption}", :class => 'inline')
91 end
93 end
92 tags << hidden_field_tag('t[]', '')
94 tags << hidden_field_tag('t[]', '')
93 tags
95 tags
94 end
96 end
95
97
96 def query_available_inline_columns_options(query)
98 def query_available_inline_columns_options(query)
97 (query.available_inline_columns - query.columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
99 (query.available_inline_columns - query.columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
98 end
100 end
99
101
100 def query_selected_inline_columns_options(query)
102 def query_selected_inline_columns_options(query)
101 (query.inline_columns & query.available_inline_columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
103 (query.inline_columns & query.available_inline_columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
102 end
104 end
103
105
104 def render_query_columns_selection(query, options={})
106 def render_query_columns_selection(query, options={})
105 tag_name = (options[:name] || 'c') + '[]'
107 tag_name = (options[:name] || 'c') + '[]'
106 render :partial => 'queries/columns', :locals => {:query => query, :tag_name => tag_name}
108 render :partial => 'queries/columns', :locals => {:query => query, :tag_name => tag_name}
107 end
109 end
108
110
109 def render_query_totals(query)
111 def render_query_totals(query)
110 return unless query.totalable_columns.present?
112 return unless query.totalable_columns.present?
111 totals = query.totalable_columns.map do |column|
113 totals = query.totalable_columns.map do |column|
112 total_tag(column, query.total_for(column))
114 total_tag(column, query.total_for(column))
113 end
115 end
114 content_tag('p', totals.join(" ").html_safe, :class => "query-totals")
116 content_tag('p', totals.join(" ").html_safe, :class => "query-totals")
115 end
117 end
116
118
117 def total_tag(column, value)
119 def total_tag(column, value)
118 label = content_tag('span', "#{column.caption}:")
120 label = content_tag('span', "#{column.caption}:")
119 value = content_tag('span', format_object(value), :class => 'value')
121 value = content_tag('span', format_object(value), :class => 'value')
120 content_tag('span', label + " " + value, :class => "total-for-#{column.name.to_s.dasherize}")
122 content_tag('span', label + " " + value, :class => "total-for-#{column.name.to_s.dasherize}")
121 end
123 end
122
124
123 def column_header(column)
125 def column_header(column)
124 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
126 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
125 :default_order => column.default_order) :
127 :default_order => column.default_order) :
126 content_tag('th', h(column.caption))
128 content_tag('th', h(column.caption))
127 end
129 end
128
130
129 def column_content(column, issue)
131 def column_content(column, issue)
130 value = column.value_object(issue)
132 value = column.value_object(issue)
131 if value.is_a?(Array)
133 if value.is_a?(Array)
132 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
134 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
133 else
135 else
134 column_value(column, issue, value)
136 column_value(column, issue, value)
135 end
137 end
136 end
138 end
137
139
138 def column_value(column, issue, value)
140 def column_value(column, issue, value)
139 case column.name
141 case column.name
140 when :id
142 when :id
141 link_to value, issue_path(issue)
143 link_to value, issue_path(issue)
142 when :subject
144 when :subject
143 link_to value, issue_path(issue)
145 link_to value, issue_path(issue)
144 when :parent
146 when :parent
145 value ? (value.visible? ? link_to_issue(value, :subject => false) : "##{value.id}") : ''
147 value ? (value.visible? ? link_to_issue(value, :subject => false) : "##{value.id}") : ''
146 when :description
148 when :description
147 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
149 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
148 when :done_ratio
150 when :done_ratio
149 progress_bar(value)
151 progress_bar(value)
150 when :relations
152 when :relations
151 content_tag('span',
153 content_tag('span',
152 value.to_s(issue) {|other| link_to_issue(other, :subject => false, :tracker => false)}.html_safe,
154 value.to_s(issue) {|other| link_to_issue(other, :subject => false, :tracker => false)}.html_safe,
153 :class => value.css_classes_for(issue))
155 :class => value.css_classes_for(issue))
154 else
156 else
155 format_object(value)
157 format_object(value)
156 end
158 end
157 end
159 end
158
160
159 def csv_content(column, issue)
161 def csv_content(column, issue)
160 value = column.value_object(issue)
162 value = column.value_object(issue)
161 if value.is_a?(Array)
163 if value.is_a?(Array)
162 value.collect {|v| csv_value(column, issue, v)}.compact.join(', ')
164 value.collect {|v| csv_value(column, issue, v)}.compact.join(', ')
163 else
165 else
164 csv_value(column, issue, value)
166 csv_value(column, issue, value)
165 end
167 end
166 end
168 end
167
169
168 def csv_value(column, object, value)
170 def csv_value(column, object, value)
169 format_object(value, false) do |value|
171 format_object(value, false) do |value|
170 case value.class.name
172 case value.class.name
171 when 'Float'
173 when 'Float'
172 sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator))
174 sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator))
173 when 'IssueRelation'
175 when 'IssueRelation'
174 value.to_s(object)
176 value.to_s(object)
175 when 'Issue'
177 when 'Issue'
176 if object.is_a?(TimeEntry)
178 if object.is_a?(TimeEntry)
177 "#{value.tracker} ##{value.id}: #{value.subject}"
179 "#{value.tracker} ##{value.id}: #{value.subject}"
178 else
180 else
179 value.id
181 value.id
180 end
182 end
181 else
183 else
182 value
184 value
183 end
185 end
184 end
186 end
185 end
187 end
186
188
187 def query_to_csv(items, query, options={})
189 def query_to_csv(items, query, options={})
188 options ||= {}
190 options ||= {}
189 columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns)
191 columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns)
190 query.available_block_columns.each do |column|
192 query.available_block_columns.each do |column|
191 if options[column.name].present?
193 if options[column.name].present?
192 columns << column
194 columns << column
193 end
195 end
194 end
196 end
195
197
196 Redmine::Export::CSV.generate do |csv|
198 Redmine::Export::CSV.generate do |csv|
197 # csv header fields
199 # csv header fields
198 csv << columns.map {|c| c.caption.to_s}
200 csv << columns.map {|c| c.caption.to_s}
199 # csv lines
201 # csv lines
200 items.each do |item|
202 items.each do |item|
201 csv << columns.map {|c| csv_content(c, item)}
203 csv << columns.map {|c| csv_content(c, item)}
202 end
204 end
203 end
205 end
204 end
206 end
205
207
206 # Retrieve query from session or build a new query
208 # Retrieve query from session or build a new query
207 def retrieve_query(klass=IssueQuery, use_session=true)
209 def retrieve_query(klass=IssueQuery, use_session=true)
208 session_key = klass.name.underscore.to_sym
210 session_key = klass.name.underscore.to_sym
209
211
210 if params[:query_id].present?
212 if params[:query_id].present?
211 cond = "project_id IS NULL"
213 cond = "project_id IS NULL"
212 cond << " OR project_id = #{@project.id}" if @project
214 cond << " OR project_id = #{@project.id}" if @project
213 @query = klass.where(cond).find(params[:query_id])
215 @query = klass.where(cond).find(params[:query_id])
214 raise ::Unauthorized unless @query.visible?
216 raise ::Unauthorized unless @query.visible?
215 @query.project = @project
217 @query.project = @project
216 session[session_key] = {:id => @query.id, :project_id => @query.project_id} if use_session
218 session[session_key] = {:id => @query.id, :project_id => @query.project_id} if use_session
217 sort_clear
219 sort_clear
218 elsif api_request? || params[:set_filter] || !use_session || session[session_key].nil? || session[session_key][:project_id] != (@project ? @project.id : nil)
220 elsif api_request? || params[:set_filter] || !use_session || session[session_key].nil? || session[session_key][:project_id] != (@project ? @project.id : nil)
219 # Give it a name, required to be valid
221 # Give it a name, required to be valid
220 @query = klass.new(:name => "_", :project => @project)
222 @query = klass.new(:name => "_", :project => @project)
221 @query.build_from_params(params)
223 @query.build_from_params(params)
222 session[session_key] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names, :totalable_names => @query.totalable_names} if use_session
224 session[session_key] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names, :totalable_names => @query.totalable_names} if use_session
223 else
225 else
224 # retrieve from session
226 # retrieve from session
225 @query = nil
227 @query = nil
226 @query = klass.find_by_id(session[session_key][:id]) if session[session_key][:id]
228 @query = klass.find_by_id(session[session_key][:id]) if session[session_key][:id]
227 @query ||= klass.new(:name => "_", :filters => session[session_key][:filters], :group_by => session[session_key][:group_by], :column_names => session[session_key][:column_names], :totalable_names => session[session_key][:totalable_names])
229 @query ||= klass.new(:name => "_", :filters => session[session_key][:filters], :group_by => session[session_key][:group_by], :column_names => session[session_key][:column_names], :totalable_names => session[session_key][:totalable_names])
228 @query.project = @project
230 @query.project = @project
229 end
231 end
230 end
232 end
231
233
232 def retrieve_query_from_session
234 def retrieve_query_from_session
233 if session[:query]
235 if session[:query]
234 if session[:query][:id]
236 if session[:query][:id]
235 @query = IssueQuery.find_by_id(session[:query][:id])
237 @query = IssueQuery.find_by_id(session[:query][:id])
236 return unless @query
238 return unless @query
237 else
239 else
238 @query = IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names], :totalable_names => session[:query][:totalable_names])
240 @query = IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names], :totalable_names => session[:query][:totalable_names])
239 end
241 end
240 if session[:query].has_key?(:project_id)
242 if session[:query].has_key?(:project_id)
241 @query.project_id = session[:query][:project_id]
243 @query.project_id = session[:query][:project_id]
242 else
244 else
243 @query.project = @project
245 @query.project = @project
244 end
246 end
245 @query
247 @query
246 end
248 end
247 end
249 end
248
250
249 # Returns the query definition as hidden field tags
251 # Returns the query definition as hidden field tags
250 def query_as_hidden_field_tags(query)
252 def query_as_hidden_field_tags(query)
251 tags = hidden_field_tag("set_filter", "1", :id => nil)
253 tags = hidden_field_tag("set_filter", "1", :id => nil)
252
254
253 if query.filters.present?
255 if query.filters.present?
254 query.filters.each do |field, filter|
256 query.filters.each do |field, filter|
255 tags << hidden_field_tag("f[]", field, :id => nil)
257 tags << hidden_field_tag("f[]", field, :id => nil)
256 tags << hidden_field_tag("op[#{field}]", filter[:operator], :id => nil)
258 tags << hidden_field_tag("op[#{field}]", filter[:operator], :id => nil)
257 filter[:values].each do |value|
259 filter[:values].each do |value|
258 tags << hidden_field_tag("v[#{field}][]", value, :id => nil)
260 tags << hidden_field_tag("v[#{field}][]", value, :id => nil)
259 end
261 end
260 end
262 end
261 else
263 else
262 tags << hidden_field_tag("f[]", "", :id => nil)
264 tags << hidden_field_tag("f[]", "", :id => nil)
263 end
265 end
264 if query.column_names.present?
266 if query.column_names.present?
265 query.column_names.each do |name|
267 query.column_names.each do |name|
266 tags << hidden_field_tag("c[]", name, :id => nil)
268 tags << hidden_field_tag("c[]", name, :id => nil)
267 end
269 end
268 end
270 end
269 if query.totalable_names.present?
271 if query.totalable_names.present?
270 query.totalable_names.each do |name|
272 query.totalable_names.each do |name|
271 tags << hidden_field_tag("t[]", name, :id => nil)
273 tags << hidden_field_tag("t[]", name, :id => nil)
272 end
274 end
273 end
275 end
274 if query.group_by.present?
276 if query.group_by.present?
275 tags << hidden_field_tag("group_by", query.group_by, :id => nil)
277 tags << hidden_field_tag("group_by", query.group_by, :id => nil)
276 end
278 end
277
279
278 tags
280 tags
279 end
281 end
280
282
281 # Returns the queries that are rendered in the sidebar
283 # Returns the queries that are rendered in the sidebar
282 def sidebar_queries(klass, project)
284 def sidebar_queries(klass, project)
283 klass.visible.global_or_on_project(@project).sorted.to_a
285 klass.visible.global_or_on_project(@project).sorted.to_a
284 end
286 end
285
287
286 # Renders a group of queries
288 # Renders a group of queries
287 def query_links(title, queries)
289 def query_links(title, queries)
288 return '' if queries.empty?
290 return '' if queries.empty?
289 # links to #index on issues/show
291 # links to #index on issues/show
290 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : {}
292 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : {}
291
293
292 content_tag('h3', title) + "\n" +
294 content_tag('h3', title) + "\n" +
293 content_tag('ul',
295 content_tag('ul',
294 queries.collect {|query|
296 queries.collect {|query|
295 css = 'query'
297 css = 'query'
296 css << ' selected' if query == @query
298 css << ' selected' if query == @query
297 content_tag('li', link_to(query.name, url_params.merge(:query_id => query), :class => css))
299 content_tag('li', link_to(query.name, url_params.merge(:query_id => query), :class => css))
298 }.join("\n").html_safe,
300 }.join("\n").html_safe,
299 :class => 'queries'
301 :class => 'queries'
300 ) + "\n"
302 ) + "\n"
301 end
303 end
302
304
303 # Renders the list of queries for the sidebar
305 # Renders the list of queries for the sidebar
304 def render_sidebar_queries(klass, project)
306 def render_sidebar_queries(klass, project)
305 queries = sidebar_queries(klass, project)
307 queries = sidebar_queries(klass, project)
306
308
307 out = ''.html_safe
309 out = ''.html_safe
308 out << query_links(l(:label_my_queries), queries.select(&:is_private?))
310 out << query_links(l(:label_my_queries), queries.select(&:is_private?))
309 out << query_links(l(:label_query_plural), queries.reject(&:is_private?))
311 out << query_links(l(:label_query_plural), queries.reject(&:is_private?))
310 out
312 out
311 end
313 end
312 end
314 end
@@ -1,89 +1,85
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module RoutesHelper
20 module RoutesHelper
21
21
22 # Returns the path to project issues or to the cross-project
22 # Returns the path to project issues or to the cross-project
23 # issue list if project is nil
23 # issue list if project is nil
24 def _project_issues_path(project, *args)
24 def _project_issues_path(project, *args)
25 if project
25 if project
26 project_issues_path(project, *args)
26 project_issues_path(project, *args)
27 else
27 else
28 issues_path(*args)
28 issues_path(*args)
29 end
29 end
30 end
30 end
31
31
32 def _project_news_path(project, *args)
32 def _project_news_path(project, *args)
33 if project
33 if project
34 project_news_index_path(project, *args)
34 project_news_index_path(project, *args)
35 else
35 else
36 news_index_path(*args)
36 news_index_path(*args)
37 end
37 end
38 end
38 end
39
39
40 def _new_project_issue_path(project, *args)
40 def _new_project_issue_path(project, *args)
41 if project
41 if project
42 new_project_issue_path(project, *args)
42 new_project_issue_path(project, *args)
43 else
43 else
44 new_issue_path(*args)
44 new_issue_path(*args)
45 end
45 end
46 end
46 end
47
47
48 def _project_calendar_path(project, *args)
48 def _project_calendar_path(project, *args)
49 project ? project_calendar_path(project, *args) : issues_calendar_path(*args)
49 project ? project_calendar_path(project, *args) : issues_calendar_path(*args)
50 end
50 end
51
51
52 def _project_gantt_path(project, *args)
52 def _project_gantt_path(project, *args)
53 project ? project_gantt_path(project, *args) : issues_gantt_path(*args)
53 project ? project_gantt_path(project, *args) : issues_gantt_path(*args)
54 end
54 end
55
55
56 def _time_entries_path(project, issue, *args)
56 def _time_entries_path(project, issue, *args)
57 if issue
57 if project
58 issue_time_entries_path(issue, *args)
59 elsif project
60 project_time_entries_path(project, *args)
58 project_time_entries_path(project, *args)
61 else
59 else
62 time_entries_path(*args)
60 time_entries_path(*args)
63 end
61 end
64 end
62 end
65
63
66 def _report_time_entries_path(project, issue, *args)
64 def _report_time_entries_path(project, issue, *args)
67 if issue
65 if project
68 report_issue_time_entries_path(issue, *args)
69 elsif project
70 report_project_time_entries_path(project, *args)
66 report_project_time_entries_path(project, *args)
71 else
67 else
72 report_time_entries_path(*args)
68 report_time_entries_path(*args)
73 end
69 end
74 end
70 end
75
71
76 def _new_time_entry_path(project, issue, *args)
72 def _new_time_entry_path(project, issue, *args)
77 if issue
73 if issue
78 new_issue_time_entry_path(issue, *args)
74 new_issue_time_entry_path(issue, *args)
79 elsif project
75 elsif project
80 new_project_time_entry_path(project, *args)
76 new_project_time_entry_path(project, *args)
81 else
77 else
82 new_time_entry_path(*args)
78 new_time_entry_path(*args)
83 end
79 end
84 end
80 end
85
81
86 def board_path(board, *args)
82 def board_path(board, *args)
87 project_board_path(board.project, board, *args)
83 project_board_path(board.project, board, *args)
88 end
84 end
89 end
85 end
@@ -1,135 +1,121
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module TimelogHelper
20 module TimelogHelper
21 include ApplicationHelper
21 include ApplicationHelper
22
22
23 def render_timelog_breadcrumb
24 links = []
25 links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
26 links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
27 if @issue
28 if @issue.visible?
29 links << link_to_issue(@issue, :subject => false)
30 else
31 links << "##{@issue.id}"
32 end
33 end
34 breadcrumb links
35 end
36
37 # Returns a collection of activities for a select field. time_entry
23 # Returns a collection of activities for a select field. time_entry
38 # is optional and will be used to check if the selected TimeEntryActivity
24 # is optional and will be used to check if the selected TimeEntryActivity
39 # is active.
25 # is active.
40 def activity_collection_for_select_options(time_entry=nil, project=nil)
26 def activity_collection_for_select_options(time_entry=nil, project=nil)
41 project ||= time_entry.try(:project)
27 project ||= time_entry.try(:project)
42 project ||= @project
28 project ||= @project
43 if project.nil?
29 if project.nil?
44 activities = TimeEntryActivity.shared.active
30 activities = TimeEntryActivity.shared.active
45 else
31 else
46 activities = project.activities
32 activities = project.activities
47 end
33 end
48
34
49 collection = []
35 collection = []
50 if time_entry && time_entry.activity && !time_entry.activity.active?
36 if time_entry && time_entry.activity && !time_entry.activity.active?
51 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ]
37 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ]
52 else
38 else
53 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
39 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
54 end
40 end
55 activities.each { |a| collection << [a.name, a.id] }
41 activities.each { |a| collection << [a.name, a.id] }
56 collection
42 collection
57 end
43 end
58
44
59 def select_hours(data, criteria, value)
45 def select_hours(data, criteria, value)
60 if value.to_s.empty?
46 if value.to_s.empty?
61 data.select {|row| row[criteria].blank? }
47 data.select {|row| row[criteria].blank? }
62 else
48 else
63 data.select {|row| row[criteria].to_s == value.to_s}
49 data.select {|row| row[criteria].to_s == value.to_s}
64 end
50 end
65 end
51 end
66
52
67 def sum_hours(data)
53 def sum_hours(data)
68 sum = 0
54 sum = 0
69 data.each do |row|
55 data.each do |row|
70 sum += row['hours'].to_f
56 sum += row['hours'].to_f
71 end
57 end
72 sum
58 sum
73 end
59 end
74
60
75 def format_criteria_value(criteria_options, value)
61 def format_criteria_value(criteria_options, value)
76 if value.blank?
62 if value.blank?
77 "[#{l(:label_none)}]"
63 "[#{l(:label_none)}]"
78 elsif k = criteria_options[:klass]
64 elsif k = criteria_options[:klass]
79 obj = k.find_by_id(value.to_i)
65 obj = k.find_by_id(value.to_i)
80 if obj.is_a?(Issue)
66 if obj.is_a?(Issue)
81 obj.visible? ? "#{obj.tracker} ##{obj.id}: #{obj.subject}" : "##{obj.id}"
67 obj.visible? ? "#{obj.tracker} ##{obj.id}: #{obj.subject}" : "##{obj.id}"
82 else
68 else
83 obj
69 obj
84 end
70 end
85 elsif cf = criteria_options[:custom_field]
71 elsif cf = criteria_options[:custom_field]
86 format_value(value, cf)
72 format_value(value, cf)
87 else
73 else
88 value.to_s
74 value.to_s
89 end
75 end
90 end
76 end
91
77
92 def report_to_csv(report)
78 def report_to_csv(report)
93 Redmine::Export::CSV.generate do |csv|
79 Redmine::Export::CSV.generate do |csv|
94 # Column headers
80 # Column headers
95 headers = report.criteria.collect {|criteria| l(report.available_criteria[criteria][:label]) }
81 headers = report.criteria.collect {|criteria| l(report.available_criteria[criteria][:label]) }
96 headers += report.periods
82 headers += report.periods
97 headers << l(:label_total_time)
83 headers << l(:label_total_time)
98 csv << headers
84 csv << headers
99 # Content
85 # Content
100 report_criteria_to_csv(csv, report.available_criteria, report.columns, report.criteria, report.periods, report.hours)
86 report_criteria_to_csv(csv, report.available_criteria, report.columns, report.criteria, report.periods, report.hours)
101 # Total row
87 # Total row
102 str_total = l(:label_total_time)
88 str_total = l(:label_total_time)
103 row = [ str_total ] + [''] * (report.criteria.size - 1)
89 row = [ str_total ] + [''] * (report.criteria.size - 1)
104 total = 0
90 total = 0
105 report.periods.each do |period|
91 report.periods.each do |period|
106 sum = sum_hours(select_hours(report.hours, report.columns, period.to_s))
92 sum = sum_hours(select_hours(report.hours, report.columns, period.to_s))
107 total += sum
93 total += sum
108 row << (sum > 0 ? sum : '')
94 row << (sum > 0 ? sum : '')
109 end
95 end
110 row << total
96 row << total
111 csv << row
97 csv << row
112 end
98 end
113 end
99 end
114
100
115 def report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours, level=0)
101 def report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours, level=0)
116 hours.collect {|h| h[criteria[level]].to_s}.uniq.each do |value|
102 hours.collect {|h| h[criteria[level]].to_s}.uniq.each do |value|
117 hours_for_value = select_hours(hours, criteria[level], value)
103 hours_for_value = select_hours(hours, criteria[level], value)
118 next if hours_for_value.empty?
104 next if hours_for_value.empty?
119 row = [''] * level
105 row = [''] * level
120 row << format_criteria_value(available_criteria[criteria[level]], value).to_s
106 row << format_criteria_value(available_criteria[criteria[level]], value).to_s
121 row += [''] * (criteria.length - level - 1)
107 row += [''] * (criteria.length - level - 1)
122 total = 0
108 total = 0
123 periods.each do |period|
109 periods.each do |period|
124 sum = sum_hours(select_hours(hours_for_value, columns, period.to_s))
110 sum = sum_hours(select_hours(hours_for_value, columns, period.to_s))
125 total += sum
111 total += sum
126 row << (sum > 0 ? sum : '')
112 row << (sum > 0 ? sum : '')
127 end
113 end
128 row << total
114 row << total
129 csv << row
115 csv << row
130 if criteria.length > level + 1
116 if criteria.length > level + 1
131 report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours_for_value, level + 1)
117 report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours_for_value, level + 1)
132 end
118 end
133 end
119 end
134 end
120 end
135 end
121 end
@@ -1,1111 +1,1111
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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, :groupable, :totalable, :default_order
19 attr_accessor :name, :sortable, :groupable, :totalable, :default_order
20 include Redmine::I18n
20 include Redmine::I18n
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.groupable = options[:groupable] || false
25 self.groupable = options[:groupable] || false
26 if groupable == true
26 if groupable == true
27 self.groupable = name.to_s
27 self.groupable = name.to_s
28 end
28 end
29 self.totalable = options[:totalable] || false
29 self.totalable = options[:totalable] || false
30 self.default_order = options[:default_order]
30 self.default_order = options[:default_order]
31 @inline = options.key?(:inline) ? options[:inline] : true
31 @inline = options.key?(:inline) ? options[:inline] : true
32 @caption_key = options[:caption] || "field_#{name}".to_sym
32 @caption_key = options[:caption] || "field_#{name}".to_sym
33 @frozen = options[:frozen]
33 @frozen = options[:frozen]
34 end
34 end
35
35
36 def caption
36 def caption
37 case @caption_key
37 case @caption_key
38 when Symbol
38 when Symbol
39 l(@caption_key)
39 l(@caption_key)
40 when Proc
40 when Proc
41 @caption_key.call
41 @caption_key.call
42 else
42 else
43 @caption_key
43 @caption_key
44 end
44 end
45 end
45 end
46
46
47 # Returns true if the column is sortable, otherwise false
47 # Returns true if the column is sortable, otherwise false
48 def sortable?
48 def sortable?
49 !@sortable.nil?
49 !@sortable.nil?
50 end
50 end
51
51
52 def sortable
52 def sortable
53 @sortable.is_a?(Proc) ? @sortable.call : @sortable
53 @sortable.is_a?(Proc) ? @sortable.call : @sortable
54 end
54 end
55
55
56 def inline?
56 def inline?
57 @inline
57 @inline
58 end
58 end
59
59
60 def frozen?
60 def frozen?
61 @frozen
61 @frozen
62 end
62 end
63
63
64 def value(object)
64 def value(object)
65 object.send name
65 object.send name
66 end
66 end
67
67
68 def value_object(object)
68 def value_object(object)
69 object.send name
69 object.send name
70 end
70 end
71
71
72 def css_classes
72 def css_classes
73 name
73 name
74 end
74 end
75 end
75 end
76
76
77 class QueryCustomFieldColumn < QueryColumn
77 class QueryCustomFieldColumn < QueryColumn
78
78
79 def initialize(custom_field)
79 def initialize(custom_field)
80 self.name = "cf_#{custom_field.id}".to_sym
80 self.name = "cf_#{custom_field.id}".to_sym
81 self.sortable = custom_field.order_statement || false
81 self.sortable = custom_field.order_statement || false
82 self.groupable = custom_field.group_statement || false
82 self.groupable = custom_field.group_statement || false
83 self.totalable = custom_field.totalable?
83 self.totalable = custom_field.totalable?
84 @inline = true
84 @inline = true
85 @cf = custom_field
85 @cf = custom_field
86 end
86 end
87
87
88 def caption
88 def caption
89 @cf.name
89 @cf.name
90 end
90 end
91
91
92 def custom_field
92 def custom_field
93 @cf
93 @cf
94 end
94 end
95
95
96 def value_object(object)
96 def value_object(object)
97 if custom_field.visible_by?(object.project, User.current)
97 if custom_field.visible_by?(object.project, User.current)
98 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
98 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
99 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
99 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
100 else
100 else
101 nil
101 nil
102 end
102 end
103 end
103 end
104
104
105 def value(object)
105 def value(object)
106 raw = value_object(object)
106 raw = value_object(object)
107 if raw.is_a?(Array)
107 if raw.is_a?(Array)
108 raw.map {|r| @cf.cast_value(r.value)}
108 raw.map {|r| @cf.cast_value(r.value)}
109 elsif raw
109 elsif raw
110 @cf.cast_value(raw.value)
110 @cf.cast_value(raw.value)
111 else
111 else
112 nil
112 nil
113 end
113 end
114 end
114 end
115
115
116 def css_classes
116 def css_classes
117 @css_classes ||= "#{name} #{@cf.field_format}"
117 @css_classes ||= "#{name} #{@cf.field_format}"
118 end
118 end
119 end
119 end
120
120
121 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
121 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
122
122
123 def initialize(association, custom_field)
123 def initialize(association, custom_field)
124 super(custom_field)
124 super(custom_field)
125 self.name = "#{association}.cf_#{custom_field.id}".to_sym
125 self.name = "#{association}.cf_#{custom_field.id}".to_sym
126 # TODO: support sorting/grouping by association custom field
126 # TODO: support sorting/grouping by association custom field
127 self.sortable = false
127 self.sortable = false
128 self.groupable = false
128 self.groupable = false
129 @association = association
129 @association = association
130 end
130 end
131
131
132 def value_object(object)
132 def value_object(object)
133 if assoc = object.send(@association)
133 if assoc = object.send(@association)
134 super(assoc)
134 super(assoc)
135 end
135 end
136 end
136 end
137
137
138 def css_classes
138 def css_classes
139 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
139 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
140 end
140 end
141 end
141 end
142
142
143 class Query < ActiveRecord::Base
143 class Query < ActiveRecord::Base
144 class StatementInvalid < ::ActiveRecord::StatementInvalid
144 class StatementInvalid < ::ActiveRecord::StatementInvalid
145 end
145 end
146
146
147 include Redmine::SubclassFactory
147 include Redmine::SubclassFactory
148
148
149 VISIBILITY_PRIVATE = 0
149 VISIBILITY_PRIVATE = 0
150 VISIBILITY_ROLES = 1
150 VISIBILITY_ROLES = 1
151 VISIBILITY_PUBLIC = 2
151 VISIBILITY_PUBLIC = 2
152
152
153 belongs_to :project
153 belongs_to :project
154 belongs_to :user
154 belongs_to :user
155 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
155 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
156 serialize :filters
156 serialize :filters
157 serialize :column_names
157 serialize :column_names
158 serialize :sort_criteria, Array
158 serialize :sort_criteria, Array
159 serialize :options, Hash
159 serialize :options, Hash
160
160
161 attr_protected :project_id, :user_id
161 attr_protected :project_id, :user_id
162
162
163 validates_presence_of :name
163 validates_presence_of :name
164 validates_length_of :name, :maximum => 255
164 validates_length_of :name, :maximum => 255
165 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
165 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
166 validate :validate_query_filters
166 validate :validate_query_filters
167 validate do |query|
167 validate do |query|
168 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
168 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
169 end
169 end
170
170
171 after_save do |query|
171 after_save do |query|
172 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
172 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
173 query.roles.clear
173 query.roles.clear
174 end
174 end
175 end
175 end
176
176
177 class_attribute :operators
177 class_attribute :operators
178 self.operators = {
178 self.operators = {
179 "=" => :label_equals,
179 "=" => :label_equals,
180 "!" => :label_not_equals,
180 "!" => :label_not_equals,
181 "o" => :label_open_issues,
181 "o" => :label_open_issues,
182 "c" => :label_closed_issues,
182 "c" => :label_closed_issues,
183 "!*" => :label_none,
183 "!*" => :label_none,
184 "*" => :label_any,
184 "*" => :label_any,
185 ">=" => :label_greater_or_equal,
185 ">=" => :label_greater_or_equal,
186 "<=" => :label_less_or_equal,
186 "<=" => :label_less_or_equal,
187 "><" => :label_between,
187 "><" => :label_between,
188 "<t+" => :label_in_less_than,
188 "<t+" => :label_in_less_than,
189 ">t+" => :label_in_more_than,
189 ">t+" => :label_in_more_than,
190 "><t+"=> :label_in_the_next_days,
190 "><t+"=> :label_in_the_next_days,
191 "t+" => :label_in,
191 "t+" => :label_in,
192 "t" => :label_today,
192 "t" => :label_today,
193 "ld" => :label_yesterday,
193 "ld" => :label_yesterday,
194 "w" => :label_this_week,
194 "w" => :label_this_week,
195 "lw" => :label_last_week,
195 "lw" => :label_last_week,
196 "l2w" => [:label_last_n_weeks, {:count => 2}],
196 "l2w" => [:label_last_n_weeks, {:count => 2}],
197 "m" => :label_this_month,
197 "m" => :label_this_month,
198 "lm" => :label_last_month,
198 "lm" => :label_last_month,
199 "y" => :label_this_year,
199 "y" => :label_this_year,
200 ">t-" => :label_less_than_ago,
200 ">t-" => :label_less_than_ago,
201 "<t-" => :label_more_than_ago,
201 "<t-" => :label_more_than_ago,
202 "><t-"=> :label_in_the_past_days,
202 "><t-"=> :label_in_the_past_days,
203 "t-" => :label_ago,
203 "t-" => :label_ago,
204 "~" => :label_contains,
204 "~" => :label_contains,
205 "!~" => :label_not_contains,
205 "!~" => :label_not_contains,
206 "=p" => :label_any_issues_in_project,
206 "=p" => :label_any_issues_in_project,
207 "=!p" => :label_any_issues_not_in_project,
207 "=!p" => :label_any_issues_not_in_project,
208 "!p" => :label_no_issues_in_project,
208 "!p" => :label_no_issues_in_project,
209 "*o" => :label_any_open_issues,
209 "*o" => :label_any_open_issues,
210 "!o" => :label_no_open_issues
210 "!o" => :label_no_open_issues
211 }
211 }
212
212
213 class_attribute :operators_by_filter_type
213 class_attribute :operators_by_filter_type
214 self.operators_by_filter_type = {
214 self.operators_by_filter_type = {
215 :list => [ "=", "!" ],
215 :list => [ "=", "!" ],
216 :list_status => [ "o", "=", "!", "c", "*" ],
216 :list_status => [ "o", "=", "!", "c", "*" ],
217 :list_optional => [ "=", "!", "!*", "*" ],
217 :list_optional => [ "=", "!", "!*", "*" ],
218 :list_subprojects => [ "*", "!*", "=" ],
218 :list_subprojects => [ "*", "!*", "=" ],
219 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
219 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
220 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
220 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
221 :string => [ "=", "~", "!", "!~", "!*", "*" ],
221 :string => [ "=", "~", "!", "!~", "!*", "*" ],
222 :text => [ "~", "!~", "!*", "*" ],
222 :text => [ "~", "!~", "!*", "*" ],
223 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
223 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
224 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
224 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
225 :relation => ["=", "=p", "=!p", "!p", "*o", "!o", "!*", "*"],
225 :relation => ["=", "=p", "=!p", "!p", "*o", "!o", "!*", "*"],
226 :tree => ["=", "~", "!*", "*"]
226 :tree => ["=", "~", "!*", "*"]
227 }
227 }
228
228
229 class_attribute :available_columns
229 class_attribute :available_columns
230 self.available_columns = []
230 self.available_columns = []
231
231
232 class_attribute :queried_class
232 class_attribute :queried_class
233
233
234 # Permission required to view the queries, set on subclasses.
234 # Permission required to view the queries, set on subclasses.
235 class_attribute :view_permission
235 class_attribute :view_permission
236
236
237 # Scope of queries that are global or on the given project
237 # Scope of queries that are global or on the given project
238 scope :global_or_on_project, lambda {|project|
238 scope :global_or_on_project, lambda {|project|
239 where(:project_id => (project.nil? ? nil : [nil, project.id]))
239 where(:project_id => (project.nil? ? nil : [nil, project.id]))
240 }
240 }
241
241
242 scope :sorted, lambda {order(:name, :id)}
242 scope :sorted, lambda {order(:name, :id)}
243
243
244 # Scope of visible queries, can be used from subclasses only.
244 # Scope of visible queries, can be used from subclasses only.
245 # Unlike other visible scopes, a class methods is used as it
245 # Unlike other visible scopes, a class methods is used as it
246 # let handle inheritance more nicely than scope DSL.
246 # let handle inheritance more nicely than scope DSL.
247 def self.visible(*args)
247 def self.visible(*args)
248 if self == ::Query
248 if self == ::Query
249 # Visibility depends on permissions for each subclass,
249 # Visibility depends on permissions for each subclass,
250 # raise an error if the scope is called from Query (eg. Query.visible)
250 # raise an error if the scope is called from Query (eg. Query.visible)
251 raise Exception.new("Cannot call .visible scope from the base Query class, but from subclasses only.")
251 raise Exception.new("Cannot call .visible scope from the base Query class, but from subclasses only.")
252 end
252 end
253
253
254 user = args.shift || User.current
254 user = args.shift || User.current
255 base = Project.allowed_to_condition(user, view_permission, *args)
255 base = Project.allowed_to_condition(user, view_permission, *args)
256 scope = joins("LEFT OUTER JOIN #{Project.table_name} ON #{table_name}.project_id = #{Project.table_name}.id").
256 scope = joins("LEFT OUTER JOIN #{Project.table_name} ON #{table_name}.project_id = #{Project.table_name}.id").
257 where("#{table_name}.project_id IS NULL OR (#{base})")
257 where("#{table_name}.project_id IS NULL OR (#{base})")
258
258
259 if user.admin?
259 if user.admin?
260 scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", VISIBILITY_PRIVATE, user.id)
260 scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", VISIBILITY_PRIVATE, user.id)
261 elsif user.memberships.any?
261 elsif user.memberships.any?
262 scope.where("#{table_name}.visibility = ?" +
262 scope.where("#{table_name}.visibility = ?" +
263 " OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" +
263 " OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" +
264 "SELECT DISTINCT q.id FROM #{table_name} q" +
264 "SELECT DISTINCT q.id FROM #{table_name} q" +
265 " INNER JOIN #{table_name_prefix}queries_roles#{table_name_suffix} qr on qr.query_id = q.id" +
265 " INNER JOIN #{table_name_prefix}queries_roles#{table_name_suffix} qr on qr.query_id = q.id" +
266 " INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = qr.role_id" +
266 " INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = qr.role_id" +
267 " INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" +
267 " INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" +
268 " WHERE q.project_id IS NULL OR q.project_id = m.project_id))" +
268 " WHERE q.project_id IS NULL OR q.project_id = m.project_id))" +
269 " OR #{table_name}.user_id = ?",
269 " OR #{table_name}.user_id = ?",
270 VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, user.id)
270 VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, user.id)
271 elsif user.logged?
271 elsif user.logged?
272 scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", VISIBILITY_PUBLIC, user.id)
272 scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", VISIBILITY_PUBLIC, user.id)
273 else
273 else
274 scope.where("#{table_name}.visibility = ?", VISIBILITY_PUBLIC)
274 scope.where("#{table_name}.visibility = ?", VISIBILITY_PUBLIC)
275 end
275 end
276 end
276 end
277
277
278 # Returns true if the query is visible to +user+ or the current user.
278 # Returns true if the query is visible to +user+ or the current user.
279 def visible?(user=User.current)
279 def visible?(user=User.current)
280 return true if user.admin?
280 return true if user.admin?
281 return false unless project.nil? || user.allowed_to?(self.class.view_permission, project)
281 return false unless project.nil? || user.allowed_to?(self.class.view_permission, project)
282 case visibility
282 case visibility
283 when VISIBILITY_PUBLIC
283 when VISIBILITY_PUBLIC
284 true
284 true
285 when VISIBILITY_ROLES
285 when VISIBILITY_ROLES
286 if project
286 if project
287 (user.roles_for_project(project) & roles).any?
287 (user.roles_for_project(project) & roles).any?
288 else
288 else
289 Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any?
289 Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any?
290 end
290 end
291 else
291 else
292 user == self.user
292 user == self.user
293 end
293 end
294 end
294 end
295
295
296 def is_private?
296 def is_private?
297 visibility == VISIBILITY_PRIVATE
297 visibility == VISIBILITY_PRIVATE
298 end
298 end
299
299
300 def is_public?
300 def is_public?
301 !is_private?
301 !is_private?
302 end
302 end
303
303
304 def queried_table_name
304 def queried_table_name
305 @queried_table_name ||= self.class.queried_class.table_name
305 @queried_table_name ||= self.class.queried_class.table_name
306 end
306 end
307
307
308 def initialize(attributes=nil, *args)
308 def initialize(attributes=nil, *args)
309 super attributes
309 super attributes
310 @is_for_all = project.nil?
310 @is_for_all = project.nil?
311 end
311 end
312
312
313 # Builds the query from the given params
313 # Builds the query from the given params
314 def build_from_params(params)
314 def build_from_params(params)
315 if params[:fields] || params[:f]
315 if params[:fields] || params[:f]
316 self.filters = {}
316 self.filters = {}
317 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
317 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
318 else
318 else
319 available_filters.keys.each do |field|
319 available_filters.keys.each do |field|
320 add_short_filter(field, params[field]) if params[field]
320 add_short_filter(field, params[field]) if params[field]
321 end
321 end
322 end
322 end
323 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
323 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
324 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
324 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
325 self.totalable_names = params[:t] || (params[:query] && params[:query][:totalable_names])
325 self.totalable_names = params[:t] || (params[:query] && params[:query][:totalable_names])
326 self
326 self
327 end
327 end
328
328
329 # Builds a new query from the given params and attributes
329 # Builds a new query from the given params and attributes
330 def self.build_from_params(params, attributes={})
330 def self.build_from_params(params, attributes={})
331 new(attributes).build_from_params(params)
331 new(attributes).build_from_params(params)
332 end
332 end
333
333
334 def validate_query_filters
334 def validate_query_filters
335 filters.each_key do |field|
335 filters.each_key do |field|
336 if values_for(field)
336 if values_for(field)
337 case type_for(field)
337 case type_for(field)
338 when :integer
338 when :integer
339 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(,[+-]?\d+)*\z/) }
339 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(,[+-]?\d+)*\z/) }
340 when :float
340 when :float
341 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(\.\d*)?\z/) }
341 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(\.\d*)?\z/) }
342 when :date, :date_past
342 when :date, :date_past
343 case operator_for(field)
343 case operator_for(field)
344 when "=", ">=", "<=", "><"
344 when "=", ">=", "<=", "><"
345 add_filter_error(field, :invalid) if values_for(field).detect {|v|
345 add_filter_error(field, :invalid) if values_for(field).detect {|v|
346 v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?)
346 v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?)
347 }
347 }
348 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
348 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
349 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
349 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
350 end
350 end
351 end
351 end
352 end
352 end
353
353
354 add_filter_error(field, :blank) unless
354 add_filter_error(field, :blank) unless
355 # filter requires one or more values
355 # filter requires one or more values
356 (values_for(field) and !values_for(field).first.blank?) or
356 (values_for(field) and !values_for(field).first.blank?) or
357 # filter doesn't require any value
357 # filter doesn't require any value
358 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "*o", "!o"].include? operator_for(field)
358 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "*o", "!o"].include? operator_for(field)
359 end if filters
359 end if filters
360 end
360 end
361
361
362 def add_filter_error(field, message)
362 def add_filter_error(field, message)
363 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
363 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
364 errors.add(:base, m)
364 errors.add(:base, m)
365 end
365 end
366
366
367 def editable_by?(user)
367 def editable_by?(user)
368 return false unless user
368 return false unless user
369 # Admin can edit them all and regular users can edit their private queries
369 # Admin can edit them all and regular users can edit their private queries
370 return true if user.admin? || (is_private? && self.user_id == user.id)
370 return true if user.admin? || (is_private? && self.user_id == user.id)
371 # Members can not edit public queries that are for all project (only admin is allowed to)
371 # Members can not edit public queries that are for all project (only admin is allowed to)
372 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
372 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
373 end
373 end
374
374
375 def trackers
375 def trackers
376 @trackers ||= (project.nil? ? Tracker.all : project.rolled_up_trackers).visible.sorted
376 @trackers ||= (project.nil? ? Tracker.all : project.rolled_up_trackers).visible.sorted
377 end
377 end
378
378
379 # Returns a hash of localized labels for all filter operators
379 # Returns a hash of localized labels for all filter operators
380 def self.operators_labels
380 def self.operators_labels
381 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
381 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
382 end
382 end
383
383
384 # Returns a representation of the available filters for JSON serialization
384 # Returns a representation of the available filters for JSON serialization
385 def available_filters_as_json
385 def available_filters_as_json
386 json = {}
386 json = {}
387 available_filters.each do |field, options|
387 available_filters.each do |field, options|
388 options = options.slice(:type, :name, :values)
388 options = options.slice(:type, :name, :values)
389 if options[:values] && values_for(field)
389 if options[:values] && values_for(field)
390 missing = Array(values_for(field)).select(&:present?) - options[:values].map(&:last)
390 missing = Array(values_for(field)).select(&:present?) - options[:values].map(&:last)
391 if missing.any? && respond_to?(method = "find_#{field}_filter_values")
391 if missing.any? && respond_to?(method = "find_#{field}_filter_values")
392 options[:values] += send(method, missing)
392 options[:values] += send(method, missing)
393 end
393 end
394 end
394 end
395 json[field] = options.stringify_keys
395 json[field] = options.stringify_keys
396 end
396 end
397 json
397 json
398 end
398 end
399
399
400 def all_projects
400 def all_projects
401 @all_projects ||= Project.visible.to_a
401 @all_projects ||= Project.visible.to_a
402 end
402 end
403
403
404 def all_projects_values
404 def all_projects_values
405 return @all_projects_values if @all_projects_values
405 return @all_projects_values if @all_projects_values
406
406
407 values = []
407 values = []
408 Project.project_tree(all_projects) do |p, level|
408 Project.project_tree(all_projects) do |p, level|
409 prefix = (level > 0 ? ('--' * level + ' ') : '')
409 prefix = (level > 0 ? ('--' * level + ' ') : '')
410 values << ["#{prefix}#{p.name}", p.id.to_s]
410 values << ["#{prefix}#{p.name}", p.id.to_s]
411 end
411 end
412 @all_projects_values = values
412 @all_projects_values = values
413 end
413 end
414
414
415 # Adds available filters
415 # Adds available filters
416 def initialize_available_filters
416 def initialize_available_filters
417 # implemented by sub-classes
417 # implemented by sub-classes
418 end
418 end
419 protected :initialize_available_filters
419 protected :initialize_available_filters
420
420
421 # Adds an available filter
421 # Adds an available filter
422 def add_available_filter(field, options)
422 def add_available_filter(field, options)
423 @available_filters ||= ActiveSupport::OrderedHash.new
423 @available_filters ||= ActiveSupport::OrderedHash.new
424 @available_filters[field] = options
424 @available_filters[field] = options
425 @available_filters
425 @available_filters
426 end
426 end
427
427
428 # Removes an available filter
428 # Removes an available filter
429 def delete_available_filter(field)
429 def delete_available_filter(field)
430 if @available_filters
430 if @available_filters
431 @available_filters.delete(field)
431 @available_filters.delete(field)
432 end
432 end
433 end
433 end
434
434
435 # Return a hash of available filters
435 # Return a hash of available filters
436 def available_filters
436 def available_filters
437 unless @available_filters
437 unless @available_filters
438 initialize_available_filters
438 initialize_available_filters
439 @available_filters.each do |field, options|
439 @available_filters.each do |field, options|
440 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
440 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
441 end
441 end
442 end
442 end
443 @available_filters
443 @available_filters
444 end
444 end
445
445
446 def add_filter(field, operator, values=nil)
446 def add_filter(field, operator, values=nil)
447 # values must be an array
447 # values must be an array
448 return unless values.nil? || values.is_a?(Array)
448 return unless values.nil? || values.is_a?(Array)
449 # check if field is defined as an available filter
449 # check if field is defined as an available filter
450 if available_filters.has_key? field
450 if available_filters.has_key? field
451 filter_options = available_filters[field]
451 filter_options = available_filters[field]
452 filters[field] = {:operator => operator, :values => (values || [''])}
452 filters[field] = {:operator => operator, :values => (values || [''])}
453 end
453 end
454 end
454 end
455
455
456 def add_short_filter(field, expression)
456 def add_short_filter(field, expression)
457 return unless expression && available_filters.has_key?(field)
457 return unless expression && available_filters.has_key?(field)
458 field_type = available_filters[field][:type]
458 field_type = available_filters[field][:type]
459 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
459 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
460 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
460 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
461 values = $1
461 values = $1
462 add_filter field, operator, values.present? ? values.split('|') : ['']
462 add_filter field, operator, values.present? ? values.split('|') : ['']
463 end || add_filter(field, '=', expression.split('|'))
463 end || add_filter(field, '=', expression.to_s.split('|'))
464 end
464 end
465
465
466 # Add multiple filters using +add_filter+
466 # Add multiple filters using +add_filter+
467 def add_filters(fields, operators, values)
467 def add_filters(fields, operators, values)
468 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
468 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
469 fields.each do |field|
469 fields.each do |field|
470 add_filter(field, operators[field], values && values[field])
470 add_filter(field, operators[field], values && values[field])
471 end
471 end
472 end
472 end
473 end
473 end
474
474
475 def has_filter?(field)
475 def has_filter?(field)
476 filters and filters[field]
476 filters and filters[field]
477 end
477 end
478
478
479 def type_for(field)
479 def type_for(field)
480 available_filters[field][:type] if available_filters.has_key?(field)
480 available_filters[field][:type] if available_filters.has_key?(field)
481 end
481 end
482
482
483 def operator_for(field)
483 def operator_for(field)
484 has_filter?(field) ? filters[field][:operator] : nil
484 has_filter?(field) ? filters[field][:operator] : nil
485 end
485 end
486
486
487 def values_for(field)
487 def values_for(field)
488 has_filter?(field) ? filters[field][:values] : nil
488 has_filter?(field) ? filters[field][:values] : nil
489 end
489 end
490
490
491 def value_for(field, index=0)
491 def value_for(field, index=0)
492 (values_for(field) || [])[index]
492 (values_for(field) || [])[index]
493 end
493 end
494
494
495 def label_for(field)
495 def label_for(field)
496 label = available_filters[field][:name] if available_filters.has_key?(field)
496 label = available_filters[field][:name] if available_filters.has_key?(field)
497 label ||= queried_class.human_attribute_name(field, :default => field)
497 label ||= queried_class.human_attribute_name(field, :default => field)
498 end
498 end
499
499
500 def self.add_available_column(column)
500 def self.add_available_column(column)
501 self.available_columns << (column) if column.is_a?(QueryColumn)
501 self.available_columns << (column) if column.is_a?(QueryColumn)
502 end
502 end
503
503
504 # Returns an array of columns that can be used to group the results
504 # Returns an array of columns that can be used to group the results
505 def groupable_columns
505 def groupable_columns
506 available_columns.select {|c| c.groupable}
506 available_columns.select {|c| c.groupable}
507 end
507 end
508
508
509 # Returns a Hash of columns and the key for sorting
509 # Returns a Hash of columns and the key for sorting
510 def sortable_columns
510 def sortable_columns
511 available_columns.inject({}) {|h, column|
511 available_columns.inject({}) {|h, column|
512 h[column.name.to_s] = column.sortable
512 h[column.name.to_s] = column.sortable
513 h
513 h
514 }
514 }
515 end
515 end
516
516
517 def columns
517 def columns
518 # preserve the column_names order
518 # preserve the column_names order
519 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
519 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
520 available_columns.find { |col| col.name == name }
520 available_columns.find { |col| col.name == name }
521 end.compact
521 end.compact
522 available_columns.select(&:frozen?) | cols
522 available_columns.select(&:frozen?) | cols
523 end
523 end
524
524
525 def inline_columns
525 def inline_columns
526 columns.select(&:inline?)
526 columns.select(&:inline?)
527 end
527 end
528
528
529 def block_columns
529 def block_columns
530 columns.reject(&:inline?)
530 columns.reject(&:inline?)
531 end
531 end
532
532
533 def available_inline_columns
533 def available_inline_columns
534 available_columns.select(&:inline?)
534 available_columns.select(&:inline?)
535 end
535 end
536
536
537 def available_block_columns
537 def available_block_columns
538 available_columns.reject(&:inline?)
538 available_columns.reject(&:inline?)
539 end
539 end
540
540
541 def available_totalable_columns
541 def available_totalable_columns
542 available_columns.select(&:totalable)
542 available_columns.select(&:totalable)
543 end
543 end
544
544
545 def default_columns_names
545 def default_columns_names
546 []
546 []
547 end
547 end
548
548
549 def column_names=(names)
549 def column_names=(names)
550 if names
550 if names
551 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
551 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
552 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
552 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
553 # Set column_names to nil if default columns
553 # Set column_names to nil if default columns
554 if names == default_columns_names
554 if names == default_columns_names
555 names = nil
555 names = nil
556 end
556 end
557 end
557 end
558 write_attribute(:column_names, names)
558 write_attribute(:column_names, names)
559 end
559 end
560
560
561 def has_column?(column)
561 def has_column?(column)
562 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
562 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
563 end
563 end
564
564
565 def has_custom_field_column?
565 def has_custom_field_column?
566 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
566 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
567 end
567 end
568
568
569 def has_default_columns?
569 def has_default_columns?
570 column_names.nil? || column_names.empty?
570 column_names.nil? || column_names.empty?
571 end
571 end
572
572
573 def totalable_columns
573 def totalable_columns
574 names = totalable_names
574 names = totalable_names
575 available_totalable_columns.select {|column| names.include?(column.name)}
575 available_totalable_columns.select {|column| names.include?(column.name)}
576 end
576 end
577
577
578 def totalable_names=(names)
578 def totalable_names=(names)
579 if names
579 if names
580 names = names.select(&:present?).map {|n| n.is_a?(Symbol) ? n : n.to_sym}
580 names = names.select(&:present?).map {|n| n.is_a?(Symbol) ? n : n.to_sym}
581 end
581 end
582 options[:totalable_names] = names
582 options[:totalable_names] = names
583 end
583 end
584
584
585 def totalable_names
585 def totalable_names
586 options[:totalable_names] || Setting.issue_list_default_totals.map(&:to_sym) || []
586 options[:totalable_names] || Setting.issue_list_default_totals.map(&:to_sym) || []
587 end
587 end
588
588
589 def sort_criteria=(arg)
589 def sort_criteria=(arg)
590 c = []
590 c = []
591 if arg.is_a?(Hash)
591 if arg.is_a?(Hash)
592 arg = arg.keys.sort.collect {|k| arg[k]}
592 arg = arg.keys.sort.collect {|k| arg[k]}
593 end
593 end
594 if arg
594 if arg
595 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
595 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
596 end
596 end
597 write_attribute(:sort_criteria, c)
597 write_attribute(:sort_criteria, c)
598 end
598 end
599
599
600 def sort_criteria
600 def sort_criteria
601 read_attribute(:sort_criteria) || []
601 read_attribute(:sort_criteria) || []
602 end
602 end
603
603
604 def sort_criteria_key(arg)
604 def sort_criteria_key(arg)
605 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
605 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
606 end
606 end
607
607
608 def sort_criteria_order(arg)
608 def sort_criteria_order(arg)
609 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
609 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
610 end
610 end
611
611
612 def sort_criteria_order_for(key)
612 def sort_criteria_order_for(key)
613 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
613 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
614 end
614 end
615
615
616 # Returns the SQL sort order that should be prepended for grouping
616 # Returns the SQL sort order that should be prepended for grouping
617 def group_by_sort_order
617 def group_by_sort_order
618 if column = group_by_column
618 if column = group_by_column
619 order = (sort_criteria_order_for(column.name) || column.default_order).try(:upcase)
619 order = (sort_criteria_order_for(column.name) || column.default_order).try(:upcase)
620 Array(column.sortable).map {|s| "#{s} #{order}"}
620 Array(column.sortable).map {|s| "#{s} #{order}"}
621 end
621 end
622 end
622 end
623
623
624 # Returns true if the query is a grouped query
624 # Returns true if the query is a grouped query
625 def grouped?
625 def grouped?
626 !group_by_column.nil?
626 !group_by_column.nil?
627 end
627 end
628
628
629 def group_by_column
629 def group_by_column
630 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
630 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
631 end
631 end
632
632
633 def group_by_statement
633 def group_by_statement
634 group_by_column.try(:groupable)
634 group_by_column.try(:groupable)
635 end
635 end
636
636
637 def project_statement
637 def project_statement
638 project_clauses = []
638 project_clauses = []
639 if project && !project.descendants.active.empty?
639 if project && !project.descendants.active.empty?
640 if has_filter?("subproject_id")
640 if has_filter?("subproject_id")
641 case operator_for("subproject_id")
641 case operator_for("subproject_id")
642 when '='
642 when '='
643 # include the selected subprojects
643 # include the selected subprojects
644 ids = [project.id] + values_for("subproject_id").each(&:to_i)
644 ids = [project.id] + values_for("subproject_id").each(&:to_i)
645 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
645 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
646 when '!*'
646 when '!*'
647 # main project only
647 # main project only
648 project_clauses << "#{Project.table_name}.id = %d" % project.id
648 project_clauses << "#{Project.table_name}.id = %d" % project.id
649 else
649 else
650 # all subprojects
650 # all subprojects
651 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
651 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
652 end
652 end
653 elsif Setting.display_subprojects_issues?
653 elsif Setting.display_subprojects_issues?
654 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
654 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
655 else
655 else
656 project_clauses << "#{Project.table_name}.id = %d" % project.id
656 project_clauses << "#{Project.table_name}.id = %d" % project.id
657 end
657 end
658 elsif project
658 elsif project
659 project_clauses << "#{Project.table_name}.id = %d" % project.id
659 project_clauses << "#{Project.table_name}.id = %d" % project.id
660 end
660 end
661 project_clauses.any? ? project_clauses.join(' AND ') : nil
661 project_clauses.any? ? project_clauses.join(' AND ') : nil
662 end
662 end
663
663
664 def statement
664 def statement
665 # filters clauses
665 # filters clauses
666 filters_clauses = []
666 filters_clauses = []
667 filters.each_key do |field|
667 filters.each_key do |field|
668 next if field == "subproject_id"
668 next if field == "subproject_id"
669 v = values_for(field).clone
669 v = values_for(field).clone
670 next unless v and !v.empty?
670 next unless v and !v.empty?
671 operator = operator_for(field)
671 operator = operator_for(field)
672
672
673 # "me" value substitution
673 # "me" value substitution
674 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
674 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
675 if v.delete("me")
675 if v.delete("me")
676 if User.current.logged?
676 if User.current.logged?
677 v.push(User.current.id.to_s)
677 v.push(User.current.id.to_s)
678 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
678 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
679 else
679 else
680 v.push("0")
680 v.push("0")
681 end
681 end
682 end
682 end
683 end
683 end
684
684
685 if field == 'project_id'
685 if field == 'project_id'
686 if v.delete('mine')
686 if v.delete('mine')
687 v += User.current.memberships.map(&:project_id).map(&:to_s)
687 v += User.current.memberships.map(&:project_id).map(&:to_s)
688 end
688 end
689 end
689 end
690
690
691 if field =~ /cf_(\d+)$/
691 if field =~ /cf_(\d+)$/
692 # custom field
692 # custom field
693 filters_clauses << sql_for_custom_field(field, operator, v, $1)
693 filters_clauses << sql_for_custom_field(field, operator, v, $1)
694 elsif respond_to?("sql_for_#{field}_field")
694 elsif respond_to?("sql_for_#{field}_field")
695 # specific statement
695 # specific statement
696 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
696 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
697 else
697 else
698 # regular field
698 # regular field
699 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
699 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
700 end
700 end
701 end if filters and valid?
701 end if filters and valid?
702
702
703 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
703 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
704 # Excludes results for which the grouped custom field is not visible
704 # Excludes results for which the grouped custom field is not visible
705 filters_clauses << c.custom_field.visibility_by_project_condition
705 filters_clauses << c.custom_field.visibility_by_project_condition
706 end
706 end
707
707
708 filters_clauses << project_statement
708 filters_clauses << project_statement
709 filters_clauses.reject!(&:blank?)
709 filters_clauses.reject!(&:blank?)
710
710
711 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
711 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
712 end
712 end
713
713
714 # Returns the sum of values for the given column
714 # Returns the sum of values for the given column
715 def total_for(column)
715 def total_for(column)
716 total_with_scope(column, base_scope)
716 total_with_scope(column, base_scope)
717 end
717 end
718
718
719 # Returns a hash of the sum of the given column for each group,
719 # Returns a hash of the sum of the given column for each group,
720 # or nil if the query is not grouped
720 # or nil if the query is not grouped
721 def total_by_group_for(column)
721 def total_by_group_for(column)
722 grouped_query do |scope|
722 grouped_query do |scope|
723 total_with_scope(column, scope)
723 total_with_scope(column, scope)
724 end
724 end
725 end
725 end
726
726
727 def totals
727 def totals
728 totals = totalable_columns.map {|column| [column, total_for(column)]}
728 totals = totalable_columns.map {|column| [column, total_for(column)]}
729 yield totals if block_given?
729 yield totals if block_given?
730 totals
730 totals
731 end
731 end
732
732
733 def totals_by_group
733 def totals_by_group
734 totals = totalable_columns.map {|column| [column, total_by_group_for(column)]}
734 totals = totalable_columns.map {|column| [column, total_by_group_for(column)]}
735 yield totals if block_given?
735 yield totals if block_given?
736 totals
736 totals
737 end
737 end
738
738
739 private
739 private
740
740
741 def grouped_query(&block)
741 def grouped_query(&block)
742 r = nil
742 r = nil
743 if grouped?
743 if grouped?
744 begin
744 begin
745 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
745 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
746 r = yield base_group_scope
746 r = yield base_group_scope
747 rescue ActiveRecord::RecordNotFound
747 rescue ActiveRecord::RecordNotFound
748 r = {nil => yield(base_scope)}
748 r = {nil => yield(base_scope)}
749 end
749 end
750 c = group_by_column
750 c = group_by_column
751 if c.is_a?(QueryCustomFieldColumn)
751 if c.is_a?(QueryCustomFieldColumn)
752 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
752 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
753 end
753 end
754 end
754 end
755 r
755 r
756 rescue ::ActiveRecord::StatementInvalid => e
756 rescue ::ActiveRecord::StatementInvalid => e
757 raise StatementInvalid.new(e.message)
757 raise StatementInvalid.new(e.message)
758 end
758 end
759
759
760 def total_with_scope(column, scope)
760 def total_with_scope(column, scope)
761 unless column.is_a?(QueryColumn)
761 unless column.is_a?(QueryColumn)
762 column = column.to_sym
762 column = column.to_sym
763 column = available_totalable_columns.detect {|c| c.name == column}
763 column = available_totalable_columns.detect {|c| c.name == column}
764 end
764 end
765 if column.is_a?(QueryCustomFieldColumn)
765 if column.is_a?(QueryCustomFieldColumn)
766 custom_field = column.custom_field
766 custom_field = column.custom_field
767 send "total_for_custom_field", custom_field, scope
767 send "total_for_custom_field", custom_field, scope
768 else
768 else
769 send "total_for_#{column.name}", scope
769 send "total_for_#{column.name}", scope
770 end
770 end
771 rescue ::ActiveRecord::StatementInvalid => e
771 rescue ::ActiveRecord::StatementInvalid => e
772 raise StatementInvalid.new(e.message)
772 raise StatementInvalid.new(e.message)
773 end
773 end
774
774
775 def base_scope
775 def base_scope
776 raise "unimplemented"
776 raise "unimplemented"
777 end
777 end
778
778
779 def base_group_scope
779 def base_group_scope
780 base_scope.
780 base_scope.
781 joins(joins_for_order_statement(group_by_statement)).
781 joins(joins_for_order_statement(group_by_statement)).
782 group(group_by_statement)
782 group(group_by_statement)
783 end
783 end
784
784
785 def total_for_custom_field(custom_field, scope, &block)
785 def total_for_custom_field(custom_field, scope, &block)
786 total = custom_field.format.total_for_scope(custom_field, scope)
786 total = custom_field.format.total_for_scope(custom_field, scope)
787 total = map_total(total) {|t| custom_field.format.cast_total_value(custom_field, t)}
787 total = map_total(total) {|t| custom_field.format.cast_total_value(custom_field, t)}
788 total
788 total
789 end
789 end
790
790
791 def map_total(total, &block)
791 def map_total(total, &block)
792 if total.is_a?(Hash)
792 if total.is_a?(Hash)
793 total.keys.each {|k| total[k] = yield total[k]}
793 total.keys.each {|k| total[k] = yield total[k]}
794 else
794 else
795 total = yield total
795 total = yield total
796 end
796 end
797 total
797 total
798 end
798 end
799
799
800 def sql_for_custom_field(field, operator, value, custom_field_id)
800 def sql_for_custom_field(field, operator, value, custom_field_id)
801 db_table = CustomValue.table_name
801 db_table = CustomValue.table_name
802 db_field = 'value'
802 db_field = 'value'
803 filter = @available_filters[field]
803 filter = @available_filters[field]
804 return nil unless filter
804 return nil unless filter
805 if filter[:field].format.target_class && filter[:field].format.target_class <= User
805 if filter[:field].format.target_class && filter[:field].format.target_class <= User
806 if value.delete('me')
806 if value.delete('me')
807 value.push User.current.id.to_s
807 value.push User.current.id.to_s
808 end
808 end
809 end
809 end
810 not_in = nil
810 not_in = nil
811 if operator == '!'
811 if operator == '!'
812 # Makes ! operator work for custom fields with multiple values
812 # Makes ! operator work for custom fields with multiple values
813 operator = '='
813 operator = '='
814 not_in = 'NOT'
814 not_in = 'NOT'
815 end
815 end
816 customized_key = "id"
816 customized_key = "id"
817 customized_class = queried_class
817 customized_class = queried_class
818 if field =~ /^(.+)\.cf_/
818 if field =~ /^(.+)\.cf_/
819 assoc = $1
819 assoc = $1
820 customized_key = "#{assoc}_id"
820 customized_key = "#{assoc}_id"
821 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
821 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
822 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
822 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
823 end
823 end
824 where = sql_for_field(field, operator, value, db_table, db_field, true)
824 where = sql_for_field(field, operator, value, db_table, db_field, true)
825 if operator =~ /[<>]/
825 if operator =~ /[<>]/
826 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
826 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
827 end
827 end
828 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
828 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
829 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
829 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
830 " LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id}" +
830 " LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id}" +
831 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
831 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
832 end
832 end
833
833
834 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
834 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
835 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
835 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
836 sql = ''
836 sql = ''
837 case operator
837 case operator
838 when "="
838 when "="
839 if value.any?
839 if value.any?
840 case type_for(field)
840 case type_for(field)
841 when :date, :date_past
841 when :date, :date_past
842 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
842 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
843 when :integer
843 when :integer
844 int_values = value.first.to_s.scan(/[+-]?\d+/).map(&:to_i).join(",")
844 int_values = value.first.to_s.scan(/[+-]?\d+/).map(&:to_i).join(",")
845 if int_values.present?
845 if int_values.present?
846 if is_custom_filter
846 if is_custom_filter
847 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) IN (#{int_values}))"
847 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) IN (#{int_values}))"
848 else
848 else
849 sql = "#{db_table}.#{db_field} IN (#{int_values})"
849 sql = "#{db_table}.#{db_field} IN (#{int_values})"
850 end
850 end
851 else
851 else
852 sql = "1=0"
852 sql = "1=0"
853 end
853 end
854 when :float
854 when :float
855 if is_custom_filter
855 if is_custom_filter
856 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
856 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
857 else
857 else
858 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
858 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
859 end
859 end
860 else
860 else
861 sql = queried_class.send(:sanitize_sql_for_conditions, ["#{db_table}.#{db_field} IN (?)", value])
861 sql = queried_class.send(:sanitize_sql_for_conditions, ["#{db_table}.#{db_field} IN (?)", value])
862 end
862 end
863 else
863 else
864 # IN an empty set
864 # IN an empty set
865 sql = "1=0"
865 sql = "1=0"
866 end
866 end
867 when "!"
867 when "!"
868 if value.any?
868 if value.any?
869 sql = queried_class.send(:sanitize_sql_for_conditions, ["(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (?))", value])
869 sql = queried_class.send(:sanitize_sql_for_conditions, ["(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (?))", value])
870 else
870 else
871 # NOT IN an empty set
871 # NOT IN an empty set
872 sql = "1=1"
872 sql = "1=1"
873 end
873 end
874 when "!*"
874 when "!*"
875 sql = "#{db_table}.#{db_field} IS NULL"
875 sql = "#{db_table}.#{db_field} IS NULL"
876 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
876 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
877 when "*"
877 when "*"
878 sql = "#{db_table}.#{db_field} IS NOT NULL"
878 sql = "#{db_table}.#{db_field} IS NOT NULL"
879 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
879 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
880 when ">="
880 when ">="
881 if [:date, :date_past].include?(type_for(field))
881 if [:date, :date_past].include?(type_for(field))
882 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
882 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
883 else
883 else
884 if is_custom_filter
884 if is_custom_filter
885 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})"
885 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})"
886 else
886 else
887 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
887 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
888 end
888 end
889 end
889 end
890 when "<="
890 when "<="
891 if [:date, :date_past].include?(type_for(field))
891 if [:date, :date_past].include?(type_for(field))
892 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
892 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
893 else
893 else
894 if is_custom_filter
894 if is_custom_filter
895 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})"
895 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})"
896 else
896 else
897 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
897 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
898 end
898 end
899 end
899 end
900 when "><"
900 when "><"
901 if [:date, :date_past].include?(type_for(field))
901 if [:date, :date_past].include?(type_for(field))
902 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
902 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
903 else
903 else
904 if is_custom_filter
904 if is_custom_filter
905 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
905 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
906 else
906 else
907 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
907 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
908 end
908 end
909 end
909 end
910 when "o"
910 when "o"
911 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false})" if field == "status_id"
911 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false})" if field == "status_id"
912 when "c"
912 when "c"
913 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_true})" if field == "status_id"
913 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_true})" if field == "status_id"
914 when "><t-"
914 when "><t-"
915 # between today - n days and today
915 # between today - n days and today
916 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
916 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
917 when ">t-"
917 when ">t-"
918 # >= today - n days
918 # >= today - n days
919 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
919 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
920 when "<t-"
920 when "<t-"
921 # <= today - n days
921 # <= today - n days
922 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
922 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
923 when "t-"
923 when "t-"
924 # = n days in past
924 # = n days in past
925 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
925 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
926 when "><t+"
926 when "><t+"
927 # between today and today + n days
927 # between today and today + n days
928 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
928 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
929 when ">t+"
929 when ">t+"
930 # >= today + n days
930 # >= today + n days
931 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
931 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
932 when "<t+"
932 when "<t+"
933 # <= today + n days
933 # <= today + n days
934 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
934 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
935 when "t+"
935 when "t+"
936 # = today + n days
936 # = today + n days
937 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
937 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
938 when "t"
938 when "t"
939 # = today
939 # = today
940 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
940 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
941 when "ld"
941 when "ld"
942 # = yesterday
942 # = yesterday
943 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
943 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
944 when "w"
944 when "w"
945 # = this week
945 # = this week
946 first_day_of_week = l(:general_first_day_of_week).to_i
946 first_day_of_week = l(:general_first_day_of_week).to_i
947 day_of_week = User.current.today.cwday
947 day_of_week = User.current.today.cwday
948 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
948 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
949 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
949 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
950 when "lw"
950 when "lw"
951 # = last week
951 # = last week
952 first_day_of_week = l(:general_first_day_of_week).to_i
952 first_day_of_week = l(:general_first_day_of_week).to_i
953 day_of_week = User.current.today.cwday
953 day_of_week = User.current.today.cwday
954 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
954 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
955 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
955 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
956 when "l2w"
956 when "l2w"
957 # = last 2 weeks
957 # = last 2 weeks
958 first_day_of_week = l(:general_first_day_of_week).to_i
958 first_day_of_week = l(:general_first_day_of_week).to_i
959 day_of_week = User.current.today.cwday
959 day_of_week = User.current.today.cwday
960 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
960 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
961 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
961 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
962 when "m"
962 when "m"
963 # = this month
963 # = this month
964 date = User.current.today
964 date = User.current.today
965 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
965 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
966 when "lm"
966 when "lm"
967 # = last month
967 # = last month
968 date = User.current.today.prev_month
968 date = User.current.today.prev_month
969 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
969 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
970 when "y"
970 when "y"
971 # = this year
971 # = this year
972 date = User.current.today
972 date = User.current.today
973 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
973 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
974 when "~"
974 when "~"
975 sql = sql_contains("#{db_table}.#{db_field}", value.first)
975 sql = sql_contains("#{db_table}.#{db_field}", value.first)
976 when "!~"
976 when "!~"
977 sql = sql_contains("#{db_table}.#{db_field}", value.first, false)
977 sql = sql_contains("#{db_table}.#{db_field}", value.first, false)
978 else
978 else
979 raise "Unknown query operator #{operator}"
979 raise "Unknown query operator #{operator}"
980 end
980 end
981
981
982 return sql
982 return sql
983 end
983 end
984
984
985 # Returns a SQL LIKE statement with wildcards
985 # Returns a SQL LIKE statement with wildcards
986 def sql_contains(db_field, value, match=true)
986 def sql_contains(db_field, value, match=true)
987 queried_class.send :sanitize_sql_for_conditions,
987 queried_class.send :sanitize_sql_for_conditions,
988 [Redmine::Database.like(db_field, '?', :match => match), "%#{value}%"]
988 [Redmine::Database.like(db_field, '?', :match => match), "%#{value}%"]
989 end
989 end
990
990
991 # Adds a filter for the given custom field
991 # Adds a filter for the given custom field
992 def add_custom_field_filter(field, assoc=nil)
992 def add_custom_field_filter(field, assoc=nil)
993 options = field.query_filter_options(self)
993 options = field.query_filter_options(self)
994 if field.format.target_class && field.format.target_class <= User
994 if field.format.target_class && field.format.target_class <= User
995 if options[:values].is_a?(Array) && User.current.logged?
995 if options[:values].is_a?(Array) && User.current.logged?
996 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
996 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
997 end
997 end
998 end
998 end
999
999
1000 filter_id = "cf_#{field.id}"
1000 filter_id = "cf_#{field.id}"
1001 filter_name = field.name
1001 filter_name = field.name
1002 if assoc.present?
1002 if assoc.present?
1003 filter_id = "#{assoc}.#{filter_id}"
1003 filter_id = "#{assoc}.#{filter_id}"
1004 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
1004 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
1005 end
1005 end
1006 add_available_filter filter_id, options.merge({
1006 add_available_filter filter_id, options.merge({
1007 :name => filter_name,
1007 :name => filter_name,
1008 :field => field
1008 :field => field
1009 })
1009 })
1010 end
1010 end
1011
1011
1012 # Adds filters for the given custom fields scope
1012 # Adds filters for the given custom fields scope
1013 def add_custom_fields_filters(scope, assoc=nil)
1013 def add_custom_fields_filters(scope, assoc=nil)
1014 scope.visible.where(:is_filter => true).sorted.each do |field|
1014 scope.visible.where(:is_filter => true).sorted.each do |field|
1015 add_custom_field_filter(field, assoc)
1015 add_custom_field_filter(field, assoc)
1016 end
1016 end
1017 end
1017 end
1018
1018
1019 # Adds filters for the given associations custom fields
1019 # Adds filters for the given associations custom fields
1020 def add_associations_custom_fields_filters(*associations)
1020 def add_associations_custom_fields_filters(*associations)
1021 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
1021 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
1022 associations.each do |assoc|
1022 associations.each do |assoc|
1023 association_klass = queried_class.reflect_on_association(assoc).klass
1023 association_klass = queried_class.reflect_on_association(assoc).klass
1024 fields_by_class.each do |field_class, fields|
1024 fields_by_class.each do |field_class, fields|
1025 if field_class.customized_class <= association_klass
1025 if field_class.customized_class <= association_klass
1026 fields.sort.each do |field|
1026 fields.sort.each do |field|
1027 add_custom_field_filter(field, assoc)
1027 add_custom_field_filter(field, assoc)
1028 end
1028 end
1029 end
1029 end
1030 end
1030 end
1031 end
1031 end
1032 end
1032 end
1033
1033
1034 def quoted_time(time, is_custom_filter)
1034 def quoted_time(time, is_custom_filter)
1035 if is_custom_filter
1035 if is_custom_filter
1036 # Custom field values are stored as strings in the DB
1036 # Custom field values are stored as strings in the DB
1037 # using this format that does not depend on DB date representation
1037 # using this format that does not depend on DB date representation
1038 time.strftime("%Y-%m-%d %H:%M:%S")
1038 time.strftime("%Y-%m-%d %H:%M:%S")
1039 else
1039 else
1040 self.class.connection.quoted_date(time)
1040 self.class.connection.quoted_date(time)
1041 end
1041 end
1042 end
1042 end
1043
1043
1044 def date_for_user_time_zone(y, m, d)
1044 def date_for_user_time_zone(y, m, d)
1045 if tz = User.current.time_zone
1045 if tz = User.current.time_zone
1046 tz.local y, m, d
1046 tz.local y, m, d
1047 else
1047 else
1048 Time.local y, m, d
1048 Time.local y, m, d
1049 end
1049 end
1050 end
1050 end
1051
1051
1052 # Returns a SQL clause for a date or datetime field.
1052 # Returns a SQL clause for a date or datetime field.
1053 def date_clause(table, field, from, to, is_custom_filter)
1053 def date_clause(table, field, from, to, is_custom_filter)
1054 s = []
1054 s = []
1055 if from
1055 if from
1056 if from.is_a?(Date)
1056 if from.is_a?(Date)
1057 from = date_for_user_time_zone(from.year, from.month, from.day).yesterday.end_of_day
1057 from = date_for_user_time_zone(from.year, from.month, from.day).yesterday.end_of_day
1058 else
1058 else
1059 from = from - 1 # second
1059 from = from - 1 # second
1060 end
1060 end
1061 if self.class.default_timezone == :utc
1061 if self.class.default_timezone == :utc
1062 from = from.utc
1062 from = from.utc
1063 end
1063 end
1064 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
1064 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
1065 end
1065 end
1066 if to
1066 if to
1067 if to.is_a?(Date)
1067 if to.is_a?(Date)
1068 to = date_for_user_time_zone(to.year, to.month, to.day).end_of_day
1068 to = date_for_user_time_zone(to.year, to.month, to.day).end_of_day
1069 end
1069 end
1070 if self.class.default_timezone == :utc
1070 if self.class.default_timezone == :utc
1071 to = to.utc
1071 to = to.utc
1072 end
1072 end
1073 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
1073 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
1074 end
1074 end
1075 s.join(' AND ')
1075 s.join(' AND ')
1076 end
1076 end
1077
1077
1078 # Returns a SQL clause for a date or datetime field using relative dates.
1078 # Returns a SQL clause for a date or datetime field using relative dates.
1079 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
1079 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
1080 date_clause(table, field, (days_from ? User.current.today + days_from : nil), (days_to ? User.current.today + days_to : nil), is_custom_filter)
1080 date_clause(table, field, (days_from ? User.current.today + days_from : nil), (days_to ? User.current.today + days_to : nil), is_custom_filter)
1081 end
1081 end
1082
1082
1083 # Returns a Date or Time from the given filter value
1083 # Returns a Date or Time from the given filter value
1084 def parse_date(arg)
1084 def parse_date(arg)
1085 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
1085 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
1086 Time.parse(arg) rescue nil
1086 Time.parse(arg) rescue nil
1087 else
1087 else
1088 Date.parse(arg) rescue nil
1088 Date.parse(arg) rescue nil
1089 end
1089 end
1090 end
1090 end
1091
1091
1092 # Additional joins required for the given sort options
1092 # Additional joins required for the given sort options
1093 def joins_for_order_statement(order_options)
1093 def joins_for_order_statement(order_options)
1094 joins = []
1094 joins = []
1095
1095
1096 if order_options
1096 if order_options
1097 if order_options.include?('authors')
1097 if order_options.include?('authors')
1098 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
1098 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
1099 end
1099 end
1100 order_options.scan(/cf_\d+/).uniq.each do |name|
1100 order_options.scan(/cf_\d+/).uniq.each do |name|
1101 column = available_columns.detect {|c| c.name.to_s == name}
1101 column = available_columns.detect {|c| c.name.to_s == name}
1102 join = column && column.custom_field.join_for_order_statement
1102 join = column && column.custom_field.join_for_order_statement
1103 if join
1103 if join
1104 joins << join
1104 joins << join
1105 end
1105 end
1106 end
1106 end
1107 end
1107 end
1108
1108
1109 joins.any? ? joins.join(' ') : nil
1109 joins.any? ? joins.join(' ') : nil
1110 end
1110 end
1111 end
1111 end
@@ -1,142 +1,163
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 TimeEntryQuery < Query
18 class TimeEntryQuery < Query
19
19
20 self.queried_class = TimeEntry
20 self.queried_class = TimeEntry
21 self.view_permission = :view_time_entries
21 self.view_permission = :view_time_entries
22
22
23 self.available_columns = [
23 self.available_columns = [
24 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
24 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
25 QueryColumn.new(:spent_on, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"], :default_order => 'desc', :groupable => true),
25 QueryColumn.new(:spent_on, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"], :default_order => 'desc', :groupable => true),
26 QueryColumn.new(:tweek, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"], :caption => l(:label_week)),
26 QueryColumn.new(:tweek, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"], :caption => l(:label_week)),
27 QueryColumn.new(:user, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
27 QueryColumn.new(:user, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
28 QueryColumn.new(:activity, :sortable => "#{TimeEntryActivity.table_name}.position", :groupable => true),
28 QueryColumn.new(:activity, :sortable => "#{TimeEntryActivity.table_name}.position", :groupable => true),
29 QueryColumn.new(:issue, :sortable => "#{Issue.table_name}.id"),
29 QueryColumn.new(:issue, :sortable => "#{Issue.table_name}.id"),
30 QueryColumn.new(:comments),
30 QueryColumn.new(:comments),
31 QueryColumn.new(:hours, :sortable => "#{TimeEntry.table_name}.hours"),
31 QueryColumn.new(:hours, :sortable => "#{TimeEntry.table_name}.hours"),
32 ]
32 ]
33
33
34 def initialize(attributes=nil, *args)
34 def initialize(attributes=nil, *args)
35 super attributes
35 super attributes
36 self.filters ||= {}
36 self.filters ||= {}
37 add_filter('spent_on', '*') unless filters.present?
37 add_filter('spent_on', '*') unless filters.present?
38 end
38 end
39
39
40 def initialize_available_filters
40 def initialize_available_filters
41 add_available_filter "spent_on", :type => :date_past
41 add_available_filter "spent_on", :type => :date_past
42
42
43 principals = []
43 principals = []
44 if project
44 if project
45 principals += project.principals.visible.sort
45 principals += project.principals.visible.sort
46 unless project.leaf?
46 unless project.leaf?
47 subprojects = project.descendants.visible.to_a
47 subprojects = project.descendants.visible.to_a
48 if subprojects.any?
48 if subprojects.any?
49 add_available_filter "subproject_id",
49 add_available_filter "subproject_id",
50 :type => :list_subprojects,
50 :type => :list_subprojects,
51 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
51 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
52 principals += Principal.member_of(subprojects).visible
52 principals += Principal.member_of(subprojects).visible
53 end
53 end
54 end
54 end
55 else
55 else
56 if all_projects.any?
56 if all_projects.any?
57 # members of visible projects
57 # members of visible projects
58 principals += Principal.member_of(all_projects).visible
58 principals += Principal.member_of(all_projects).visible
59 # project filter
59 # project filter
60 project_values = []
60 project_values = []
61 if User.current.logged? && User.current.memberships.any?
61 if User.current.logged? && User.current.memberships.any?
62 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
62 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
63 end
63 end
64 project_values += all_projects_values
64 project_values += all_projects_values
65 add_available_filter("project_id",
65 add_available_filter("project_id",
66 :type => :list, :values => project_values
66 :type => :list, :values => project_values
67 ) unless project_values.empty?
67 ) unless project_values.empty?
68 end
68 end
69 end
69 end
70
71 add_available_filter("issue_id", :type => :tree, :label => :label_issue)
72
70 principals.uniq!
73 principals.uniq!
71 principals.sort!
74 principals.sort!
72 users = principals.select {|p| p.is_a?(User)}
75 users = principals.select {|p| p.is_a?(User)}
73
76
74 users_values = []
77 users_values = []
75 users_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
78 users_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
76 users_values += users.collect{|s| [s.name, s.id.to_s] }
79 users_values += users.collect{|s| [s.name, s.id.to_s] }
77 add_available_filter("user_id",
80 add_available_filter("user_id",
78 :type => :list_optional, :values => users_values
81 :type => :list_optional, :values => users_values
79 ) unless users_values.empty?
82 ) unless users_values.empty?
80
83
81 activities = (project ? project.activities : TimeEntryActivity.shared)
84 activities = (project ? project.activities : TimeEntryActivity.shared)
82 add_available_filter("activity_id",
85 add_available_filter("activity_id",
83 :type => :list, :values => activities.map {|a| [a.name, a.id.to_s]}
86 :type => :list, :values => activities.map {|a| [a.name, a.id.to_s]}
84 ) unless activities.empty?
87 ) unless activities.empty?
85
88
86 add_available_filter "comments", :type => :text
89 add_available_filter "comments", :type => :text
87 add_available_filter "hours", :type => :float
90 add_available_filter "hours", :type => :float
88
91
89 add_custom_fields_filters(TimeEntryCustomField)
92 add_custom_fields_filters(TimeEntryCustomField)
90 add_associations_custom_fields_filters :project, :issue, :user
93 add_associations_custom_fields_filters :project, :issue, :user
91 end
94 end
92
95
93 def available_columns
96 def available_columns
94 return @available_columns if @available_columns
97 return @available_columns if @available_columns
95 @available_columns = self.class.available_columns.dup
98 @available_columns = self.class.available_columns.dup
96 @available_columns += TimeEntryCustomField.visible.
99 @available_columns += TimeEntryCustomField.visible.
97 map {|cf| QueryCustomFieldColumn.new(cf) }
100 map {|cf| QueryCustomFieldColumn.new(cf) }
98 @available_columns += IssueCustomField.visible.
101 @available_columns += IssueCustomField.visible.
99 map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf) }
102 map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf) }
100 @available_columns
103 @available_columns
101 end
104 end
102
105
103 def default_columns_names
106 def default_columns_names
104 @default_columns_names ||= [:project, :spent_on, :user, :activity, :issue, :comments, :hours]
107 @default_columns_names ||= [:project, :spent_on, :user, :activity, :issue, :comments, :hours]
105 end
108 end
106
109
107 def results_scope(options={})
110 def results_scope(options={})
108 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
111 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
109
112
110 TimeEntry.visible.
113 TimeEntry.visible.
111 where(statement).
114 where(statement).
112 order(order_option).
115 order(order_option).
113 joins(joins_for_order_statement(order_option.join(','))).
116 joins(joins_for_order_statement(order_option.join(','))).
114 includes(:activity).
117 includes(:activity).
115 references(:activity)
118 references(:activity)
116 end
119 end
120
121 def sql_for_issue_id_field(field, operator, value)
122 case operator
123 when "="
124 "#{TimeEntry.table_name}.issue_id = #{value.first.to_i}"
125 when "~"
126 issue = Issue.where(:id => value.first.to_i).first
127 if issue && (issue_ids = issue.self_and_descendants.pluck(:id)).any?
128 "#{TimeEntry.table_name}.issue_id IN (#{issue_ids.join(',')})"
129 else
130 "1=0"
131 end
132 when "!*"
133 "#{TimeEntry.table_name}.issue_id IS NULL"
134 when "*"
135 "#{TimeEntry.table_name}.issue_id IS NOT NULL"
136 end
137 end
117
138
118 def sql_for_activity_id_field(field, operator, value)
139 def sql_for_activity_id_field(field, operator, value)
119 condition_on_id = sql_for_field(field, operator, value, Enumeration.table_name, 'id')
140 condition_on_id = sql_for_field(field, operator, value, Enumeration.table_name, 'id')
120 condition_on_parent_id = sql_for_field(field, operator, value, Enumeration.table_name, 'parent_id')
141 condition_on_parent_id = sql_for_field(field, operator, value, Enumeration.table_name, 'parent_id')
121 ids = value.map(&:to_i).join(',')
142 ids = value.map(&:to_i).join(',')
122 table_name = Enumeration.table_name
143 table_name = Enumeration.table_name
123 if operator == '='
144 if operator == '='
124 "(#{table_name}.id IN (#{ids}) OR #{table_name}.parent_id IN (#{ids}))"
145 "(#{table_name}.id IN (#{ids}) OR #{table_name}.parent_id IN (#{ids}))"
125 else
146 else
126 "(#{table_name}.id NOT IN (#{ids}) AND (#{table_name}.parent_id IS NULL OR #{table_name}.parent_id NOT IN (#{ids})))"
147 "(#{table_name}.id NOT IN (#{ids}) AND (#{table_name}.parent_id IS NULL OR #{table_name}.parent_id NOT IN (#{ids})))"
127 end
148 end
128 end
149 end
129
150
130 # Accepts :from/:to params as shortcut filters
151 # Accepts :from/:to params as shortcut filters
131 def build_from_params(params)
152 def build_from_params(params)
132 super
153 super
133 if params[:from].present? && params[:to].present?
154 if params[:from].present? && params[:to].present?
134 add_filter('spent_on', '><', [params[:from], params[:to]])
155 add_filter('spent_on', '><', [params[:from], params[:to]])
135 elsif params[:from].present?
156 elsif params[:from].present?
136 add_filter('spent_on', '>=', [params[:from]])
157 add_filter('spent_on', '>=', [params[:from]])
137 elsif params[:to].present?
158 elsif params[:to].present?
138 add_filter('spent_on', '<=', [params[:to]])
159 add_filter('spent_on', '<=', [params[:to]])
139 end
160 end
140 self
161 self
141 end
162 end
142 end
163 end
@@ -1,51 +1,51
1 <div id="query_form_with_buttons" class="hide-when-print">
1 <div id="query_form_with_buttons" class="hide-when-print">
2 <%= hidden_field_tag 'set_filter', '1' %>
2 <%= hidden_field_tag 'set_filter', '1' %>
3 <div id="query_form_content">
3 <div id="query_form_content">
4 <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
4 <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
5 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
5 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
6 <div style="<%= @query.new_record? ? "" : "display: none;" %>">
6 <div style="<%= @query.new_record? ? "" : "display: none;" %>">
7 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
7 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
8 </div>
8 </div>
9 </fieldset>
9 </fieldset>
10 <fieldset id="options" class="collapsible collapsed">
10 <fieldset id="options" class="collapsible collapsed">
11 <legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend>
11 <legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend>
12 <div style="display: none;">
12 <div style="display: none;">
13 <table>
13 <table>
14 <tr>
14 <tr>
15 <td class="field"><%= l(:field_column_names) %></td>
15 <td class="field"><%= l(:field_column_names) %></td>
16 <td><%= render_query_columns_selection(@query) %></td>
16 <td><%= render_query_columns_selection(@query) %></td>
17 </tr>
17 </tr>
18 </table>
18 </table>
19 </div>
19 </div>
20 </fieldset>
20 </fieldset>
21 </div>
21 </div>
22
22
23 <p class="buttons">
23 <p class="buttons">
24 <%= link_to_function l(:button_apply), '$("#query_form").submit()', :class => 'icon icon-checked' %>
24 <%= link_to_function l(:button_apply), '$("#query_form").submit()', :class => 'icon icon-checked' %>
25 <%= link_to l(:button_clear), {:project_id => @project, :issue_id => @issue}, :class => 'icon icon-reload' %>
25 <%= link_to l(:button_clear), {:project_id => @project, :issue_id => @issue}, :class => 'icon icon-reload' %>
26 <% if @query.new_record? %>
26 <% if @query.new_record? %>
27 <% if User.current.allowed_to?(:save_queries, @project, :global => true) %>
27 <% if User.current.allowed_to?(:save_queries, @project, :global => true) %>
28 <%= link_to_function l(:button_save),
28 <%= link_to_function l(:button_save),
29 "$('#query_form').attr('action', '#{ @project ? new_project_query_path(@project) : new_query_path }').submit()",
29 "$('#query_form').attr('action', '#{ @project ? new_project_query_path(@project) : new_query_path }').submit()",
30 :class => 'icon icon-save' %>
30 :class => 'icon icon-save' %>
31 <% end %>
31 <% end %>
32 <% else %>
32 <% else %>
33 <% if @query.editable_by?(User.current) %>
33 <% if @query.editable_by?(User.current) %>
34 <%= link_to l(:button_edit), edit_query_path(@query), :class => 'icon icon-edit' %>
34 <%= link_to l(:button_edit), edit_query_path(@query), :class => 'icon icon-edit' %>
35 <%= delete_link query_path(@query) %>
35 <%= delete_link query_path(@query) %>
36 <% end %>
36 <% end %>
37 <% end %>
37 <% end %>
38 </p>
38 </p>
39
39
40 <%= hidden_field_tag 'type', 'TimeEntryQuery' %>
40 <%= hidden_field_tag 'type', 'TimeEntryQuery' %>
41 </div>
41 </div>
42
42
43 <div class="tabs hide-when-print">
43 <div class="tabs hide-when-print">
44 <% query_params = params.slice(:f, :op, :v, :sort, :query_id) %>
44 <% query_params = request.query_parameters %>
45 <ul>
45 <ul>
46 <li><%= link_to(l(:label_details), _time_entries_path(@project, @issue, query_params),
46 <li><%= link_to(l(:label_details), _time_entries_path(@project, nil, :params => query_params),
47 :class => (action_name == 'index' ? 'selected' : nil)) %></li>
47 :class => (action_name == 'index' ? 'selected' : nil)) %></li>
48 <li><%= link_to(l(:label_report), _report_time_entries_path(@project, @issue, query_params),
48 <li><%= link_to(l(:label_report), _report_time_entries_path(@project, nil, :params => query_params),
49 :class => (action_name == 'report' ? 'selected' : nil)) %></li>
49 :class => (action_name == 'report' ? 'selected' : nil)) %></li>
50 </ul>
50 </ul>
51 </div>
51 </div>
@@ -1,52 +1,50
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to l(:button_log_time),
2 <%= link_to l(:button_log_time),
3 _new_time_entry_path(@project, @issue),
3 _new_time_entry_path(@project, @issue),
4 :class => 'icon icon-time-add' if User.current.allowed_to?(:log_time, @project, :global => true) %>
4 :class => 'icon icon-time-add' if User.current.allowed_to?(:log_time, @project, :global => true) %>
5 </div>
5 </div>
6
6
7 <%= render_timelog_breadcrumb %>
8
9 <h2><%= @query.new_record? ? l(:label_spent_time) : @query.name %></h2>
7 <h2><%= @query.new_record? ? l(:label_spent_time) : @query.name %></h2>
10
8
11 <%= form_tag(params.slice(:project_id, :issue_id), :method => :get, :id => 'query_form') do %>
9 <%= form_tag(_time_entries_path(@project, nil), :method => :get, :id => 'query_form') do %>
12 <%= render :partial => 'date_range' %>
10 <%= render :partial => 'date_range' %>
13 <% end %>
11 <% end %>
14
12
15 <div class="total-hours">
13 <div class="total-hours">
16 <p><%= l(:label_total_time) %>: <%= html_hours(l_hours(@total_hours)) %></p>
14 <p><%= l(:label_total_time) %>: <%= html_hours(l_hours(@total_hours)) %></p>
17 </div>
15 </div>
18
16
19 <% unless @entries.empty? %>
17 <% unless @entries.empty? %>
20 <%= render :partial => 'list', :locals => { :entries => @entries }%>
18 <%= render :partial => 'list', :locals => { :entries => @entries }%>
21 <span class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></span>
19 <span class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></span>
22
20
23 <% other_formats_links do |f| %>
21 <% other_formats_links do |f| %>
24 <%= f.link_to_with_query_parameters 'Atom', :key => User.current.rss_key %>
22 <%= f.link_to_with_query_parameters 'Atom', :key => User.current.rss_key %>
25 <%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '330px'); return false;" %>
23 <%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '330px'); return false;" %>
26 <% end %>
24 <% end %>
27
25
28 <div id="csv-export-options" style="display:none;">
26 <div id="csv-export-options" style="display:none;">
29 <h3 class="title"><%= l(:label_export_options, :export_format => 'CSV') %></h3>
27 <h3 class="title"><%= l(:label_export_options, :export_format => 'CSV') %></h3>
30 <%= form_tag(params.slice(:project_id, :issue_id).merge(:format => 'csv', :page=>nil), :method => :get, :id => 'csv-export-form') do %>
28 <%= form_tag(params.slice(:project_id, :issue_id).merge(:format => 'csv', :page=>nil), :method => :get, :id => 'csv-export-form') do %>
31 <%= query_hidden_tags @query %>
29 <%= query_hidden_tags @query %>
32 <p>
30 <p>
33 <label><%= radio_button_tag 'columns', '', true %> <%= l(:description_selected_columns) %></label><br />
31 <label><%= radio_button_tag 'columns', '', true %> <%= l(:description_selected_columns) %></label><br />
34 <label><%= radio_button_tag 'columns', 'all' %> <%= l(:description_all_columns) %></label>
32 <label><%= radio_button_tag 'columns', 'all' %> <%= l(:description_all_columns) %></label>
35 </p>
33 </p>
36 <p class="buttons">
34 <p class="buttons">
37 <%= submit_tag l(:button_export), :name => nil, :onclick => "hideModal(this);" %>
35 <%= submit_tag l(:button_export), :name => nil, :onclick => "hideModal(this);" %>
38 <%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => 'button' %>
36 <%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => 'button' %>
39 </p>
37 </p>
40 <% end %>
38 <% end %>
41 </div>
39 </div>
42 <% end %>
40 <% end %>
43
41
44 <% content_for :sidebar do %>
42 <% content_for :sidebar do %>
45 <%= render_sidebar_queries(TimeEntryQuery, @project) %>
43 <%= render_sidebar_queries(TimeEntryQuery, @project) %>
46 <% end %>
44 <% end %>
47
45
48 <% html_title(@query.new_record? ? l(:label_spent_time) : @query.name, l(:label_details)) %>
46 <% html_title(@query.new_record? ? l(:label_spent_time) : @query.name, l(:label_details)) %>
49
47
50 <% content_for :header_tags do %>
48 <% content_for :header_tags do %>
51 <%= auto_discovery_link_tag(:atom, {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :title => l(:label_spent_time)) %>
49 <%= auto_discovery_link_tag(:atom, {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :title => l(:label_spent_time)) %>
52 <% end %>
50 <% end %>
@@ -1,78 +1,76
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to l(:button_log_time),
2 <%= link_to l(:button_log_time),
3 _new_time_entry_path(@project, @issue),
3 _new_time_entry_path(@project, @issue),
4 :class => 'icon icon-time-add' if User.current.allowed_to?(:log_time, @project, :global => true) %>
4 :class => 'icon icon-time-add' if User.current.allowed_to?(:log_time, @project, :global => true) %>
5 </div>
5 </div>
6
6
7 <%= render_timelog_breadcrumb %>
8
9 <h2><%= @query.new_record? ? l(:label_spent_time) : @query.name %></h2>
7 <h2><%= @query.new_record? ? l(:label_spent_time) : @query.name %></h2>
10
8
11 <%= form_tag(params.slice(:project_id, :issue_id), :method => :get, :id => 'query_form') do %>
9 <%= form_tag(_report_time_entries_path(@project, nil), :method => :get, :id => 'query_form') do %>
12 <% @report.criteria.each do |criterion| %>
10 <% @report.criteria.each do |criterion| %>
13 <%= hidden_field_tag 'criteria[]', criterion, :id => nil %>
11 <%= hidden_field_tag 'criteria[]', criterion, :id => nil %>
14 <% end %>
12 <% end %>
15 <%= render :partial => 'timelog/date_range' %>
13 <%= render :partial => 'timelog/date_range' %>
16
14
17 <p><label for='columns'><%= l(:label_details) %></label>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'],
15 <p><label for='columns'><%= l(:label_details) %></label>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'],
18 [l(:label_month), 'month'],
16 [l(:label_month), 'month'],
19 [l(:label_week), 'week'],
17 [l(:label_week), 'week'],
20 [l(:label_day_plural).titleize, 'day']], @report.columns),
18 [l(:label_day_plural).titleize, 'day']], @report.columns),
21 :onchange => "this.form.submit();" %>
19 :onchange => "this.form.submit();" %>
22
20
23 <label for='criterias'><%= l(:button_add) %></label>: <%= select_tag('criteria[]', options_for_select([[]] + (@report.available_criteria.keys - @report.criteria).collect{|k| [l_or_humanize(@report.available_criteria[k][:label]), k]}),
21 <label for='criterias'><%= l(:button_add) %></label>: <%= select_tag('criteria[]', options_for_select([[]] + (@report.available_criteria.keys - @report.criteria).collect{|k| [l_or_humanize(@report.available_criteria[k][:label]), k]}),
24 :onchange => "this.form.submit();",
22 :onchange => "this.form.submit();",
25 :style => 'width: 200px',
23 :style => 'width: 200px',
26 :disabled => (@report.criteria.length >= 3),
24 :disabled => (@report.criteria.length >= 3),
27 :id => "criterias") %>
25 :id => "criterias") %>
28 <%= link_to l(:button_clear), {:project_id => @project, :issue_id => @issue, :period_type => params[:period_type], :period => params[:period], :from => @from, :to => @to, :columns => @report.columns}, :class => 'icon icon-reload' %></p>
26 <%= link_to l(:button_clear), {:project_id => @project, :issue_id => @issue, :period_type => params[:period_type], :period => params[:period], :from => @from, :to => @to, :columns => @report.columns}, :class => 'icon icon-reload' %></p>
29 <% end %>
27 <% end %>
30
28
31 <% unless @report.criteria.empty? %>
29 <% unless @report.criteria.empty? %>
32 <div class="total-hours">
30 <div class="total-hours">
33 <p><%= l(:label_total_time) %>: <%= html_hours(l_hours(@report.total_hours)) %></p>
31 <p><%= l(:label_total_time) %>: <%= html_hours(l_hours(@report.total_hours)) %></p>
34 </div>
32 </div>
35
33
36 <% unless @report.hours.empty? %>
34 <% unless @report.hours.empty? %>
37 <div class="autoscroll">
35 <div class="autoscroll">
38 <table class="list" id="time-report">
36 <table class="list" id="time-report">
39 <thead>
37 <thead>
40 <tr>
38 <tr>
41 <% @report.criteria.each do |criteria| %>
39 <% @report.criteria.each do |criteria| %>
42 <th><%= l_or_humanize(@report.available_criteria[criteria][:label]) %></th>
40 <th><%= l_or_humanize(@report.available_criteria[criteria][:label]) %></th>
43 <% end %>
41 <% end %>
44 <% columns_width = (40 / (@report.periods.length+1)).to_i %>
42 <% columns_width = (40 / (@report.periods.length+1)).to_i %>
45 <% @report.periods.each do |period| %>
43 <% @report.periods.each do |period| %>
46 <th class="period" style="width:<%= columns_width %>%;"><%= period %></th>
44 <th class="period" style="width:<%= columns_width %>%;"><%= period %></th>
47 <% end %>
45 <% end %>
48 <th class="total" style="width:<%= columns_width %>%;"><%= l(:label_total_time) %></th>
46 <th class="total" style="width:<%= columns_width %>%;"><%= l(:label_total_time) %></th>
49 </tr>
47 </tr>
50 </thead>
48 </thead>
51 <tbody>
49 <tbody>
52 <%= render :partial => 'report_criteria', :locals => {:criterias => @report.criteria, :hours => @report.hours, :level => 0} %>
50 <%= render :partial => 'report_criteria', :locals => {:criterias => @report.criteria, :hours => @report.hours, :level => 0} %>
53 <tr class="total">
51 <tr class="total">
54 <td><%= l(:label_total_time) %></td>
52 <td><%= l(:label_total_time) %></td>
55 <%= ('<td></td>' * (@report.criteria.size - 1)).html_safe %>
53 <%= ('<td></td>' * (@report.criteria.size - 1)).html_safe %>
56 <% total = 0 -%>
54 <% total = 0 -%>
57 <% @report.periods.each do |period| -%>
55 <% @report.periods.each do |period| -%>
58 <% sum = sum_hours(select_hours(@report.hours, @report.columns, period.to_s)); total += sum -%>
56 <% sum = sum_hours(select_hours(@report.hours, @report.columns, period.to_s)); total += sum -%>
59 <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
57 <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
60 <% end -%>
58 <% end -%>
61 <td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td>
59 <td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td>
62 </tr>
60 </tr>
63 </tbody>
61 </tbody>
64 </table>
62 </table>
65 </div>
63 </div>
66
64
67 <% other_formats_links do |f| %>
65 <% other_formats_links do |f| %>
68 <%= f.link_to 'CSV', :url => params %>
66 <%= f.link_to 'CSV', :url => params %>
69 <% end %>
67 <% end %>
70 <% end %>
68 <% end %>
71 <% end %>
69 <% end %>
72
70
73 <% content_for :sidebar do %>
71 <% content_for :sidebar do %>
74 <%= render_sidebar_queries(TimeEntryQuery, @project) %>
72 <%= render_sidebar_queries(TimeEntryQuery, @project) %>
75 <% end %>
73 <% end %>
76
74
77 <% html_title(@query.new_record? ? l(:label_spent_time) : @query.name, l(:label_report)) %>
75 <% html_title(@query.new_record? ? l(:label_spent_time) : @query.name, l(:label_report)) %>
78
76
@@ -1,387 +1,383
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 Rails.application.routes.draw do
18 Rails.application.routes.draw do
19 root :to => 'welcome#index', :as => 'home'
19 root :to => 'welcome#index', :as => 'home'
20
20
21 match 'login', :to => 'account#login', :as => 'signin', :via => [:get, :post]
21 match 'login', :to => 'account#login', :as => 'signin', :via => [:get, :post]
22 match 'logout', :to => 'account#logout', :as => 'signout', :via => [:get, :post]
22 match 'logout', :to => 'account#logout', :as => 'signout', :via => [:get, :post]
23 match 'account/register', :to => 'account#register', :via => [:get, :post], :as => 'register'
23 match 'account/register', :to => 'account#register', :via => [:get, :post], :as => 'register'
24 match 'account/lost_password', :to => 'account#lost_password', :via => [:get, :post], :as => 'lost_password'
24 match 'account/lost_password', :to => 'account#lost_password', :via => [:get, :post], :as => 'lost_password'
25 match 'account/activate', :to => 'account#activate', :via => :get
25 match 'account/activate', :to => 'account#activate', :via => :get
26 get 'account/activation_email', :to => 'account#activation_email', :as => 'activation_email'
26 get 'account/activation_email', :to => 'account#activation_email', :as => 'activation_email'
27
27
28 match '/news/preview', :controller => 'previews', :action => 'news', :as => 'preview_news', :via => [:get, :post, :put, :patch]
28 match '/news/preview', :controller => 'previews', :action => 'news', :as => 'preview_news', :via => [:get, :post, :put, :patch]
29 match '/issues/preview/new/:project_id', :to => 'previews#issue', :as => 'preview_new_issue', :via => [:get, :post, :put, :patch]
29 match '/issues/preview/new/:project_id', :to => 'previews#issue', :as => 'preview_new_issue', :via => [:get, :post, :put, :patch]
30 match '/issues/preview/edit/:id', :to => 'previews#issue', :as => 'preview_edit_issue', :via => [:get, :post, :put, :patch]
30 match '/issues/preview/edit/:id', :to => 'previews#issue', :as => 'preview_edit_issue', :via => [:get, :post, :put, :patch]
31 match '/issues/preview', :to => 'previews#issue', :as => 'preview_issue', :via => [:get, :post, :put, :patch]
31 match '/issues/preview', :to => 'previews#issue', :as => 'preview_issue', :via => [:get, :post, :put, :patch]
32
32
33 match 'projects/:id/wiki', :to => 'wikis#edit', :via => :post
33 match 'projects/:id/wiki', :to => 'wikis#edit', :via => :post
34 match 'projects/:id/wiki/destroy', :to => 'wikis#destroy', :via => [:get, :post]
34 match 'projects/:id/wiki/destroy', :to => 'wikis#destroy', :via => [:get, :post]
35
35
36 match 'boards/:board_id/topics/new', :to => 'messages#new', :via => [:get, :post], :as => 'new_board_message'
36 match 'boards/:board_id/topics/new', :to => 'messages#new', :via => [:get, :post], :as => 'new_board_message'
37 get 'boards/:board_id/topics/:id', :to => 'messages#show', :as => 'board_message'
37 get 'boards/:board_id/topics/:id', :to => 'messages#show', :as => 'board_message'
38 match 'boards/:board_id/topics/quote/:id', :to => 'messages#quote', :via => [:get, :post]
38 match 'boards/:board_id/topics/quote/:id', :to => 'messages#quote', :via => [:get, :post]
39 get 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
39 get 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
40
40
41 post 'boards/:board_id/topics/preview', :to => 'messages#preview', :as => 'preview_board_message'
41 post 'boards/:board_id/topics/preview', :to => 'messages#preview', :as => 'preview_board_message'
42 post 'boards/:board_id/topics/:id/replies', :to => 'messages#reply'
42 post 'boards/:board_id/topics/:id/replies', :to => 'messages#reply'
43 post 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
43 post 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
44 post 'boards/:board_id/topics/:id/destroy', :to => 'messages#destroy'
44 post 'boards/:board_id/topics/:id/destroy', :to => 'messages#destroy'
45
45
46 # Misc issue routes. TODO: move into resources
46 # Misc issue routes. TODO: move into resources
47 match '/issues/auto_complete', :to => 'auto_completes#issues', :via => :get, :as => 'auto_complete_issues'
47 match '/issues/auto_complete', :to => 'auto_completes#issues', :via => :get, :as => 'auto_complete_issues'
48 match '/issues/context_menu', :to => 'context_menus#issues', :as => 'issues_context_menu', :via => [:get, :post]
48 match '/issues/context_menu', :to => 'context_menus#issues', :as => 'issues_context_menu', :via => [:get, :post]
49 match '/issues/changes', :to => 'journals#index', :as => 'issue_changes', :via => :get
49 match '/issues/changes', :to => 'journals#index', :as => 'issue_changes', :via => :get
50 match '/issues/:id/quoted', :to => 'journals#new', :id => /\d+/, :via => :post, :as => 'quoted_issue'
50 match '/issues/:id/quoted', :to => 'journals#new', :id => /\d+/, :via => :post, :as => 'quoted_issue'
51
51
52 resources :journals, :only => [:edit, :update] do
52 resources :journals, :only => [:edit, :update] do
53 member do
53 member do
54 get 'diff'
54 get 'diff'
55 end
55 end
56 end
56 end
57
57
58 get '/projects/:project_id/issues/gantt', :to => 'gantts#show', :as => 'project_gantt'
58 get '/projects/:project_id/issues/gantt', :to => 'gantts#show', :as => 'project_gantt'
59 get '/issues/gantt', :to => 'gantts#show'
59 get '/issues/gantt', :to => 'gantts#show'
60
60
61 get '/projects/:project_id/issues/calendar', :to => 'calendars#show', :as => 'project_calendar'
61 get '/projects/:project_id/issues/calendar', :to => 'calendars#show', :as => 'project_calendar'
62 get '/issues/calendar', :to => 'calendars#show'
62 get '/issues/calendar', :to => 'calendars#show'
63
63
64 get 'projects/:id/issues/report', :to => 'reports#issue_report', :as => 'project_issues_report'
64 get 'projects/:id/issues/report', :to => 'reports#issue_report', :as => 'project_issues_report'
65 get 'projects/:id/issues/report/:detail', :to => 'reports#issue_report_details', :as => 'project_issues_report_details'
65 get 'projects/:id/issues/report/:detail', :to => 'reports#issue_report_details', :as => 'project_issues_report_details'
66
66
67 get '/issues/imports/new', :to => 'imports#new', :as => 'new_issues_import'
67 get '/issues/imports/new', :to => 'imports#new', :as => 'new_issues_import'
68 post '/imports', :to => 'imports#create', :as => 'imports'
68 post '/imports', :to => 'imports#create', :as => 'imports'
69 get '/imports/:id', :to => 'imports#show', :as => 'import'
69 get '/imports/:id', :to => 'imports#show', :as => 'import'
70 match '/imports/:id/settings', :to => 'imports#settings', :via => [:get, :post], :as => 'import_settings'
70 match '/imports/:id/settings', :to => 'imports#settings', :via => [:get, :post], :as => 'import_settings'
71 match '/imports/:id/mapping', :to => 'imports#mapping', :via => [:get, :post], :as => 'import_mapping'
71 match '/imports/:id/mapping', :to => 'imports#mapping', :via => [:get, :post], :as => 'import_mapping'
72 match '/imports/:id/run', :to => 'imports#run', :via => [:get, :post], :as => 'import_run'
72 match '/imports/:id/run', :to => 'imports#run', :via => [:get, :post], :as => 'import_run'
73
73
74 match 'my/account', :controller => 'my', :action => 'account', :via => [:get, :post]
74 match 'my/account', :controller => 'my', :action => 'account', :via => [:get, :post]
75 match 'my/account/destroy', :controller => 'my', :action => 'destroy', :via => [:get, :post]
75 match 'my/account/destroy', :controller => 'my', :action => 'destroy', :via => [:get, :post]
76 match 'my/page', :controller => 'my', :action => 'page', :via => :get
76 match 'my/page', :controller => 'my', :action => 'page', :via => :get
77 match 'my', :controller => 'my', :action => 'index', :via => :get # Redirects to my/page
77 match 'my', :controller => 'my', :action => 'index', :via => :get # Redirects to my/page
78 get 'my/api_key', :to => 'my#show_api_key', :as => 'my_api_key'
78 get 'my/api_key', :to => 'my#show_api_key', :as => 'my_api_key'
79 post 'my/api_key', :to => 'my#reset_api_key'
79 post 'my/api_key', :to => 'my#reset_api_key'
80 post 'my/rss_key', :to => 'my#reset_rss_key', :as => 'my_rss_key'
80 post 'my/rss_key', :to => 'my#reset_rss_key', :as => 'my_rss_key'
81 match 'my/password', :controller => 'my', :action => 'password', :via => [:get, :post]
81 match 'my/password', :controller => 'my', :action => 'password', :via => [:get, :post]
82 match 'my/page_layout', :controller => 'my', :action => 'page_layout', :via => :get
82 match 'my/page_layout', :controller => 'my', :action => 'page_layout', :via => :get
83 match 'my/add_block', :controller => 'my', :action => 'add_block', :via => :post
83 match 'my/add_block', :controller => 'my', :action => 'add_block', :via => :post
84 match 'my/remove_block', :controller => 'my', :action => 'remove_block', :via => :post
84 match 'my/remove_block', :controller => 'my', :action => 'remove_block', :via => :post
85 match 'my/order_blocks', :controller => 'my', :action => 'order_blocks', :via => :post
85 match 'my/order_blocks', :controller => 'my', :action => 'order_blocks', :via => :post
86
86
87 resources :users do
87 resources :users do
88 resources :memberships, :controller => 'principal_memberships'
88 resources :memberships, :controller => 'principal_memberships'
89 resources :email_addresses, :only => [:index, :create, :update, :destroy]
89 resources :email_addresses, :only => [:index, :create, :update, :destroy]
90 end
90 end
91
91
92 post 'watchers/watch', :to => 'watchers#watch', :as => 'watch'
92 post 'watchers/watch', :to => 'watchers#watch', :as => 'watch'
93 delete 'watchers/watch', :to => 'watchers#unwatch'
93 delete 'watchers/watch', :to => 'watchers#unwatch'
94 get 'watchers/new', :to => 'watchers#new', :as => 'new_watchers'
94 get 'watchers/new', :to => 'watchers#new', :as => 'new_watchers'
95 post 'watchers', :to => 'watchers#create'
95 post 'watchers', :to => 'watchers#create'
96 post 'watchers/append', :to => 'watchers#append'
96 post 'watchers/append', :to => 'watchers#append'
97 delete 'watchers', :to => 'watchers#destroy'
97 delete 'watchers', :to => 'watchers#destroy'
98 get 'watchers/autocomplete_for_user', :to => 'watchers#autocomplete_for_user'
98 get 'watchers/autocomplete_for_user', :to => 'watchers#autocomplete_for_user'
99 # Specific routes for issue watchers API
99 # Specific routes for issue watchers API
100 post 'issues/:object_id/watchers', :to => 'watchers#create', :object_type => 'issue'
100 post 'issues/:object_id/watchers', :to => 'watchers#create', :object_type => 'issue'
101 delete 'issues/:object_id/watchers/:user_id' => 'watchers#destroy', :object_type => 'issue'
101 delete 'issues/:object_id/watchers/:user_id' => 'watchers#destroy', :object_type => 'issue'
102
102
103 resources :projects do
103 resources :projects do
104 member do
104 member do
105 get 'settings(/:tab)', :action => 'settings', :as => 'settings'
105 get 'settings(/:tab)', :action => 'settings', :as => 'settings'
106 post 'modules'
106 post 'modules'
107 post 'archive'
107 post 'archive'
108 post 'unarchive'
108 post 'unarchive'
109 post 'close'
109 post 'close'
110 post 'reopen'
110 post 'reopen'
111 match 'copy', :via => [:get, :post]
111 match 'copy', :via => [:get, :post]
112 end
112 end
113
113
114 shallow do
114 shallow do
115 resources :memberships, :controller => 'members', :only => [:index, :show, :new, :create, :update, :destroy] do
115 resources :memberships, :controller => 'members', :only => [:index, :show, :new, :create, :update, :destroy] do
116 collection do
116 collection do
117 get 'autocomplete'
117 get 'autocomplete'
118 end
118 end
119 end
119 end
120 end
120 end
121
121
122 resource :enumerations, :controller => 'project_enumerations', :only => [:update, :destroy]
122 resource :enumerations, :controller => 'project_enumerations', :only => [:update, :destroy]
123
123
124 get 'issues/:copy_from/copy', :to => 'issues#new', :as => 'copy_issue'
124 get 'issues/:copy_from/copy', :to => 'issues#new', :as => 'copy_issue'
125 resources :issues, :only => [:index, :new, :create]
125 resources :issues, :only => [:index, :new, :create]
126 # Used when updating the form of a new issue
126 # Used when updating the form of a new issue
127 post 'issues/new', :to => 'issues#new'
127 post 'issues/new', :to => 'issues#new'
128
128
129 resources :files, :only => [:index, :new, :create]
129 resources :files, :only => [:index, :new, :create]
130
130
131 resources :versions, :except => [:index, :show, :edit, :update, :destroy] do
131 resources :versions, :except => [:index, :show, :edit, :update, :destroy] do
132 collection do
132 collection do
133 put 'close_completed'
133 put 'close_completed'
134 end
134 end
135 end
135 end
136 get 'versions.:format', :to => 'versions#index'
136 get 'versions.:format', :to => 'versions#index'
137 get 'roadmap', :to => 'versions#index', :format => false
137 get 'roadmap', :to => 'versions#index', :format => false
138 get 'versions', :to => 'versions#index'
138 get 'versions', :to => 'versions#index'
139
139
140 resources :news, :except => [:show, :edit, :update, :destroy]
140 resources :news, :except => [:show, :edit, :update, :destroy]
141 resources :time_entries, :controller => 'timelog', :except => [:show, :edit, :update, :destroy] do
141 resources :time_entries, :controller => 'timelog', :except => [:show, :edit, :update, :destroy] do
142 get 'report', :on => :collection
142 get 'report', :on => :collection
143 end
143 end
144 resources :queries, :only => [:new, :create]
144 resources :queries, :only => [:new, :create]
145 shallow do
145 shallow do
146 resources :issue_categories
146 resources :issue_categories
147 end
147 end
148 resources :documents, :except => [:show, :edit, :update, :destroy]
148 resources :documents, :except => [:show, :edit, :update, :destroy]
149 resources :boards
149 resources :boards
150 shallow do
150 shallow do
151 resources :repositories, :except => [:index, :show] do
151 resources :repositories, :except => [:index, :show] do
152 member do
152 member do
153 match 'committers', :via => [:get, :post]
153 match 'committers', :via => [:get, :post]
154 end
154 end
155 end
155 end
156 end
156 end
157
157
158 match 'wiki/index', :controller => 'wiki', :action => 'index', :via => :get
158 match 'wiki/index', :controller => 'wiki', :action => 'index', :via => :get
159 resources :wiki, :except => [:index, :create], :as => 'wiki_page' do
159 resources :wiki, :except => [:index, :create], :as => 'wiki_page' do
160 member do
160 member do
161 get 'rename'
161 get 'rename'
162 post 'rename'
162 post 'rename'
163 get 'history'
163 get 'history'
164 get 'diff'
164 get 'diff'
165 match 'preview', :via => [:post, :put, :patch]
165 match 'preview', :via => [:post, :put, :patch]
166 post 'protect'
166 post 'protect'
167 post 'add_attachment'
167 post 'add_attachment'
168 end
168 end
169 collection do
169 collection do
170 get 'export'
170 get 'export'
171 get 'date_index'
171 get 'date_index'
172 post 'new'
172 post 'new'
173 end
173 end
174 end
174 end
175 match 'wiki', :controller => 'wiki', :action => 'show', :via => :get
175 match 'wiki', :controller => 'wiki', :action => 'show', :via => :get
176 get 'wiki/:id/:version', :to => 'wiki#show', :constraints => {:version => /\d+/}
176 get 'wiki/:id/:version', :to => 'wiki#show', :constraints => {:version => /\d+/}
177 delete 'wiki/:id/:version', :to => 'wiki#destroy_version'
177 delete 'wiki/:id/:version', :to => 'wiki#destroy_version'
178 get 'wiki/:id/:version/annotate', :to => 'wiki#annotate'
178 get 'wiki/:id/:version/annotate', :to => 'wiki#annotate'
179 get 'wiki/:id/:version/diff', :to => 'wiki#diff'
179 get 'wiki/:id/:version/diff', :to => 'wiki#diff'
180 end
180 end
181
181
182 resources :issues do
182 resources :issues do
183 member do
183 member do
184 # Used when updating the form of an existing issue
184 # Used when updating the form of an existing issue
185 patch 'edit', :to => 'issues#edit'
185 patch 'edit', :to => 'issues#edit'
186 end
186 end
187 collection do
187 collection do
188 match 'bulk_edit', :via => [:get, :post]
188 match 'bulk_edit', :via => [:get, :post]
189 post 'bulk_update'
189 post 'bulk_update'
190 end
190 end
191 resources :time_entries, :controller => 'timelog', :except => [:show, :edit, :update, :destroy] do
191 resources :time_entries, :controller => 'timelog', :only => [:new, :create]
192 collection do
193 get 'report'
194 end
195 end
196 shallow do
192 shallow do
197 resources :relations, :controller => 'issue_relations', :only => [:index, :show, :create, :destroy]
193 resources :relations, :controller => 'issue_relations', :only => [:index, :show, :create, :destroy]
198 end
194 end
199 end
195 end
200 # Used when updating the form of a new issue outside a project
196 # Used when updating the form of a new issue outside a project
201 post '/issues/new', :to => 'issues#new'
197 post '/issues/new', :to => 'issues#new'
202 match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete
198 match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete
203
199
204 resources :queries, :except => [:show]
200 resources :queries, :except => [:show]
205
201
206 resources :news, :only => [:index, :show, :edit, :update, :destroy]
202 resources :news, :only => [:index, :show, :edit, :update, :destroy]
207 match '/news/:id/comments', :to => 'comments#create', :via => :post
203 match '/news/:id/comments', :to => 'comments#create', :via => :post
208 match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete
204 match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete
209
205
210 resources :versions, :only => [:show, :edit, :update, :destroy] do
206 resources :versions, :only => [:show, :edit, :update, :destroy] do
211 post 'status_by', :on => :member
207 post 'status_by', :on => :member
212 end
208 end
213
209
214 resources :documents, :only => [:show, :edit, :update, :destroy] do
210 resources :documents, :only => [:show, :edit, :update, :destroy] do
215 post 'add_attachment', :on => :member
211 post 'add_attachment', :on => :member
216 end
212 end
217
213
218 match '/time_entries/context_menu', :to => 'context_menus#time_entries', :as => :time_entries_context_menu, :via => [:get, :post]
214 match '/time_entries/context_menu', :to => 'context_menus#time_entries', :as => :time_entries_context_menu, :via => [:get, :post]
219
215
220 resources :time_entries, :controller => 'timelog', :except => :destroy do
216 resources :time_entries, :controller => 'timelog', :except => :destroy do
221 collection do
217 collection do
222 get 'report'
218 get 'report'
223 get 'bulk_edit'
219 get 'bulk_edit'
224 post 'bulk_update'
220 post 'bulk_update'
225 end
221 end
226 end
222 end
227 match '/time_entries/:id', :to => 'timelog#destroy', :via => :delete, :id => /\d+/
223 match '/time_entries/:id', :to => 'timelog#destroy', :via => :delete, :id => /\d+/
228 # TODO: delete /time_entries for bulk deletion
224 # TODO: delete /time_entries for bulk deletion
229 match '/time_entries/destroy', :to => 'timelog#destroy', :via => :delete
225 match '/time_entries/destroy', :to => 'timelog#destroy', :via => :delete
230 # Used to update the new time entry form
226 # Used to update the new time entry form
231 post '/time_entries/new', :to => 'timelog#new'
227 post '/time_entries/new', :to => 'timelog#new'
232
228
233 get 'projects/:id/activity', :to => 'activities#index', :as => :project_activity
229 get 'projects/:id/activity', :to => 'activities#index', :as => :project_activity
234 get 'activity', :to => 'activities#index'
230 get 'activity', :to => 'activities#index'
235
231
236 # repositories routes
232 # repositories routes
237 get 'projects/:id/repository/:repository_id/statistics', :to => 'repositories#stats'
233 get 'projects/:id/repository/:repository_id/statistics', :to => 'repositories#stats'
238 get 'projects/:id/repository/:repository_id/graph', :to => 'repositories#graph'
234 get 'projects/:id/repository/:repository_id/graph', :to => 'repositories#graph'
239
235
240 get 'projects/:id/repository/:repository_id/changes(/*path)',
236 get 'projects/:id/repository/:repository_id/changes(/*path)',
241 :to => 'repositories#changes',
237 :to => 'repositories#changes',
242 :format => false
238 :format => false
243
239
244 get 'projects/:id/repository/:repository_id/revisions/:rev', :to => 'repositories#revision'
240 get 'projects/:id/repository/:repository_id/revisions/:rev', :to => 'repositories#revision'
245 get 'projects/:id/repository/:repository_id/revision', :to => 'repositories#revision'
241 get 'projects/:id/repository/:repository_id/revision', :to => 'repositories#revision'
246 post 'projects/:id/repository/:repository_id/revisions/:rev/issues', :to => 'repositories#add_related_issue'
242 post 'projects/:id/repository/:repository_id/revisions/:rev/issues', :to => 'repositories#add_related_issue'
247 delete 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
243 delete 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
248 get 'projects/:id/repository/:repository_id/revisions', :to => 'repositories#revisions'
244 get 'projects/:id/repository/:repository_id/revisions', :to => 'repositories#revisions'
249 %w(browse show entry raw annotate diff).each do |action|
245 %w(browse show entry raw annotate diff).each do |action|
250 get "projects/:id/repository/:repository_id/revisions/:rev/#{action}(/*path)",
246 get "projects/:id/repository/:repository_id/revisions/:rev/#{action}(/*path)",
251 :controller => 'repositories',
247 :controller => 'repositories',
252 :action => action,
248 :action => action,
253 :format => false,
249 :format => false,
254 :constraints => {:rev => /[a-z0-9\.\-_]+/}
250 :constraints => {:rev => /[a-z0-9\.\-_]+/}
255 end
251 end
256
252
257 get 'projects/:id/repository/statistics', :to => 'repositories#stats'
253 get 'projects/:id/repository/statistics', :to => 'repositories#stats'
258 get 'projects/:id/repository/graph', :to => 'repositories#graph'
254 get 'projects/:id/repository/graph', :to => 'repositories#graph'
259
255
260 get 'projects/:id/repository/changes(/*path)',
256 get 'projects/:id/repository/changes(/*path)',
261 :to => 'repositories#changes',
257 :to => 'repositories#changes',
262 :format => false
258 :format => false
263
259
264 get 'projects/:id/repository/revisions', :to => 'repositories#revisions'
260 get 'projects/:id/repository/revisions', :to => 'repositories#revisions'
265 get 'projects/:id/repository/revisions/:rev', :to => 'repositories#revision'
261 get 'projects/:id/repository/revisions/:rev', :to => 'repositories#revision'
266 get 'projects/:id/repository/revision', :to => 'repositories#revision'
262 get 'projects/:id/repository/revision', :to => 'repositories#revision'
267 post 'projects/:id/repository/revisions/:rev/issues', :to => 'repositories#add_related_issue'
263 post 'projects/:id/repository/revisions/:rev/issues', :to => 'repositories#add_related_issue'
268 delete 'projects/:id/repository/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
264 delete 'projects/:id/repository/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
269 %w(browse show entry raw annotate diff).each do |action|
265 %w(browse show entry raw annotate diff).each do |action|
270 get "projects/:id/repository/revisions/:rev/#{action}(/*path)",
266 get "projects/:id/repository/revisions/:rev/#{action}(/*path)",
271 :controller => 'repositories',
267 :controller => 'repositories',
272 :action => action,
268 :action => action,
273 :format => false,
269 :format => false,
274 :constraints => {:rev => /[a-z0-9\.\-_]+/}
270 :constraints => {:rev => /[a-z0-9\.\-_]+/}
275 end
271 end
276 %w(browse entry raw changes annotate diff).each do |action|
272 %w(browse entry raw changes annotate diff).each do |action|
277 get "projects/:id/repository/:repository_id/#{action}(/*path)",
273 get "projects/:id/repository/:repository_id/#{action}(/*path)",
278 :controller => 'repositories',
274 :controller => 'repositories',
279 :action => action,
275 :action => action,
280 :format => false
276 :format => false
281 end
277 end
282 %w(browse entry raw changes annotate diff).each do |action|
278 %w(browse entry raw changes annotate diff).each do |action|
283 get "projects/:id/repository/#{action}(/*path)",
279 get "projects/:id/repository/#{action}(/*path)",
284 :controller => 'repositories',
280 :controller => 'repositories',
285 :action => action,
281 :action => action,
286 :format => false
282 :format => false
287 end
283 end
288
284
289 get 'projects/:id/repository/:repository_id/show/*path', :to => 'repositories#show', :format => false
285 get 'projects/:id/repository/:repository_id/show/*path', :to => 'repositories#show', :format => false
290 get 'projects/:id/repository/show/*path', :to => 'repositories#show', :format => false
286 get 'projects/:id/repository/show/*path', :to => 'repositories#show', :format => false
291
287
292 get 'projects/:id/repository/:repository_id', :to => 'repositories#show', :path => nil
288 get 'projects/:id/repository/:repository_id', :to => 'repositories#show', :path => nil
293 get 'projects/:id/repository', :to => 'repositories#show', :path => nil
289 get 'projects/:id/repository', :to => 'repositories#show', :path => nil
294
290
295 # additional routes for having the file name at the end of url
291 # additional routes for having the file name at the end of url
296 get 'attachments/:id/:filename', :to => 'attachments#show', :id => /\d+/, :filename => /.*/, :as => 'named_attachment'
292 get 'attachments/:id/:filename', :to => 'attachments#show', :id => /\d+/, :filename => /.*/, :as => 'named_attachment'
297 get 'attachments/download/:id/:filename', :to => 'attachments#download', :id => /\d+/, :filename => /.*/, :as => 'download_named_attachment'
293 get 'attachments/download/:id/:filename', :to => 'attachments#download', :id => /\d+/, :filename => /.*/, :as => 'download_named_attachment'
298 get 'attachments/download/:id', :to => 'attachments#download', :id => /\d+/
294 get 'attachments/download/:id', :to => 'attachments#download', :id => /\d+/
299 get 'attachments/thumbnail/:id(/:size)', :to => 'attachments#thumbnail', :id => /\d+/, :size => /\d+/, :as => 'thumbnail'
295 get 'attachments/thumbnail/:id(/:size)', :to => 'attachments#thumbnail', :id => /\d+/, :size => /\d+/, :as => 'thumbnail'
300 resources :attachments, :only => [:show, :destroy]
296 resources :attachments, :only => [:show, :destroy]
301 get 'attachments/:object_type/:object_id/edit', :to => 'attachments#edit', :as => :object_attachments_edit
297 get 'attachments/:object_type/:object_id/edit', :to => 'attachments#edit', :as => :object_attachments_edit
302 patch 'attachments/:object_type/:object_id', :to => 'attachments#update', :as => :object_attachments
298 patch 'attachments/:object_type/:object_id', :to => 'attachments#update', :as => :object_attachments
303
299
304 resources :groups do
300 resources :groups do
305 resources :memberships, :controller => 'principal_memberships'
301 resources :memberships, :controller => 'principal_memberships'
306 member do
302 member do
307 get 'autocomplete_for_user'
303 get 'autocomplete_for_user'
308 end
304 end
309 end
305 end
310
306
311 get 'groups/:id/users/new', :to => 'groups#new_users', :id => /\d+/, :as => 'new_group_users'
307 get 'groups/:id/users/new', :to => 'groups#new_users', :id => /\d+/, :as => 'new_group_users'
312 post 'groups/:id/users', :to => 'groups#add_users', :id => /\d+/, :as => 'group_users'
308 post 'groups/:id/users', :to => 'groups#add_users', :id => /\d+/, :as => 'group_users'
313 delete 'groups/:id/users/:user_id', :to => 'groups#remove_user', :id => /\d+/, :as => 'group_user'
309 delete 'groups/:id/users/:user_id', :to => 'groups#remove_user', :id => /\d+/, :as => 'group_user'
314
310
315 resources :trackers, :except => :show do
311 resources :trackers, :except => :show do
316 collection do
312 collection do
317 match 'fields', :via => [:get, :post]
313 match 'fields', :via => [:get, :post]
318 end
314 end
319 end
315 end
320 resources :issue_statuses, :except => :show do
316 resources :issue_statuses, :except => :show do
321 collection do
317 collection do
322 post 'update_issue_done_ratio'
318 post 'update_issue_done_ratio'
323 end
319 end
324 end
320 end
325 resources :custom_fields, :except => :show do
321 resources :custom_fields, :except => :show do
326 resources :enumerations, :controller => 'custom_field_enumerations', :except => [:show, :new, :edit]
322 resources :enumerations, :controller => 'custom_field_enumerations', :except => [:show, :new, :edit]
327 put 'enumerations', :to => 'custom_field_enumerations#update_each'
323 put 'enumerations', :to => 'custom_field_enumerations#update_each'
328 end
324 end
329 resources :roles do
325 resources :roles do
330 collection do
326 collection do
331 match 'permissions', :via => [:get, :post]
327 match 'permissions', :via => [:get, :post]
332 end
328 end
333 end
329 end
334 resources :enumerations, :except => :show
330 resources :enumerations, :except => :show
335 match 'enumerations/:type', :to => 'enumerations#index', :via => :get
331 match 'enumerations/:type', :to => 'enumerations#index', :via => :get
336
332
337 get 'projects/:id/search', :controller => 'search', :action => 'index'
333 get 'projects/:id/search', :controller => 'search', :action => 'index'
338 get 'search', :controller => 'search', :action => 'index'
334 get 'search', :controller => 'search', :action => 'index'
339
335
340
336
341 get 'mail_handler', :to => 'mail_handler#new'
337 get 'mail_handler', :to => 'mail_handler#new'
342 post 'mail_handler', :to => 'mail_handler#index'
338 post 'mail_handler', :to => 'mail_handler#index'
343
339
344 get 'admin', :to => 'admin#index'
340 get 'admin', :to => 'admin#index'
345 get 'admin/projects', :to => 'admin#projects'
341 get 'admin/projects', :to => 'admin#projects'
346 get 'admin/plugins', :to => 'admin#plugins'
342 get 'admin/plugins', :to => 'admin#plugins'
347 get 'admin/info', :to => 'admin#info'
343 get 'admin/info', :to => 'admin#info'
348 post 'admin/test_email', :to => 'admin#test_email', :as => 'test_email'
344 post 'admin/test_email', :to => 'admin#test_email', :as => 'test_email'
349 post 'admin/default_configuration', :to => 'admin#default_configuration'
345 post 'admin/default_configuration', :to => 'admin#default_configuration'
350
346
351 resources :auth_sources do
347 resources :auth_sources do
352 member do
348 member do
353 get 'test_connection', :as => 'try_connection'
349 get 'test_connection', :as => 'try_connection'
354 end
350 end
355 collection do
351 collection do
356 get 'autocomplete_for_new_user'
352 get 'autocomplete_for_new_user'
357 end
353 end
358 end
354 end
359
355
360 match 'workflows', :controller => 'workflows', :action => 'index', :via => :get
356 match 'workflows', :controller => 'workflows', :action => 'index', :via => :get
361 match 'workflows/edit', :controller => 'workflows', :action => 'edit', :via => [:get, :post]
357 match 'workflows/edit', :controller => 'workflows', :action => 'edit', :via => [:get, :post]
362 match 'workflows/permissions', :controller => 'workflows', :action => 'permissions', :via => [:get, :post]
358 match 'workflows/permissions', :controller => 'workflows', :action => 'permissions', :via => [:get, :post]
363 match 'workflows/copy', :controller => 'workflows', :action => 'copy', :via => [:get, :post]
359 match 'workflows/copy', :controller => 'workflows', :action => 'copy', :via => [:get, :post]
364 match 'settings', :controller => 'settings', :action => 'index', :via => :get
360 match 'settings', :controller => 'settings', :action => 'index', :via => :get
365 match 'settings/edit', :controller => 'settings', :action => 'edit', :via => [:get, :post]
361 match 'settings/edit', :controller => 'settings', :action => 'edit', :via => [:get, :post]
366 match 'settings/plugin/:id', :controller => 'settings', :action => 'plugin', :via => [:get, :post], :as => 'plugin_settings'
362 match 'settings/plugin/:id', :controller => 'settings', :action => 'plugin', :via => [:get, :post], :as => 'plugin_settings'
367
363
368 match 'sys/projects', :to => 'sys#projects', :via => :get
364 match 'sys/projects', :to => 'sys#projects', :via => :get
369 match 'sys/projects/:id/repository', :to => 'sys#create_project_repository', :via => :post
365 match 'sys/projects/:id/repository', :to => 'sys#create_project_repository', :via => :post
370 match 'sys/fetch_changesets', :to => 'sys#fetch_changesets', :via => [:get, :post]
366 match 'sys/fetch_changesets', :to => 'sys#fetch_changesets', :via => [:get, :post]
371
367
372 match 'uploads', :to => 'attachments#upload', :via => :post
368 match 'uploads', :to => 'attachments#upload', :via => :post
373
369
374 get 'robots.txt', :to => 'welcome#robots'
370 get 'robots.txt', :to => 'welcome#robots'
375
371
376 Dir.glob File.expand_path("plugins/*", Rails.root) do |plugin_dir|
372 Dir.glob File.expand_path("plugins/*", Rails.root) do |plugin_dir|
377 file = File.join(plugin_dir, "config/routes.rb")
373 file = File.join(plugin_dir, "config/routes.rb")
378 if File.exists?(file)
374 if File.exists?(file)
379 begin
375 begin
380 instance_eval File.read(file)
376 instance_eval File.read(file)
381 rescue Exception => e
377 rescue Exception => e
382 puts "An error occurred while loading the routes definition of #{File.basename(plugin_dir)} plugin (#{file}): #{e.message}."
378 puts "An error occurred while loading the routes definition of #{File.basename(plugin_dir)} plugin (#{file}): #{e.message}."
383 exit 1
379 exit 1
384 end
380 end
385 end
381 end
386 end
382 end
387 end
383 end
@@ -1,354 +1,345
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # Redmine - project management software
2 # Redmine - project management software
3 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 # Copyright (C) 2006-2016 Jean-Philippe Lang
4 #
4 #
5 # This program is free software; you can redistribute it and/or
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
8 # of the License, or (at your option) any later version.
9 #
9 #
10 # This program is distributed in the hope that it will be useful,
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
13 # GNU General Public License for more details.
14 #
14 #
15 # You should have received a copy of the GNU General Public License
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
18
19 require File.expand_path('../../test_helper', __FILE__)
19 require File.expand_path('../../test_helper', __FILE__)
20
20
21 class TimeEntryReportsControllerTest < ActionController::TestCase
21 class TimeEntryReportsControllerTest < ActionController::TestCase
22 tests TimelogController
22 tests TimelogController
23
23
24 fixtures :projects, :enabled_modules, :roles, :members, :member_roles,
24 fixtures :projects, :enabled_modules, :roles, :members, :member_roles,
25 :email_addresses,
25 :email_addresses,
26 :issues, :time_entries, :users, :trackers, :enumerations,
26 :issues, :time_entries, :users, :trackers, :enumerations,
27 :issue_statuses, :custom_fields, :custom_values,
27 :issue_statuses, :custom_fields, :custom_values,
28 :projects_trackers, :custom_fields_trackers,
28 :projects_trackers, :custom_fields_trackers,
29 :custom_fields_projects
29 :custom_fields_projects
30
30
31 include Redmine::I18n
31 include Redmine::I18n
32
32
33 def setup
33 def setup
34 Setting.default_language = "en"
34 Setting.default_language = "en"
35 end
35 end
36
36
37 def test_report_at_project_level
37 def test_report_at_project_level
38 get :report, :project_id => 'ecookbook'
38 get :report, :project_id => 'ecookbook'
39 assert_response :success
39 assert_response :success
40 assert_template 'report'
40 assert_template 'report'
41 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries/report'
41 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries/report'
42 end
42 end
43
43
44 def test_report_all_projects
44 def test_report_all_projects
45 get :report
45 get :report
46 assert_response :success
46 assert_response :success
47 assert_template 'report'
47 assert_template 'report'
48 assert_select 'form#query_form[action=?]', '/time_entries/report'
48 assert_select 'form#query_form[action=?]', '/time_entries/report'
49 end
49 end
50
50
51 def test_report_all_projects_denied
51 def test_report_all_projects_denied
52 r = Role.anonymous
52 r = Role.anonymous
53 r.permissions.delete(:view_time_entries)
53 r.permissions.delete(:view_time_entries)
54 r.permissions_will_change!
54 r.permissions_will_change!
55 r.save
55 r.save
56 get :report
56 get :report
57 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Ftime_entries%2Freport'
57 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Ftime_entries%2Freport'
58 end
58 end
59
59
60 def test_report_all_projects_one_criteria
60 def test_report_all_projects_one_criteria
61 get :report, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criteria => ['project']
61 get :report, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criteria => ['project']
62 assert_response :success
62 assert_response :success
63 assert_template 'report'
63 assert_template 'report'
64 assert_not_nil assigns(:report)
64 assert_not_nil assigns(:report)
65 assert_equal "8.65", "%.2f" % assigns(:report).total_hours
65 assert_equal "8.65", "%.2f" % assigns(:report).total_hours
66 end
66 end
67
67
68 def test_report_all_time
68 def test_report_all_time
69 get :report, :project_id => 1, :criteria => ['project', 'issue']
69 get :report, :project_id => 1, :criteria => ['project', 'issue']
70 assert_response :success
70 assert_response :success
71 assert_template 'report'
71 assert_template 'report'
72 assert_not_nil assigns(:report)
72 assert_not_nil assigns(:report)
73 assert_equal "162.90", "%.2f" % assigns(:report).total_hours
73 assert_equal "162.90", "%.2f" % assigns(:report).total_hours
74 end
74 end
75
75
76 def test_report_all_time_by_day
76 def test_report_all_time_by_day
77 get :report, :project_id => 1, :criteria => ['project', 'issue'], :columns => 'day'
77 get :report, :project_id => 1, :criteria => ['project', 'issue'], :columns => 'day'
78 assert_response :success
78 assert_response :success
79 assert_template 'report'
79 assert_template 'report'
80 assert_not_nil assigns(:report)
80 assert_not_nil assigns(:report)
81 assert_equal "162.90", "%.2f" % assigns(:report).total_hours
81 assert_equal "162.90", "%.2f" % assigns(:report).total_hours
82 assert_select 'th', :text => '2007-03-12'
82 assert_select 'th', :text => '2007-03-12'
83 end
83 end
84
84
85 def test_report_one_criteria
85 def test_report_one_criteria
86 get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criteria => ['project']
86 get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criteria => ['project']
87 assert_response :success
87 assert_response :success
88 assert_template 'report'
88 assert_template 'report'
89 assert_not_nil assigns(:report)
89 assert_not_nil assigns(:report)
90 assert_equal "8.65", "%.2f" % assigns(:report).total_hours
90 assert_equal "8.65", "%.2f" % assigns(:report).total_hours
91 end
91 end
92
92
93 def test_report_two_criteria
93 def test_report_two_criteria
94 get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criteria => ["user", "activity"]
94 get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criteria => ["user", "activity"]
95 assert_response :success
95 assert_response :success
96 assert_template 'report'
96 assert_template 'report'
97 assert_not_nil assigns(:report)
97 assert_not_nil assigns(:report)
98 assert_equal "162.90", "%.2f" % assigns(:report).total_hours
98 assert_equal "162.90", "%.2f" % assigns(:report).total_hours
99 end
99 end
100
100
101 def test_report_custom_field_criteria_with_multiple_values_on_single_value_custom_field_should_not_fail
101 def test_report_custom_field_criteria_with_multiple_values_on_single_value_custom_field_should_not_fail
102 field = TimeEntryCustomField.create!(:name => 'multi', :field_format => 'list', :possible_values => ['value1', 'value2'])
102 field = TimeEntryCustomField.create!(:name => 'multi', :field_format => 'list', :possible_values => ['value1', 'value2'])
103 entry = TimeEntry.create!(:project => Project.find(1), :hours => 1, :activity_id => 10, :user => User.find(2), :spent_on => Date.today)
103 entry = TimeEntry.create!(:project => Project.find(1), :hours => 1, :activity_id => 10, :user => User.find(2), :spent_on => Date.today)
104 CustomValue.create!(:customized => entry, :custom_field => field, :value => 'value1')
104 CustomValue.create!(:customized => entry, :custom_field => field, :value => 'value1')
105 CustomValue.create!(:customized => entry, :custom_field => field, :value => 'value2')
105 CustomValue.create!(:customized => entry, :custom_field => field, :value => 'value2')
106
106
107 get :report, :project_id => 1, :columns => 'day', :criteria => ["cf_#{field.id}"]
107 get :report, :project_id => 1, :columns => 'day', :criteria => ["cf_#{field.id}"]
108 assert_response :success
108 assert_response :success
109 end
109 end
110
110
111 def test_report_multiple_values_custom_fields_should_not_be_proposed
111 def test_report_multiple_values_custom_fields_should_not_be_proposed
112 TimeEntryCustomField.create!(:name => 'Single', :field_format => 'list', :possible_values => ['value1', 'value2'])
112 TimeEntryCustomField.create!(:name => 'Single', :field_format => 'list', :possible_values => ['value1', 'value2'])
113 TimeEntryCustomField.create!(:name => 'Multi', :field_format => 'list', :multiple => true, :possible_values => ['value1', 'value2'])
113 TimeEntryCustomField.create!(:name => 'Multi', :field_format => 'list', :multiple => true, :possible_values => ['value1', 'value2'])
114
114
115 get :report, :project_id => 1
115 get :report, :project_id => 1
116 assert_response :success
116 assert_response :success
117 assert_select 'select[name=?]', 'criteria[]' do
117 assert_select 'select[name=?]', 'criteria[]' do
118 assert_select 'option', :text => 'Single'
118 assert_select 'option', :text => 'Single'
119 assert_select 'option', :text => 'Multi', :count => 0
119 assert_select 'option', :text => 'Multi', :count => 0
120 end
120 end
121 end
121 end
122
122
123 def test_report_one_day
123 def test_report_one_day
124 get :report, :project_id => 1, :columns => 'day', :from => "2007-03-23", :to => "2007-03-23", :criteria => ["user", "activity"]
124 get :report, :project_id => 1, :columns => 'day', :from => "2007-03-23", :to => "2007-03-23", :criteria => ["user", "activity"]
125 assert_response :success
125 assert_response :success
126 assert_template 'report'
126 assert_template 'report'
127 assert_not_nil assigns(:report)
127 assert_not_nil assigns(:report)
128 assert_equal "4.25", "%.2f" % assigns(:report).total_hours
128 assert_equal "4.25", "%.2f" % assigns(:report).total_hours
129 end
129 end
130
130
131 def test_report_at_issue_level
132 get :report, :issue_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criteria => ["user", "activity"]
133 assert_response :success
134 assert_template 'report'
135 assert_not_nil assigns(:report)
136 assert_equal "154.25", "%.2f" % assigns(:report).total_hours
137 assert_select 'form#query_form[action=?]', '/issues/1/time_entries/report'
138 end
139
140 def test_report_by_week_should_use_commercial_year
131 def test_report_by_week_should_use_commercial_year
141 TimeEntry.delete_all
132 TimeEntry.delete_all
142 TimeEntry.generate!(:hours => '2', :spent_on => '2009-12-25') # 2009-52
133 TimeEntry.generate!(:hours => '2', :spent_on => '2009-12-25') # 2009-52
143 TimeEntry.generate!(:hours => '4', :spent_on => '2009-12-31') # 2009-53
134 TimeEntry.generate!(:hours => '4', :spent_on => '2009-12-31') # 2009-53
144 TimeEntry.generate!(:hours => '8', :spent_on => '2010-01-01') # 2009-53
135 TimeEntry.generate!(:hours => '8', :spent_on => '2010-01-01') # 2009-53
145 TimeEntry.generate!(:hours => '16', :spent_on => '2010-01-05') # 2010-1
136 TimeEntry.generate!(:hours => '16', :spent_on => '2010-01-05') # 2010-1
146
137
147 get :report, :columns => 'week', :from => "2009-12-25", :to => "2010-01-05", :criteria => ["project"]
138 get :report, :columns => 'week', :from => "2009-12-25", :to => "2010-01-05", :criteria => ["project"]
148 assert_response :success
139 assert_response :success
149
140
150 assert_select '#time-report thead tr' do
141 assert_select '#time-report thead tr' do
151 assert_select 'th:nth-child(1)', :text => 'Project'
142 assert_select 'th:nth-child(1)', :text => 'Project'
152 assert_select 'th:nth-child(2)', :text => '2009-52'
143 assert_select 'th:nth-child(2)', :text => '2009-52'
153 assert_select 'th:nth-child(3)', :text => '2009-53'
144 assert_select 'th:nth-child(3)', :text => '2009-53'
154 assert_select 'th:nth-child(4)', :text => '2010-1'
145 assert_select 'th:nth-child(4)', :text => '2010-1'
155 assert_select 'th:nth-child(5)', :text => 'Total time'
146 assert_select 'th:nth-child(5)', :text => 'Total time'
156 end
147 end
157 assert_select '#time-report tbody tr' do
148 assert_select '#time-report tbody tr' do
158 assert_select 'td:nth-child(1)', :text => 'eCookbook'
149 assert_select 'td:nth-child(1)', :text => 'eCookbook'
159 assert_select 'td:nth-child(2)', :text => '2.00'
150 assert_select 'td:nth-child(2)', :text => '2.00'
160 assert_select 'td:nth-child(3)', :text => '12.00'
151 assert_select 'td:nth-child(3)', :text => '12.00'
161 assert_select 'td:nth-child(4)', :text => '16.00'
152 assert_select 'td:nth-child(4)', :text => '16.00'
162 assert_select 'td:nth-child(5)', :text => '30.00' # Total
153 assert_select 'td:nth-child(5)', :text => '30.00' # Total
163 end
154 end
164 end
155 end
165
156
166 def test_report_should_propose_association_custom_fields
157 def test_report_should_propose_association_custom_fields
167 get :report
158 get :report
168 assert_response :success
159 assert_response :success
169 assert_template 'report'
160 assert_template 'report'
170
161
171 assert_select 'select[name=?]', 'criteria[]' do
162 assert_select 'select[name=?]', 'criteria[]' do
172 assert_select 'option[value=cf_1]', {:text => 'Database'}, 'Issue custom field not found'
163 assert_select 'option[value=cf_1]', {:text => 'Database'}, 'Issue custom field not found'
173 assert_select 'option[value=cf_3]', {:text => 'Development status'}, 'Project custom field not found'
164 assert_select 'option[value=cf_3]', {:text => 'Development status'}, 'Project custom field not found'
174 assert_select 'option[value=cf_7]', {:text => 'Billable'}, 'TimeEntryActivity custom field not found'
165 assert_select 'option[value=cf_7]', {:text => 'Billable'}, 'TimeEntryActivity custom field not found'
175 end
166 end
176 end
167 end
177
168
178 def test_report_with_association_custom_fields
169 def test_report_with_association_custom_fields
179 get :report, :criteria => ['cf_1', 'cf_3', 'cf_7']
170 get :report, :criteria => ['cf_1', 'cf_3', 'cf_7']
180 assert_response :success
171 assert_response :success
181 assert_template 'report'
172 assert_template 'report'
182 assert_not_nil assigns(:report)
173 assert_not_nil assigns(:report)
183 assert_equal 3, assigns(:report).criteria.size
174 assert_equal 3, assigns(:report).criteria.size
184 assert_equal "162.90", "%.2f" % assigns(:report).total_hours
175 assert_equal "162.90", "%.2f" % assigns(:report).total_hours
185
176
186 # Custom fields columns
177 # Custom fields columns
187 assert_select 'th', :text => 'Database'
178 assert_select 'th', :text => 'Database'
188 assert_select 'th', :text => 'Development status'
179 assert_select 'th', :text => 'Development status'
189 assert_select 'th', :text => 'Billable'
180 assert_select 'th', :text => 'Billable'
190
181
191 # Custom field row
182 # Custom field row
192 assert_select 'tr' do
183 assert_select 'tr' do
193 assert_select 'td', :text => 'MySQL'
184 assert_select 'td', :text => 'MySQL'
194 assert_select 'td.hours', :text => '1.00'
185 assert_select 'td.hours', :text => '1.00'
195 end
186 end
196 end
187 end
197
188
198 def test_report_one_criteria_no_result
189 def test_report_one_criteria_no_result
199 get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criteria => ['project']
190 get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criteria => ['project']
200 assert_response :success
191 assert_response :success
201 assert_template 'report'
192 assert_template 'report'
202 assert_not_nil assigns(:report)
193 assert_not_nil assigns(:report)
203 assert_equal "0.00", "%.2f" % assigns(:report).total_hours
194 assert_equal "0.00", "%.2f" % assigns(:report).total_hours
204 end
195 end
205
196
206 def test_report_status_criterion
197 def test_report_status_criterion
207 get :report, :project_id => 1, :criteria => ['status']
198 get :report, :project_id => 1, :criteria => ['status']
208 assert_response :success
199 assert_response :success
209 assert_template 'report'
200 assert_template 'report'
210 assert_select 'th', :text => 'Status'
201 assert_select 'th', :text => 'Status'
211 assert_select 'td', :text => 'New'
202 assert_select 'td', :text => 'New'
212 end
203 end
213
204
214 def test_report_all_projects_csv_export
205 def test_report_all_projects_csv_export
215 get :report, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30",
206 get :report, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30",
216 :criteria => ["project", "user", "activity"], :format => "csv"
207 :criteria => ["project", "user", "activity"], :format => "csv"
217 assert_response :success
208 assert_response :success
218 assert_equal 'text/csv; header=present', @response.content_type
209 assert_equal 'text/csv; header=present', @response.content_type
219 lines = @response.body.chomp.split("\n")
210 lines = @response.body.chomp.split("\n")
220 # Headers
211 # Headers
221 assert_equal 'Project,User,Activity,2007-3,2007-4,Total time', lines.first
212 assert_equal 'Project,User,Activity,2007-3,2007-4,Total time', lines.first
222 # Total row
213 # Total row
223 assert_equal 'Total time,"","",154.25,8.65,162.90', lines.last
214 assert_equal 'Total time,"","",154.25,8.65,162.90', lines.last
224 end
215 end
225
216
226 def test_report_csv_export
217 def test_report_csv_export
227 get :report, :project_id => 1, :columns => 'month',
218 get :report, :project_id => 1, :columns => 'month',
228 :from => "2007-01-01", :to => "2007-06-30",
219 :from => "2007-01-01", :to => "2007-06-30",
229 :criteria => ["project", "user", "activity"], :format => "csv"
220 :criteria => ["project", "user", "activity"], :format => "csv"
230 assert_response :success
221 assert_response :success
231 assert_equal 'text/csv; header=present', @response.content_type
222 assert_equal 'text/csv; header=present', @response.content_type
232 lines = @response.body.chomp.split("\n")
223 lines = @response.body.chomp.split("\n")
233 # Headers
224 # Headers
234 assert_equal 'Project,User,Activity,2007-3,2007-4,Total time', lines.first
225 assert_equal 'Project,User,Activity,2007-3,2007-4,Total time', lines.first
235 # Total row
226 # Total row
236 assert_equal 'Total time,"","",154.25,8.65,162.90', lines.last
227 assert_equal 'Total time,"","",154.25,8.65,162.90', lines.last
237 end
228 end
238
229
239 def test_csv_big_5
230 def test_csv_big_5
240 str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88".force_encoding('UTF-8')
231 str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88".force_encoding('UTF-8')
241 str_big5 = "\xa4@\xa4\xeb".force_encoding('Big5')
232 str_big5 = "\xa4@\xa4\xeb".force_encoding('Big5')
242 user = User.find_by_id(3)
233 user = User.find_by_id(3)
243 user.firstname = str_utf8
234 user.firstname = str_utf8
244 user.lastname = "test-lastname"
235 user.lastname = "test-lastname"
245 assert user.save
236 assert user.save
246 comments = "test_csv_big_5"
237 comments = "test_csv_big_5"
247 te1 = TimeEntry.create(:spent_on => '2011-11-11',
238 te1 = TimeEntry.create(:spent_on => '2011-11-11',
248 :hours => 7.3,
239 :hours => 7.3,
249 :project => Project.find(1),
240 :project => Project.find(1),
250 :user => user,
241 :user => user,
251 :activity => TimeEntryActivity.find_by_name('Design'),
242 :activity => TimeEntryActivity.find_by_name('Design'),
252 :comments => comments)
243 :comments => comments)
253
244
254 te2 = TimeEntry.find_by_comments(comments)
245 te2 = TimeEntry.find_by_comments(comments)
255 assert_not_nil te2
246 assert_not_nil te2
256 assert_equal 7.3, te2.hours
247 assert_equal 7.3, te2.hours
257 assert_equal 3, te2.user_id
248 assert_equal 3, te2.user_id
258
249
259 with_settings :default_language => "zh-TW" do
250 with_settings :default_language => "zh-TW" do
260 get :report, :project_id => 1, :columns => 'day',
251 get :report, :project_id => 1, :columns => 'day',
261 :from => "2011-11-11", :to => "2011-11-11",
252 :from => "2011-11-11", :to => "2011-11-11",
262 :criteria => ["user"], :format => "csv"
253 :criteria => ["user"], :format => "csv"
263 end
254 end
264 assert_response :success
255 assert_response :success
265 assert_equal 'text/csv; header=present', @response.content_type
256 assert_equal 'text/csv; header=present', @response.content_type
266 lines = @response.body.chomp.split("\n")
257 lines = @response.body.chomp.split("\n")
267 # Headers
258 # Headers
268 s1 = "\xa5\xce\xa4\xe1,2011-11-11,\xa4u\xae\xc9\xc1`\xadp".force_encoding('Big5')
259 s1 = "\xa5\xce\xa4\xe1,2011-11-11,\xa4u\xae\xc9\xc1`\xadp".force_encoding('Big5')
269 s2 = "\xa4u\xae\xc9\xc1`\xadp".force_encoding('Big5')
260 s2 = "\xa4u\xae\xc9\xc1`\xadp".force_encoding('Big5')
270 assert_equal s1, lines.first
261 assert_equal s1, lines.first
271 # Total row
262 # Total row
272 assert_equal "#{str_big5} #{user.lastname},7.30,7.30", lines[1]
263 assert_equal "#{str_big5} #{user.lastname},7.30,7.30", lines[1]
273 assert_equal "#{s2},7.30,7.30", lines[2]
264 assert_equal "#{s2},7.30,7.30", lines[2]
274
265
275 str_tw = "Traditional Chinese (\xe7\xb9\x81\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87)".force_encoding('UTF-8')
266 str_tw = "Traditional Chinese (\xe7\xb9\x81\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87)".force_encoding('UTF-8')
276 assert_equal str_tw, l(:general_lang_name)
267 assert_equal str_tw, l(:general_lang_name)
277 assert_equal 'Big5', l(:general_csv_encoding)
268 assert_equal 'Big5', l(:general_csv_encoding)
278 assert_equal ',', l(:general_csv_separator)
269 assert_equal ',', l(:general_csv_separator)
279 assert_equal '.', l(:general_csv_decimal_separator)
270 assert_equal '.', l(:general_csv_decimal_separator)
280 end
271 end
281
272
282 def test_csv_cannot_convert_should_be_replaced_big_5
273 def test_csv_cannot_convert_should_be_replaced_big_5
283 str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85".force_encoding('UTF-8')
274 str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85".force_encoding('UTF-8')
284 user = User.find_by_id(3)
275 user = User.find_by_id(3)
285 user.firstname = str_utf8
276 user.firstname = str_utf8
286 user.lastname = "test-lastname"
277 user.lastname = "test-lastname"
287 assert user.save
278 assert user.save
288 comments = "test_replaced"
279 comments = "test_replaced"
289 te1 = TimeEntry.create(:spent_on => '2011-11-11',
280 te1 = TimeEntry.create(:spent_on => '2011-11-11',
290 :hours => 7.3,
281 :hours => 7.3,
291 :project => Project.find(1),
282 :project => Project.find(1),
292 :user => user,
283 :user => user,
293 :activity => TimeEntryActivity.find_by_name('Design'),
284 :activity => TimeEntryActivity.find_by_name('Design'),
294 :comments => comments)
285 :comments => comments)
295
286
296 te2 = TimeEntry.find_by_comments(comments)
287 te2 = TimeEntry.find_by_comments(comments)
297 assert_not_nil te2
288 assert_not_nil te2
298 assert_equal 7.3, te2.hours
289 assert_equal 7.3, te2.hours
299 assert_equal 3, te2.user_id
290 assert_equal 3, te2.user_id
300
291
301 with_settings :default_language => "zh-TW" do
292 with_settings :default_language => "zh-TW" do
302 get :report, :project_id => 1, :columns => 'day',
293 get :report, :project_id => 1, :columns => 'day',
303 :from => "2011-11-11", :to => "2011-11-11",
294 :from => "2011-11-11", :to => "2011-11-11",
304 :criteria => ["user"], :format => "csv"
295 :criteria => ["user"], :format => "csv"
305 end
296 end
306 assert_response :success
297 assert_response :success
307 assert_equal 'text/csv; header=present', @response.content_type
298 assert_equal 'text/csv; header=present', @response.content_type
308 lines = @response.body.chomp.split("\n")
299 lines = @response.body.chomp.split("\n")
309 # Headers
300 # Headers
310 s1 = "\xa5\xce\xa4\xe1,2011-11-11,\xa4u\xae\xc9\xc1`\xadp".force_encoding('Big5')
301 s1 = "\xa5\xce\xa4\xe1,2011-11-11,\xa4u\xae\xc9\xc1`\xadp".force_encoding('Big5')
311 assert_equal s1, lines.first
302 assert_equal s1, lines.first
312 # Total row
303 # Total row
313 s2 = "\xa5H?".force_encoding('Big5')
304 s2 = "\xa5H?".force_encoding('Big5')
314 assert_equal "#{s2} #{user.lastname},7.30,7.30", lines[1]
305 assert_equal "#{s2} #{user.lastname},7.30,7.30", lines[1]
315 end
306 end
316
307
317 def test_csv_fr
308 def test_csv_fr
318 with_settings :default_language => "fr" do
309 with_settings :default_language => "fr" do
319 str1 = "test_csv_fr"
310 str1 = "test_csv_fr"
320 user = User.find_by_id(3)
311 user = User.find_by_id(3)
321 te1 = TimeEntry.create(:spent_on => '2011-11-11',
312 te1 = TimeEntry.create(:spent_on => '2011-11-11',
322 :hours => 7.3,
313 :hours => 7.3,
323 :project => Project.find(1),
314 :project => Project.find(1),
324 :user => user,
315 :user => user,
325 :activity => TimeEntryActivity.find_by_name('Design'),
316 :activity => TimeEntryActivity.find_by_name('Design'),
326 :comments => str1)
317 :comments => str1)
327
318
328 te2 = TimeEntry.find_by_comments(str1)
319 te2 = TimeEntry.find_by_comments(str1)
329 assert_not_nil te2
320 assert_not_nil te2
330 assert_equal 7.3, te2.hours
321 assert_equal 7.3, te2.hours
331 assert_equal 3, te2.user_id
322 assert_equal 3, te2.user_id
332
323
333 get :report, :project_id => 1, :columns => 'day',
324 get :report, :project_id => 1, :columns => 'day',
334 :from => "2011-11-11", :to => "2011-11-11",
325 :from => "2011-11-11", :to => "2011-11-11",
335 :criteria => ["user"], :format => "csv"
326 :criteria => ["user"], :format => "csv"
336 assert_response :success
327 assert_response :success
337 assert_equal 'text/csv; header=present', @response.content_type
328 assert_equal 'text/csv; header=present', @response.content_type
338 lines = @response.body.chomp.split("\n")
329 lines = @response.body.chomp.split("\n")
339 # Headers
330 # Headers
340 s1 = "Utilisateur;2011-11-11;Temps total".force_encoding('ISO-8859-1')
331 s1 = "Utilisateur;2011-11-11;Temps total".force_encoding('ISO-8859-1')
341 s2 = "Temps total".force_encoding('ISO-8859-1')
332 s2 = "Temps total".force_encoding('ISO-8859-1')
342 assert_equal s1, lines.first
333 assert_equal s1, lines.first
343 # Total row
334 # Total row
344 assert_equal "#{user.firstname} #{user.lastname};7,30;7,30", lines[1]
335 assert_equal "#{user.firstname} #{user.lastname};7,30;7,30", lines[1]
345 assert_equal "#{s2};7,30;7,30", lines[2]
336 assert_equal "#{s2};7,30;7,30", lines[2]
346
337
347 str_fr = "French (Fran\xc3\xa7ais)".force_encoding('UTF-8')
338 str_fr = "French (Fran\xc3\xa7ais)".force_encoding('UTF-8')
348 assert_equal str_fr, l(:general_lang_name)
339 assert_equal str_fr, l(:general_lang_name)
349 assert_equal 'ISO-8859-1', l(:general_csv_encoding)
340 assert_equal 'ISO-8859-1', l(:general_csv_encoding)
350 assert_equal ';', l(:general_csv_separator)
341 assert_equal ';', l(:general_csv_separator)
351 assert_equal ',', l(:general_csv_decimal_separator)
342 assert_equal ',', l(:general_csv_decimal_separator)
352 end
343 end
353 end
344 end
354 end
345 end
@@ -1,846 +1,823
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # Redmine - project management software
2 # Redmine - project management software
3 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 # Copyright (C) 2006-2016 Jean-Philippe Lang
4 #
4 #
5 # This program is free software; you can redistribute it and/or
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
8 # of the License, or (at your option) any later version.
9 #
9 #
10 # This program is distributed in the hope that it will be useful,
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
13 # GNU General Public License for more details.
14 #
14 #
15 # You should have received a copy of the GNU General Public License
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
18
19 require File.expand_path('../../test_helper', __FILE__)
19 require File.expand_path('../../test_helper', __FILE__)
20
20
21 class TimelogControllerTest < ActionController::TestCase
21 class TimelogControllerTest < ActionController::TestCase
22 fixtures :projects, :enabled_modules, :roles, :members,
22 fixtures :projects, :enabled_modules, :roles, :members,
23 :member_roles, :issues, :time_entries, :users,
23 :member_roles, :issues, :time_entries, :users,
24 :trackers, :enumerations, :issue_statuses,
24 :trackers, :enumerations, :issue_statuses,
25 :custom_fields, :custom_values,
25 :custom_fields, :custom_values,
26 :projects_trackers, :custom_fields_trackers,
26 :projects_trackers, :custom_fields_trackers,
27 :custom_fields_projects
27 :custom_fields_projects
28
28
29 include Redmine::I18n
29 include Redmine::I18n
30
30
31 def test_new
31 def test_new
32 @request.session[:user_id] = 3
32 @request.session[:user_id] = 3
33 get :new
33 get :new
34 assert_response :success
34 assert_response :success
35 assert_template 'new'
35 assert_template 'new'
36 assert_select 'input[name=?][type=hidden]', 'project_id', 0
36 assert_select 'input[name=?][type=hidden]', 'project_id', 0
37 assert_select 'input[name=?][type=hidden]', 'issue_id', 0
37 assert_select 'input[name=?][type=hidden]', 'issue_id', 0
38 assert_select 'select[name=?]', 'time_entry[project_id]' do
38 assert_select 'select[name=?]', 'time_entry[project_id]' do
39 # blank option for project
39 # blank option for project
40 assert_select 'option[value=""]'
40 assert_select 'option[value=""]'
41 end
41 end
42 end
42 end
43
43
44 def test_new_with_project_id
44 def test_new_with_project_id
45 @request.session[:user_id] = 3
45 @request.session[:user_id] = 3
46 get :new, :project_id => 1
46 get :new, :project_id => 1
47 assert_response :success
47 assert_response :success
48 assert_template 'new'
48 assert_template 'new'
49 assert_select 'input[name=?][type=hidden]', 'project_id'
49 assert_select 'input[name=?][type=hidden]', 'project_id'
50 assert_select 'input[name=?][type=hidden]', 'issue_id', 0
50 assert_select 'input[name=?][type=hidden]', 'issue_id', 0
51 assert_select 'select[name=?]', 'time_entry[project_id]', 0
51 assert_select 'select[name=?]', 'time_entry[project_id]', 0
52 end
52 end
53
53
54 def test_new_with_issue_id
54 def test_new_with_issue_id
55 @request.session[:user_id] = 3
55 @request.session[:user_id] = 3
56 get :new, :issue_id => 2
56 get :new, :issue_id => 2
57 assert_response :success
57 assert_response :success
58 assert_template 'new'
58 assert_template 'new'
59 assert_select 'input[name=?][type=hidden]', 'project_id', 0
59 assert_select 'input[name=?][type=hidden]', 'project_id', 0
60 assert_select 'input[name=?][type=hidden]', 'issue_id'
60 assert_select 'input[name=?][type=hidden]', 'issue_id'
61 assert_select 'select[name=?]', 'time_entry[project_id]', 0
61 assert_select 'select[name=?]', 'time_entry[project_id]', 0
62 end
62 end
63
63
64 def test_new_without_project_should_prefill_the_form
64 def test_new_without_project_should_prefill_the_form
65 @request.session[:user_id] = 3
65 @request.session[:user_id] = 3
66 get :new, :time_entry => {:project_id => '1'}
66 get :new, :time_entry => {:project_id => '1'}
67 assert_response :success
67 assert_response :success
68 assert_template 'new'
68 assert_template 'new'
69 assert_select 'select[name=?]', 'time_entry[project_id]' do
69 assert_select 'select[name=?]', 'time_entry[project_id]' do
70 assert_select 'option[value="1"][selected=selected]'
70 assert_select 'option[value="1"][selected=selected]'
71 end
71 end
72 end
72 end
73
73
74 def test_new_without_project_should_deny_without_permission
74 def test_new_without_project_should_deny_without_permission
75 Role.all.each {|role| role.remove_permission! :log_time}
75 Role.all.each {|role| role.remove_permission! :log_time}
76 @request.session[:user_id] = 3
76 @request.session[:user_id] = 3
77
77
78 get :new
78 get :new
79 assert_response 403
79 assert_response 403
80 end
80 end
81
81
82 def test_new_should_select_default_activity
82 def test_new_should_select_default_activity
83 @request.session[:user_id] = 3
83 @request.session[:user_id] = 3
84 get :new, :project_id => 1
84 get :new, :project_id => 1
85 assert_response :success
85 assert_response :success
86 assert_select 'select[name=?]', 'time_entry[activity_id]' do
86 assert_select 'select[name=?]', 'time_entry[activity_id]' do
87 assert_select 'option[selected=selected]', :text => 'Development'
87 assert_select 'option[selected=selected]', :text => 'Development'
88 end
88 end
89 end
89 end
90
90
91 def test_new_should_only_show_active_time_entry_activities
91 def test_new_should_only_show_active_time_entry_activities
92 @request.session[:user_id] = 3
92 @request.session[:user_id] = 3
93 get :new, :project_id => 1
93 get :new, :project_id => 1
94 assert_response :success
94 assert_response :success
95 assert_select 'option', :text => 'Inactive Activity', :count => 0
95 assert_select 'option', :text => 'Inactive Activity', :count => 0
96 end
96 end
97
97
98 def test_post_new_as_js_should_update_activity_options
98 def test_post_new_as_js_should_update_activity_options
99 @request.session[:user_id] = 3
99 @request.session[:user_id] = 3
100 post :new, :time_entry => {:project_id => 1}, :format => 'js'
100 post :new, :time_entry => {:project_id => 1}, :format => 'js'
101 assert_response :success
101 assert_response :success
102 assert_include '#time_entry_activity_id', response.body
102 assert_include '#time_entry_activity_id', response.body
103 end
103 end
104
104
105 def test_get_edit_existing_time
105 def test_get_edit_existing_time
106 @request.session[:user_id] = 2
106 @request.session[:user_id] = 2
107 get :edit, :id => 2, :project_id => nil
107 get :edit, :id => 2, :project_id => nil
108 assert_response :success
108 assert_response :success
109 assert_template 'edit'
109 assert_template 'edit'
110 assert_select 'form[action=?]', '/time_entries/2'
110 assert_select 'form[action=?]', '/time_entries/2'
111 end
111 end
112
112
113 def test_get_edit_with_an_existing_time_entry_with_inactive_activity
113 def test_get_edit_with_an_existing_time_entry_with_inactive_activity
114 te = TimeEntry.find(1)
114 te = TimeEntry.find(1)
115 te.activity = TimeEntryActivity.find_by_name("Inactive Activity")
115 te.activity = TimeEntryActivity.find_by_name("Inactive Activity")
116 te.save!(:validate => false)
116 te.save!(:validate => false)
117
117
118 @request.session[:user_id] = 1
118 @request.session[:user_id] = 1
119 get :edit, :project_id => 1, :id => 1
119 get :edit, :project_id => 1, :id => 1
120 assert_response :success
120 assert_response :success
121 assert_template 'edit'
121 assert_template 'edit'
122 # Blank option since nothing is pre-selected
122 # Blank option since nothing is pre-selected
123 assert_select 'option', :text => '--- Please select ---'
123 assert_select 'option', :text => '--- Please select ---'
124 end
124 end
125
125
126 def test_post_create
126 def test_post_create
127 @request.session[:user_id] = 3
127 @request.session[:user_id] = 3
128 assert_difference 'TimeEntry.count' do
128 assert_difference 'TimeEntry.count' do
129 post :create, :project_id => 1,
129 post :create, :project_id => 1,
130 :time_entry => {:comments => 'Some work on TimelogControllerTest',
130 :time_entry => {:comments => 'Some work on TimelogControllerTest',
131 # Not the default activity
131 # Not the default activity
132 :activity_id => '11',
132 :activity_id => '11',
133 :spent_on => '2008-03-14',
133 :spent_on => '2008-03-14',
134 :issue_id => '1',
134 :issue_id => '1',
135 :hours => '7.3'}
135 :hours => '7.3'}
136 assert_redirected_to '/projects/ecookbook/time_entries'
136 assert_redirected_to '/projects/ecookbook/time_entries'
137 end
137 end
138
138
139 t = TimeEntry.order('id DESC').first
139 t = TimeEntry.order('id DESC').first
140 assert_not_nil t
140 assert_not_nil t
141 assert_equal 'Some work on TimelogControllerTest', t.comments
141 assert_equal 'Some work on TimelogControllerTest', t.comments
142 assert_equal 1, t.project_id
142 assert_equal 1, t.project_id
143 assert_equal 1, t.issue_id
143 assert_equal 1, t.issue_id
144 assert_equal 11, t.activity_id
144 assert_equal 11, t.activity_id
145 assert_equal 7.3, t.hours
145 assert_equal 7.3, t.hours
146 assert_equal 3, t.user_id
146 assert_equal 3, t.user_id
147 end
147 end
148
148
149 def test_post_create_with_blank_issue
149 def test_post_create_with_blank_issue
150 @request.session[:user_id] = 3
150 @request.session[:user_id] = 3
151 assert_difference 'TimeEntry.count' do
151 assert_difference 'TimeEntry.count' do
152 post :create, :project_id => 1,
152 post :create, :project_id => 1,
153 :time_entry => {:comments => 'Some work on TimelogControllerTest',
153 :time_entry => {:comments => 'Some work on TimelogControllerTest',
154 # Not the default activity
154 # Not the default activity
155 :activity_id => '11',
155 :activity_id => '11',
156 :issue_id => '',
156 :issue_id => '',
157 :spent_on => '2008-03-14',
157 :spent_on => '2008-03-14',
158 :hours => '7.3'}
158 :hours => '7.3'}
159 assert_redirected_to '/projects/ecookbook/time_entries'
159 assert_redirected_to '/projects/ecookbook/time_entries'
160 end
160 end
161
161
162 t = TimeEntry.order('id DESC').first
162 t = TimeEntry.order('id DESC').first
163 assert_not_nil t
163 assert_not_nil t
164 assert_equal 'Some work on TimelogControllerTest', t.comments
164 assert_equal 'Some work on TimelogControllerTest', t.comments
165 assert_equal 1, t.project_id
165 assert_equal 1, t.project_id
166 assert_nil t.issue_id
166 assert_nil t.issue_id
167 assert_equal 11, t.activity_id
167 assert_equal 11, t.activity_id
168 assert_equal 7.3, t.hours
168 assert_equal 7.3, t.hours
169 assert_equal 3, t.user_id
169 assert_equal 3, t.user_id
170 end
170 end
171
171
172 def test_create_on_project_with_time_tracking_disabled_should_fail
172 def test_create_on_project_with_time_tracking_disabled_should_fail
173 Project.find(1).disable_module! :time_tracking
173 Project.find(1).disable_module! :time_tracking
174
174
175 @request.session[:user_id] = 2
175 @request.session[:user_id] = 2
176 assert_no_difference 'TimeEntry.count' do
176 assert_no_difference 'TimeEntry.count' do
177 post :create, :time_entry => {
177 post :create, :time_entry => {
178 :project_id => '1', :issue_id => '',
178 :project_id => '1', :issue_id => '',
179 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
179 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
180 }
180 }
181 end
181 end
182 end
182 end
183
183
184 def test_create_on_project_without_permission_should_fail
184 def test_create_on_project_without_permission_should_fail
185 Role.find(1).remove_permission! :log_time
185 Role.find(1).remove_permission! :log_time
186
186
187 @request.session[:user_id] = 2
187 @request.session[:user_id] = 2
188 assert_no_difference 'TimeEntry.count' do
188 assert_no_difference 'TimeEntry.count' do
189 post :create, :time_entry => {
189 post :create, :time_entry => {
190 :project_id => '1', :issue_id => '',
190 :project_id => '1', :issue_id => '',
191 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
191 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
192 }
192 }
193 end
193 end
194 end
194 end
195
195
196 def test_create_on_issue_in_project_with_time_tracking_disabled_should_fail
196 def test_create_on_issue_in_project_with_time_tracking_disabled_should_fail
197 Project.find(1).disable_module! :time_tracking
197 Project.find(1).disable_module! :time_tracking
198
198
199 @request.session[:user_id] = 2
199 @request.session[:user_id] = 2
200 assert_no_difference 'TimeEntry.count' do
200 assert_no_difference 'TimeEntry.count' do
201 post :create, :time_entry => {
201 post :create, :time_entry => {
202 :project_id => '', :issue_id => '1',
202 :project_id => '', :issue_id => '1',
203 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
203 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
204 }
204 }
205 assert_select_error /Issue is invalid/
205 assert_select_error /Issue is invalid/
206 end
206 end
207 end
207 end
208
208
209 def test_create_on_issue_in_project_without_permission_should_fail
209 def test_create_on_issue_in_project_without_permission_should_fail
210 Role.find(1).remove_permission! :log_time
210 Role.find(1).remove_permission! :log_time
211
211
212 @request.session[:user_id] = 2
212 @request.session[:user_id] = 2
213 assert_no_difference 'TimeEntry.count' do
213 assert_no_difference 'TimeEntry.count' do
214 post :create, :time_entry => {
214 post :create, :time_entry => {
215 :project_id => '', :issue_id => '1',
215 :project_id => '', :issue_id => '1',
216 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
216 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
217 }
217 }
218 assert_select_error /Issue is invalid/
218 assert_select_error /Issue is invalid/
219 end
219 end
220 end
220 end
221
221
222 def test_create_on_issue_that_is_not_visible_should_not_disclose_subject
222 def test_create_on_issue_that_is_not_visible_should_not_disclose_subject
223 issue = Issue.generate!(:subject => "issue_that_is_not_visible", :is_private => true)
223 issue = Issue.generate!(:subject => "issue_that_is_not_visible", :is_private => true)
224 assert !issue.visible?(User.find(3))
224 assert !issue.visible?(User.find(3))
225
225
226 @request.session[:user_id] = 3
226 @request.session[:user_id] = 3
227 assert_no_difference 'TimeEntry.count' do
227 assert_no_difference 'TimeEntry.count' do
228 post :create, :time_entry => {
228 post :create, :time_entry => {
229 :project_id => '', :issue_id => issue.id.to_s,
229 :project_id => '', :issue_id => issue.id.to_s,
230 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
230 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
231 }
231 }
232 end
232 end
233 assert_select_error /Issue is invalid/
233 assert_select_error /Issue is invalid/
234 assert_select "input[name=?][value=?]", "time_entry[issue_id]", issue.id.to_s
234 assert_select "input[name=?][value=?]", "time_entry[issue_id]", issue.id.to_s
235 assert_select "#time_entry_issue", 0
235 assert_select "#time_entry_issue", 0
236 assert !response.body.include?('issue_that_is_not_visible')
236 assert !response.body.include?('issue_that_is_not_visible')
237 end
237 end
238
238
239 def test_create_and_continue_at_project_level
239 def test_create_and_continue_at_project_level
240 @request.session[:user_id] = 2
240 @request.session[:user_id] = 2
241 assert_difference 'TimeEntry.count' do
241 assert_difference 'TimeEntry.count' do
242 post :create, :time_entry => {:project_id => '1',
242 post :create, :time_entry => {:project_id => '1',
243 :activity_id => '11',
243 :activity_id => '11',
244 :issue_id => '',
244 :issue_id => '',
245 :spent_on => '2008-03-14',
245 :spent_on => '2008-03-14',
246 :hours => '7.3'},
246 :hours => '7.3'},
247 :continue => '1'
247 :continue => '1'
248 assert_redirected_to '/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=&time_entry%5Bproject_id%5D=1'
248 assert_redirected_to '/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=&time_entry%5Bproject_id%5D=1'
249 end
249 end
250 end
250 end
251
251
252 def test_create_and_continue_at_issue_level
252 def test_create_and_continue_at_issue_level
253 @request.session[:user_id] = 2
253 @request.session[:user_id] = 2
254 assert_difference 'TimeEntry.count' do
254 assert_difference 'TimeEntry.count' do
255 post :create, :time_entry => {:project_id => '',
255 post :create, :time_entry => {:project_id => '',
256 :activity_id => '11',
256 :activity_id => '11',
257 :issue_id => '1',
257 :issue_id => '1',
258 :spent_on => '2008-03-14',
258 :spent_on => '2008-03-14',
259 :hours => '7.3'},
259 :hours => '7.3'},
260 :continue => '1'
260 :continue => '1'
261 assert_redirected_to '/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=1&time_entry%5Bproject_id%5D='
261 assert_redirected_to '/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=1&time_entry%5Bproject_id%5D='
262 end
262 end
263 end
263 end
264
264
265 def test_create_and_continue_with_project_id
265 def test_create_and_continue_with_project_id
266 @request.session[:user_id] = 2
266 @request.session[:user_id] = 2
267 assert_difference 'TimeEntry.count' do
267 assert_difference 'TimeEntry.count' do
268 post :create, :project_id => 1,
268 post :create, :project_id => 1,
269 :time_entry => {:activity_id => '11',
269 :time_entry => {:activity_id => '11',
270 :issue_id => '',
270 :issue_id => '',
271 :spent_on => '2008-03-14',
271 :spent_on => '2008-03-14',
272 :hours => '7.3'},
272 :hours => '7.3'},
273 :continue => '1'
273 :continue => '1'
274 assert_redirected_to '/projects/ecookbook/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=&time_entry%5Bproject_id%5D='
274 assert_redirected_to '/projects/ecookbook/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=&time_entry%5Bproject_id%5D='
275 end
275 end
276 end
276 end
277
277
278 def test_create_and_continue_with_issue_id
278 def test_create_and_continue_with_issue_id
279 @request.session[:user_id] = 2
279 @request.session[:user_id] = 2
280 assert_difference 'TimeEntry.count' do
280 assert_difference 'TimeEntry.count' do
281 post :create, :issue_id => 1,
281 post :create, :issue_id => 1,
282 :time_entry => {:activity_id => '11',
282 :time_entry => {:activity_id => '11',
283 :issue_id => '1',
283 :issue_id => '1',
284 :spent_on => '2008-03-14',
284 :spent_on => '2008-03-14',
285 :hours => '7.3'},
285 :hours => '7.3'},
286 :continue => '1'
286 :continue => '1'
287 assert_redirected_to '/issues/1/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=1&time_entry%5Bproject_id%5D='
287 assert_redirected_to '/issues/1/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=1&time_entry%5Bproject_id%5D='
288 end
288 end
289 end
289 end
290
290
291 def test_create_without_log_time_permission_should_be_denied
291 def test_create_without_log_time_permission_should_be_denied
292 @request.session[:user_id] = 2
292 @request.session[:user_id] = 2
293 Role.find_by_name('Manager').remove_permission! :log_time
293 Role.find_by_name('Manager').remove_permission! :log_time
294 post :create, :project_id => 1,
294 post :create, :project_id => 1,
295 :time_entry => {:activity_id => '11',
295 :time_entry => {:activity_id => '11',
296 :issue_id => '',
296 :issue_id => '',
297 :spent_on => '2008-03-14',
297 :spent_on => '2008-03-14',
298 :hours => '7.3'}
298 :hours => '7.3'}
299
299
300 assert_response 403
300 assert_response 403
301 end
301 end
302
302
303 def test_create_without_project_and_issue_should_fail
303 def test_create_without_project_and_issue_should_fail
304 @request.session[:user_id] = 2
304 @request.session[:user_id] = 2
305 post :create, :time_entry => {:issue_id => ''}
305 post :create, :time_entry => {:issue_id => ''}
306
306
307 assert_response :success
307 assert_response :success
308 assert_template 'new'
308 assert_template 'new'
309 end
309 end
310
310
311 def test_create_with_failure
311 def test_create_with_failure
312 @request.session[:user_id] = 2
312 @request.session[:user_id] = 2
313 post :create, :project_id => 1,
313 post :create, :project_id => 1,
314 :time_entry => {:activity_id => '',
314 :time_entry => {:activity_id => '',
315 :issue_id => '',
315 :issue_id => '',
316 :spent_on => '2008-03-14',
316 :spent_on => '2008-03-14',
317 :hours => '7.3'}
317 :hours => '7.3'}
318
318
319 assert_response :success
319 assert_response :success
320 assert_template 'new'
320 assert_template 'new'
321 end
321 end
322
322
323 def test_create_without_project
323 def test_create_without_project
324 @request.session[:user_id] = 2
324 @request.session[:user_id] = 2
325 assert_difference 'TimeEntry.count' do
325 assert_difference 'TimeEntry.count' do
326 post :create, :time_entry => {:project_id => '1',
326 post :create, :time_entry => {:project_id => '1',
327 :activity_id => '11',
327 :activity_id => '11',
328 :issue_id => '',
328 :issue_id => '',
329 :spent_on => '2008-03-14',
329 :spent_on => '2008-03-14',
330 :hours => '7.3'}
330 :hours => '7.3'}
331 end
331 end
332
332
333 assert_redirected_to '/projects/ecookbook/time_entries'
333 assert_redirected_to '/projects/ecookbook/time_entries'
334 time_entry = TimeEntry.order('id DESC').first
334 time_entry = TimeEntry.order('id DESC').first
335 assert_equal 1, time_entry.project_id
335 assert_equal 1, time_entry.project_id
336 end
336 end
337
337
338 def test_create_without_project_should_fail_with_issue_not_inside_project
338 def test_create_without_project_should_fail_with_issue_not_inside_project
339 @request.session[:user_id] = 2
339 @request.session[:user_id] = 2
340 assert_no_difference 'TimeEntry.count' do
340 assert_no_difference 'TimeEntry.count' do
341 post :create, :time_entry => {:project_id => '1',
341 post :create, :time_entry => {:project_id => '1',
342 :activity_id => '11',
342 :activity_id => '11',
343 :issue_id => '5',
343 :issue_id => '5',
344 :spent_on => '2008-03-14',
344 :spent_on => '2008-03-14',
345 :hours => '7.3'}
345 :hours => '7.3'}
346 end
346 end
347
347
348 assert_response :success
348 assert_response :success
349 assert assigns(:time_entry).errors[:issue_id].present?
349 assert assigns(:time_entry).errors[:issue_id].present?
350 end
350 end
351
351
352 def test_create_without_project_should_deny_without_permission
352 def test_create_without_project_should_deny_without_permission
353 @request.session[:user_id] = 2
353 @request.session[:user_id] = 2
354 Project.find(3).disable_module!(:time_tracking)
354 Project.find(3).disable_module!(:time_tracking)
355
355
356 assert_no_difference 'TimeEntry.count' do
356 assert_no_difference 'TimeEntry.count' do
357 post :create, :time_entry => {:project_id => '3',
357 post :create, :time_entry => {:project_id => '3',
358 :activity_id => '11',
358 :activity_id => '11',
359 :issue_id => '',
359 :issue_id => '',
360 :spent_on => '2008-03-14',
360 :spent_on => '2008-03-14',
361 :hours => '7.3'}
361 :hours => '7.3'}
362 end
362 end
363
363
364 assert_response 403
364 assert_response 403
365 end
365 end
366
366
367 def test_create_without_project_with_failure
367 def test_create_without_project_with_failure
368 @request.session[:user_id] = 2
368 @request.session[:user_id] = 2
369 assert_no_difference 'TimeEntry.count' do
369 assert_no_difference 'TimeEntry.count' do
370 post :create, :time_entry => {:project_id => '1',
370 post :create, :time_entry => {:project_id => '1',
371 :activity_id => '11',
371 :activity_id => '11',
372 :issue_id => '',
372 :issue_id => '',
373 :spent_on => '2008-03-14',
373 :spent_on => '2008-03-14',
374 :hours => ''}
374 :hours => ''}
375 end
375 end
376
376
377 assert_response :success
377 assert_response :success
378 assert_select 'select[name=?]', 'time_entry[project_id]' do
378 assert_select 'select[name=?]', 'time_entry[project_id]' do
379 assert_select 'option[value="1"][selected=selected]'
379 assert_select 'option[value="1"][selected=selected]'
380 end
380 end
381 end
381 end
382
382
383 def test_update
383 def test_update
384 entry = TimeEntry.find(1)
384 entry = TimeEntry.find(1)
385 assert_equal 1, entry.issue_id
385 assert_equal 1, entry.issue_id
386 assert_equal 2, entry.user_id
386 assert_equal 2, entry.user_id
387
387
388 @request.session[:user_id] = 1
388 @request.session[:user_id] = 1
389 put :update, :id => 1,
389 put :update, :id => 1,
390 :time_entry => {:issue_id => '2',
390 :time_entry => {:issue_id => '2',
391 :hours => '8'}
391 :hours => '8'}
392 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
392 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
393 entry.reload
393 entry.reload
394
394
395 assert_equal 8, entry.hours
395 assert_equal 8, entry.hours
396 assert_equal 2, entry.issue_id
396 assert_equal 2, entry.issue_id
397 assert_equal 2, entry.user_id
397 assert_equal 2, entry.user_id
398 end
398 end
399
399
400 def test_update_should_allow_to_change_issue_to_another_project
400 def test_update_should_allow_to_change_issue_to_another_project
401 entry = TimeEntry.generate!(:issue_id => 1)
401 entry = TimeEntry.generate!(:issue_id => 1)
402
402
403 @request.session[:user_id] = 1
403 @request.session[:user_id] = 1
404 put :update, :id => entry.id, :time_entry => {:issue_id => '5'}
404 put :update, :id => entry.id, :time_entry => {:issue_id => '5'}
405 assert_response 302
405 assert_response 302
406 entry.reload
406 entry.reload
407
407
408 assert_equal 5, entry.issue_id
408 assert_equal 5, entry.issue_id
409 assert_equal 3, entry.project_id
409 assert_equal 3, entry.project_id
410 end
410 end
411
411
412 def test_update_should_not_allow_to_change_issue_to_an_invalid_project
412 def test_update_should_not_allow_to_change_issue_to_an_invalid_project
413 entry = TimeEntry.generate!(:issue_id => 1)
413 entry = TimeEntry.generate!(:issue_id => 1)
414 Project.find(3).disable_module!(:time_tracking)
414 Project.find(3).disable_module!(:time_tracking)
415
415
416 @request.session[:user_id] = 1
416 @request.session[:user_id] = 1
417 put :update, :id => entry.id, :time_entry => {:issue_id => '5'}
417 put :update, :id => entry.id, :time_entry => {:issue_id => '5'}
418 assert_response 200
418 assert_response 200
419 assert_include "Issue is invalid", assigns(:time_entry).errors.full_messages
419 assert_include "Issue is invalid", assigns(:time_entry).errors.full_messages
420 end
420 end
421
421
422 def test_get_bulk_edit
422 def test_get_bulk_edit
423 @request.session[:user_id] = 2
423 @request.session[:user_id] = 2
424 get :bulk_edit, :ids => [1, 2]
424 get :bulk_edit, :ids => [1, 2]
425 assert_response :success
425 assert_response :success
426 assert_template 'bulk_edit'
426 assert_template 'bulk_edit'
427
427
428 assert_select 'ul#bulk-selection' do
428 assert_select 'ul#bulk-selection' do
429 assert_select 'li', 2
429 assert_select 'li', 2
430 assert_select 'li a', :text => '03/23/2007 - eCookbook: 4.25 hours'
430 assert_select 'li a', :text => '03/23/2007 - eCookbook: 4.25 hours'
431 end
431 end
432
432
433 assert_select 'form#bulk_edit_form[action=?]', '/time_entries/bulk_update' do
433 assert_select 'form#bulk_edit_form[action=?]', '/time_entries/bulk_update' do
434 # System wide custom field
434 # System wide custom field
435 assert_select 'select[name=?]', 'time_entry[custom_field_values][10]'
435 assert_select 'select[name=?]', 'time_entry[custom_field_values][10]'
436
436
437 # Activities
437 # Activities
438 assert_select 'select[name=?]', 'time_entry[activity_id]' do
438 assert_select 'select[name=?]', 'time_entry[activity_id]' do
439 assert_select 'option[value=""]', :text => '(No change)'
439 assert_select 'option[value=""]', :text => '(No change)'
440 assert_select 'option[value="9"]', :text => 'Design'
440 assert_select 'option[value="9"]', :text => 'Design'
441 end
441 end
442 end
442 end
443 end
443 end
444
444
445 def test_get_bulk_edit_on_different_projects
445 def test_get_bulk_edit_on_different_projects
446 @request.session[:user_id] = 2
446 @request.session[:user_id] = 2
447 get :bulk_edit, :ids => [1, 2, 6]
447 get :bulk_edit, :ids => [1, 2, 6]
448 assert_response :success
448 assert_response :success
449 assert_template 'bulk_edit'
449 assert_template 'bulk_edit'
450 end
450 end
451
451
452 def test_bulk_edit_with_edit_own_time_entries_permission
452 def test_bulk_edit_with_edit_own_time_entries_permission
453 @request.session[:user_id] = 2
453 @request.session[:user_id] = 2
454 Role.find_by_name('Manager').remove_permission! :edit_time_entries
454 Role.find_by_name('Manager').remove_permission! :edit_time_entries
455 Role.find_by_name('Manager').add_permission! :edit_own_time_entries
455 Role.find_by_name('Manager').add_permission! :edit_own_time_entries
456 ids = (0..1).map {TimeEntry.generate!(:user => User.find(2)).id}
456 ids = (0..1).map {TimeEntry.generate!(:user => User.find(2)).id}
457
457
458 get :bulk_edit, :ids => ids
458 get :bulk_edit, :ids => ids
459 assert_response :success
459 assert_response :success
460 end
460 end
461
461
462 def test_bulk_update
462 def test_bulk_update
463 @request.session[:user_id] = 2
463 @request.session[:user_id] = 2
464 # update time entry activity
464 # update time entry activity
465 post :bulk_update, :ids => [1, 2], :time_entry => { :activity_id => 9}
465 post :bulk_update, :ids => [1, 2], :time_entry => { :activity_id => 9}
466
466
467 assert_response 302
467 assert_response 302
468 # check that the issues were updated
468 # check that the issues were updated
469 assert_equal [9, 9], TimeEntry.where(:id => [1, 2]).collect {|i| i.activity_id}
469 assert_equal [9, 9], TimeEntry.where(:id => [1, 2]).collect {|i| i.activity_id}
470 end
470 end
471
471
472 def test_bulk_update_with_failure
472 def test_bulk_update_with_failure
473 @request.session[:user_id] = 2
473 @request.session[:user_id] = 2
474 post :bulk_update, :ids => [1, 2], :time_entry => { :hours => 'A'}
474 post :bulk_update, :ids => [1, 2], :time_entry => { :hours => 'A'}
475
475
476 assert_response 302
476 assert_response 302
477 assert_match /Failed to save 2 time entrie/, flash[:error]
477 assert_match /Failed to save 2 time entrie/, flash[:error]
478 end
478 end
479
479
480 def test_bulk_update_on_different_projects
480 def test_bulk_update_on_different_projects
481 @request.session[:user_id] = 2
481 @request.session[:user_id] = 2
482 # makes user a manager on the other project
482 # makes user a manager on the other project
483 Member.create!(:user_id => 2, :project_id => 3, :role_ids => [1])
483 Member.create!(:user_id => 2, :project_id => 3, :role_ids => [1])
484
484
485 # update time entry activity
485 # update time entry activity
486 post :bulk_update, :ids => [1, 2, 4], :time_entry => { :activity_id => 9 }
486 post :bulk_update, :ids => [1, 2, 4], :time_entry => { :activity_id => 9 }
487
487
488 assert_response 302
488 assert_response 302
489 # check that the issues were updated
489 # check that the issues were updated
490 assert_equal [9, 9, 9], TimeEntry.where(:id => [1, 2, 4]).collect {|i| i.activity_id}
490 assert_equal [9, 9, 9], TimeEntry.where(:id => [1, 2, 4]).collect {|i| i.activity_id}
491 end
491 end
492
492
493 def test_bulk_update_on_different_projects_without_rights
493 def test_bulk_update_on_different_projects_without_rights
494 @request.session[:user_id] = 3
494 @request.session[:user_id] = 3
495 user = User.find(3)
495 user = User.find(3)
496 action = { :controller => "timelog", :action => "bulk_update" }
496 action = { :controller => "timelog", :action => "bulk_update" }
497 assert user.allowed_to?(action, TimeEntry.find(1).project)
497 assert user.allowed_to?(action, TimeEntry.find(1).project)
498 assert ! user.allowed_to?(action, TimeEntry.find(5).project)
498 assert ! user.allowed_to?(action, TimeEntry.find(5).project)
499 post :bulk_update, :ids => [1, 5], :time_entry => { :activity_id => 9 }
499 post :bulk_update, :ids => [1, 5], :time_entry => { :activity_id => 9 }
500 assert_response 403
500 assert_response 403
501 end
501 end
502
502
503 def test_bulk_update_with_edit_own_time_entries_permission
503 def test_bulk_update_with_edit_own_time_entries_permission
504 @request.session[:user_id] = 2
504 @request.session[:user_id] = 2
505 Role.find_by_name('Manager').remove_permission! :edit_time_entries
505 Role.find_by_name('Manager').remove_permission! :edit_time_entries
506 Role.find_by_name('Manager').add_permission! :edit_own_time_entries
506 Role.find_by_name('Manager').add_permission! :edit_own_time_entries
507 ids = (0..1).map {TimeEntry.generate!(:user => User.find(2)).id}
507 ids = (0..1).map {TimeEntry.generate!(:user => User.find(2)).id}
508
508
509 post :bulk_update, :ids => ids, :time_entry => { :activity_id => 9 }
509 post :bulk_update, :ids => ids, :time_entry => { :activity_id => 9 }
510 assert_response 302
510 assert_response 302
511 end
511 end
512
512
513 def test_bulk_update_with_edit_own_time_entries_permissions_should_be_denied_for_time_entries_of_other_user
513 def test_bulk_update_with_edit_own_time_entries_permissions_should_be_denied_for_time_entries_of_other_user
514 @request.session[:user_id] = 2
514 @request.session[:user_id] = 2
515 Role.find_by_name('Manager').remove_permission! :edit_time_entries
515 Role.find_by_name('Manager').remove_permission! :edit_time_entries
516 Role.find_by_name('Manager').add_permission! :edit_own_time_entries
516 Role.find_by_name('Manager').add_permission! :edit_own_time_entries
517
517
518 post :bulk_update, :ids => [1, 2], :time_entry => { :activity_id => 9 }
518 post :bulk_update, :ids => [1, 2], :time_entry => { :activity_id => 9 }
519 assert_response 403
519 assert_response 403
520 end
520 end
521
521
522 def test_bulk_update_custom_field
522 def test_bulk_update_custom_field
523 @request.session[:user_id] = 2
523 @request.session[:user_id] = 2
524 post :bulk_update, :ids => [1, 2], :time_entry => { :custom_field_values => {'10' => '0'} }
524 post :bulk_update, :ids => [1, 2], :time_entry => { :custom_field_values => {'10' => '0'} }
525
525
526 assert_response 302
526 assert_response 302
527 assert_equal ["0", "0"], TimeEntry.where(:id => [1, 2]).collect {|i| i.custom_value_for(10).value}
527 assert_equal ["0", "0"], TimeEntry.where(:id => [1, 2]).collect {|i| i.custom_value_for(10).value}
528 end
528 end
529
529
530 def test_bulk_update_clear_custom_field
530 def test_bulk_update_clear_custom_field
531 field = TimeEntryCustomField.generate!(:field_format => 'string')
531 field = TimeEntryCustomField.generate!(:field_format => 'string')
532 @request.session[:user_id] = 2
532 @request.session[:user_id] = 2
533 post :bulk_update, :ids => [1, 2], :time_entry => { :custom_field_values => {field.id.to_s => '__none__'} }
533 post :bulk_update, :ids => [1, 2], :time_entry => { :custom_field_values => {field.id.to_s => '__none__'} }
534
534
535 assert_response 302
535 assert_response 302
536 assert_equal ["", ""], TimeEntry.where(:id => [1, 2]).collect {|i| i.custom_value_for(field).value}
536 assert_equal ["", ""], TimeEntry.where(:id => [1, 2]).collect {|i| i.custom_value_for(field).value}
537 end
537 end
538
538
539 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
539 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
540 @request.session[:user_id] = 2
540 @request.session[:user_id] = 2
541 post :bulk_update, :ids => [1,2], :back_url => '/time_entries'
541 post :bulk_update, :ids => [1,2], :back_url => '/time_entries'
542
542
543 assert_response :redirect
543 assert_response :redirect
544 assert_redirected_to '/time_entries'
544 assert_redirected_to '/time_entries'
545 end
545 end
546
546
547 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
547 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
548 @request.session[:user_id] = 2
548 @request.session[:user_id] = 2
549 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
549 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
550
550
551 assert_response :redirect
551 assert_response :redirect
552 assert_redirected_to :controller => 'timelog', :action => 'index', :project_id => Project.find(1).identifier
552 assert_redirected_to :controller => 'timelog', :action => 'index', :project_id => Project.find(1).identifier
553 end
553 end
554
554
555 def test_post_bulk_update_without_edit_permission_should_be_denied
555 def test_post_bulk_update_without_edit_permission_should_be_denied
556 @request.session[:user_id] = 2
556 @request.session[:user_id] = 2
557 Role.find_by_name('Manager').remove_permission! :edit_time_entries
557 Role.find_by_name('Manager').remove_permission! :edit_time_entries
558 post :bulk_update, :ids => [1,2]
558 post :bulk_update, :ids => [1,2]
559
559
560 assert_response 403
560 assert_response 403
561 end
561 end
562
562
563 def test_destroy
563 def test_destroy
564 @request.session[:user_id] = 2
564 @request.session[:user_id] = 2
565 delete :destroy, :id => 1
565 delete :destroy, :id => 1
566 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
566 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
567 assert_equal I18n.t(:notice_successful_delete), flash[:notice]
567 assert_equal I18n.t(:notice_successful_delete), flash[:notice]
568 assert_nil TimeEntry.find_by_id(1)
568 assert_nil TimeEntry.find_by_id(1)
569 end
569 end
570
570
571 def test_destroy_should_fail
571 def test_destroy_should_fail
572 # simulate that this fails (e.g. due to a plugin), see #5700
572 # simulate that this fails (e.g. due to a plugin), see #5700
573 TimeEntry.any_instance.expects(:destroy).returns(false)
573 TimeEntry.any_instance.expects(:destroy).returns(false)
574
574
575 @request.session[:user_id] = 2
575 @request.session[:user_id] = 2
576 delete :destroy, :id => 1
576 delete :destroy, :id => 1
577 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
577 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
578 assert_equal I18n.t(:notice_unable_delete_time_entry), flash[:error]
578 assert_equal I18n.t(:notice_unable_delete_time_entry), flash[:error]
579 assert_not_nil TimeEntry.find_by_id(1)
579 assert_not_nil TimeEntry.find_by_id(1)
580 end
580 end
581
581
582 def test_index_all_projects
582 def test_index_all_projects
583 get :index
583 get :index
584 assert_response :success
584 assert_response :success
585 assert_template 'index'
585 assert_template 'index'
586 assert_not_nil assigns(:total_hours)
586 assert_not_nil assigns(:total_hours)
587 assert_equal "162.90", "%.2f" % assigns(:total_hours)
587 assert_equal "162.90", "%.2f" % assigns(:total_hours)
588 assert_select 'form#query_form[action=?]', '/time_entries'
588 assert_select 'form#query_form[action=?]', '/time_entries'
589 end
589 end
590
590
591 def test_index_all_projects_should_show_log_time_link
591 def test_index_all_projects_should_show_log_time_link
592 @request.session[:user_id] = 2
592 @request.session[:user_id] = 2
593 get :index
593 get :index
594 assert_response :success
594 assert_response :success
595 assert_template 'index'
595 assert_template 'index'
596 assert_select 'a[href=?]', '/time_entries/new', :text => /Log time/
596 assert_select 'a[href=?]', '/time_entries/new', :text => /Log time/
597 end
597 end
598
598
599 def test_index_my_spent_time
599 def test_index_my_spent_time
600 @request.session[:user_id] = 2
600 @request.session[:user_id] = 2
601 get :index, :user_id => 'me'
601 get :index, :user_id => 'me'
602 assert_response :success
602 assert_response :success
603 assert_template 'index'
603 assert_template 'index'
604 assert assigns(:entries).all? {|entry| entry.user_id == 2}
604 assert assigns(:entries).all? {|entry| entry.user_id == 2}
605 end
605 end
606
606
607 def test_index_at_project_level
607 def test_index_at_project_level
608 get :index, :project_id => 'ecookbook'
608 get :index, :project_id => 'ecookbook'
609 assert_response :success
609 assert_response :success
610 assert_template 'index'
610 assert_template 'index'
611 assert_not_nil assigns(:entries)
611 assert_not_nil assigns(:entries)
612 assert_equal 4, assigns(:entries).size
612 assert_equal 4, assigns(:entries).size
613 # project and subproject
613 # project and subproject
614 assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
614 assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
615 assert_not_nil assigns(:total_hours)
615 assert_not_nil assigns(:total_hours)
616 assert_equal "162.90", "%.2f" % assigns(:total_hours)
616 assert_equal "162.90", "%.2f" % assigns(:total_hours)
617 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
617 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
618 end
618 end
619
619
620 def test_index_with_display_subprojects_issues_to_false_should_not_include_subproject_entries
620 def test_index_with_display_subprojects_issues_to_false_should_not_include_subproject_entries
621 entry = TimeEntry.generate!(:project => Project.find(3))
621 entry = TimeEntry.generate!(:project => Project.find(3))
622
622
623 with_settings :display_subprojects_issues => '0' do
623 with_settings :display_subprojects_issues => '0' do
624 get :index, :project_id => 'ecookbook'
624 get :index, :project_id => 'ecookbook'
625 assert_response :success
625 assert_response :success
626 assert_template 'index'
626 assert_template 'index'
627 assert_not_include entry, assigns(:entries)
627 assert_not_include entry, assigns(:entries)
628 end
628 end
629 end
629 end
630
630
631 def test_index_with_display_subprojects_issues_to_false_and_subproject_filter_should_include_subproject_entries
631 def test_index_with_display_subprojects_issues_to_false_and_subproject_filter_should_include_subproject_entries
632 entry = TimeEntry.generate!(:project => Project.find(3))
632 entry = TimeEntry.generate!(:project => Project.find(3))
633
633
634 with_settings :display_subprojects_issues => '0' do
634 with_settings :display_subprojects_issues => '0' do
635 get :index, :project_id => 'ecookbook', :subproject_id => 3
635 get :index, :project_id => 'ecookbook', :subproject_id => 3
636 assert_response :success
636 assert_response :success
637 assert_template 'index'
637 assert_template 'index'
638 assert_include entry, assigns(:entries)
638 assert_include entry, assigns(:entries)
639 end
639 end
640 end
640 end
641
641
642 def test_index_at_project_level_with_date_range
642 def test_index_at_project_level_with_date_range
643 get :index, :project_id => 'ecookbook',
643 get :index, :project_id => 'ecookbook',
644 :f => ['spent_on'],
644 :f => ['spent_on'],
645 :op => {'spent_on' => '><'},
645 :op => {'spent_on' => '><'},
646 :v => {'spent_on' => ['2007-03-20', '2007-04-30']}
646 :v => {'spent_on' => ['2007-03-20', '2007-04-30']}
647 assert_response :success
647 assert_response :success
648 assert_template 'index'
648 assert_template 'index'
649 assert_not_nil assigns(:entries)
649 assert_not_nil assigns(:entries)
650 assert_equal 3, assigns(:entries).size
650 assert_equal 3, assigns(:entries).size
651 assert_not_nil assigns(:total_hours)
651 assert_not_nil assigns(:total_hours)
652 assert_equal "12.90", "%.2f" % assigns(:total_hours)
652 assert_equal "12.90", "%.2f" % assigns(:total_hours)
653 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
653 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
654 end
654 end
655
655
656 def test_index_at_project_level_with_date_range_using_from_and_to_params
656 def test_index_at_project_level_with_date_range_using_from_and_to_params
657 get :index, :project_id => 'ecookbook', :from => '2007-03-20', :to => '2007-04-30'
657 get :index, :project_id => 'ecookbook', :from => '2007-03-20', :to => '2007-04-30'
658 assert_response :success
658 assert_response :success
659 assert_template 'index'
659 assert_template 'index'
660 assert_not_nil assigns(:entries)
660 assert_not_nil assigns(:entries)
661 assert_equal 3, assigns(:entries).size
661 assert_equal 3, assigns(:entries).size
662 assert_not_nil assigns(:total_hours)
662 assert_not_nil assigns(:total_hours)
663 assert_equal "12.90", "%.2f" % assigns(:total_hours)
663 assert_equal "12.90", "%.2f" % assigns(:total_hours)
664 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
664 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
665 end
665 end
666
666
667 def test_index_at_project_level_with_period
667 def test_index_at_project_level_with_period
668 get :index, :project_id => 'ecookbook',
668 get :index, :project_id => 'ecookbook',
669 :f => ['spent_on'],
669 :f => ['spent_on'],
670 :op => {'spent_on' => '>t-'},
670 :op => {'spent_on' => '>t-'},
671 :v => {'spent_on' => ['7']}
671 :v => {'spent_on' => ['7']}
672 assert_response :success
672 assert_response :success
673 assert_template 'index'
673 assert_template 'index'
674 assert_not_nil assigns(:entries)
674 assert_not_nil assigns(:entries)
675 assert_not_nil assigns(:total_hours)
675 assert_not_nil assigns(:total_hours)
676 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
676 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
677 end
677 end
678
678
679 def test_index_at_issue_level
680 get :index, :issue_id => 1
681 assert_response :success
682 assert_template 'index'
683 assert_not_nil assigns(:entries)
684 assert_equal 2, assigns(:entries).size
685 assert_not_nil assigns(:total_hours)
686 assert_equal 154.25, assigns(:total_hours)
687 # display all time
688 assert_nil assigns(:from)
689 assert_nil assigns(:to)
690 assert_select 'form#query_form[action=?]', '/issues/1/time_entries'
691 end
692
693 def test_index_should_sort_by_spent_on_and_created_on
679 def test_index_should_sort_by_spent_on_and_created_on
694 t1 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:00:00', :activity_id => 10)
680 t1 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:00:00', :activity_id => 10)
695 t2 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:05:00', :activity_id => 10)
681 t2 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:05:00', :activity_id => 10)
696 t3 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-15', :created_on => '2012-06-16 20:10:00', :activity_id => 10)
682 t3 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-15', :created_on => '2012-06-16 20:10:00', :activity_id => 10)
697
683
698 get :index, :project_id => 1,
684 get :index, :project_id => 1,
699 :f => ['spent_on'],
685 :f => ['spent_on'],
700 :op => {'spent_on' => '><'},
686 :op => {'spent_on' => '><'},
701 :v => {'spent_on' => ['2012-06-15', '2012-06-16']}
687 :v => {'spent_on' => ['2012-06-15', '2012-06-16']}
702 assert_response :success
688 assert_response :success
703 assert_equal [t2, t1, t3], assigns(:entries)
689 assert_equal [t2, t1, t3], assigns(:entries)
704
690
705 get :index, :project_id => 1,
691 get :index, :project_id => 1,
706 :f => ['spent_on'],
692 :f => ['spent_on'],
707 :op => {'spent_on' => '><'},
693 :op => {'spent_on' => '><'},
708 :v => {'spent_on' => ['2012-06-15', '2012-06-16']},
694 :v => {'spent_on' => ['2012-06-15', '2012-06-16']},
709 :sort => 'spent_on'
695 :sort => 'spent_on'
710 assert_response :success
696 assert_response :success
711 assert_equal [t3, t1, t2], assigns(:entries)
697 assert_equal [t3, t1, t2], assigns(:entries)
712 end
698 end
713
699
714 def test_index_with_filter_on_issue_custom_field
700 def test_index_with_filter_on_issue_custom_field
715 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'})
701 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'})
716 entry = TimeEntry.generate!(:issue => issue, :hours => 2.5)
702 entry = TimeEntry.generate!(:issue => issue, :hours => 2.5)
717
703
718 get :index, :f => ['issue.cf_2'], :op => {'issue.cf_2' => '='}, :v => {'issue.cf_2' => ['filter_on_issue_custom_field']}
704 get :index, :f => ['issue.cf_2'], :op => {'issue.cf_2' => '='}, :v => {'issue.cf_2' => ['filter_on_issue_custom_field']}
719 assert_response :success
705 assert_response :success
720 assert_equal [entry], assigns(:entries)
706 assert_equal [entry], assigns(:entries)
721 end
707 end
722
708
723 def test_index_with_issue_custom_field_column
709 def test_index_with_issue_custom_field_column
724 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'})
710 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'})
725 entry = TimeEntry.generate!(:issue => issue, :hours => 2.5)
711 entry = TimeEntry.generate!(:issue => issue, :hours => 2.5)
726
712
727 get :index, :c => %w(project spent_on issue comments hours issue.cf_2)
713 get :index, :c => %w(project spent_on issue comments hours issue.cf_2)
728 assert_response :success
714 assert_response :success
729 assert_include :'issue.cf_2', assigns(:query).column_names
715 assert_include :'issue.cf_2', assigns(:query).column_names
730 assert_select 'td.issue_cf_2', :text => 'filter_on_issue_custom_field'
716 assert_select 'td.issue_cf_2', :text => 'filter_on_issue_custom_field'
731 end
717 end
732
718
733 def test_index_with_time_entry_custom_field_column
719 def test_index_with_time_entry_custom_field_column
734 field = TimeEntryCustomField.generate!(:field_format => 'string')
720 field = TimeEntryCustomField.generate!(:field_format => 'string')
735 entry = TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value'})
721 entry = TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value'})
736 field_name = "cf_#{field.id}"
722 field_name = "cf_#{field.id}"
737
723
738 get :index, :c => ["hours", field_name]
724 get :index, :c => ["hours", field_name]
739 assert_response :success
725 assert_response :success
740 assert_include field_name.to_sym, assigns(:query).column_names
726 assert_include field_name.to_sym, assigns(:query).column_names
741 assert_select "td.#{field_name}", :text => 'CF Value'
727 assert_select "td.#{field_name}", :text => 'CF Value'
742 end
728 end
743
729
744 def test_index_with_time_entry_custom_field_sorting
730 def test_index_with_time_entry_custom_field_sorting
745 field = TimeEntryCustomField.generate!(:field_format => 'string', :name => 'String Field')
731 field = TimeEntryCustomField.generate!(:field_format => 'string', :name => 'String Field')
746 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 1'})
732 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 1'})
747 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 3'})
733 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 3'})
748 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 2'})
734 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 2'})
749 field_name = "cf_#{field.id}"
735 field_name = "cf_#{field.id}"
750
736
751 get :index, :c => ["hours", field_name], :sort => field_name
737 get :index, :c => ["hours", field_name], :sort => field_name
752 assert_response :success
738 assert_response :success
753 assert_include field_name.to_sym, assigns(:query).column_names
739 assert_include field_name.to_sym, assigns(:query).column_names
754 assert_select "th a.sort", :text => 'String Field'
740 assert_select "th a.sort", :text => 'String Field'
755
741
756 # Make sure that values are properly sorted
742 # Make sure that values are properly sorted
757 values = assigns(:entries).map {|e| e.custom_field_value(field)}.compact
743 values = assigns(:entries).map {|e| e.custom_field_value(field)}.compact
758 assert_equal 3, values.size
744 assert_equal 3, values.size
759 assert_equal values.sort, values
745 assert_equal values.sort, values
760 end
746 end
761
747
762 def test_index_with_query
748 def test_index_with_query
763 query = TimeEntryQuery.new(:project_id => 1, :name => 'Time Entry Query', :visibility => 2)
749 query = TimeEntryQuery.new(:project_id => 1, :name => 'Time Entry Query', :visibility => 2)
764 query.save!
750 query.save!
765 @request.session[:user_id] = 2
751 @request.session[:user_id] = 2
766
752
767 get :index, :project_id => 'ecookbook', :query_id => query.id
753 get :index, :project_id => 'ecookbook', :query_id => query.id
768 assert_response :success
754 assert_response :success
769 assert_select 'h2', :text => query.name
755 assert_select 'h2', :text => query.name
770 assert_select '#sidebar a.selected', :text => query.name
756 assert_select '#sidebar a.selected', :text => query.name
771 end
757 end
772
758
773 def test_index_atom_feed
759 def test_index_atom_feed
774 get :index, :project_id => 1, :format => 'atom'
760 get :index, :project_id => 1, :format => 'atom'
775 assert_response :success
761 assert_response :success
776 assert_equal 'application/atom+xml', @response.content_type
762 assert_equal 'application/atom+xml', @response.content_type
777 assert_not_nil assigns(:items)
763 assert_not_nil assigns(:items)
778 assert assigns(:items).first.is_a?(TimeEntry)
764 assert assigns(:items).first.is_a?(TimeEntry)
779 end
765 end
780
766
781 def test_index_at_project_level_should_include_csv_export_dialog
767 def test_index_at_project_level_should_include_csv_export_dialog
782 get :index, :project_id => 'ecookbook',
768 get :index, :project_id => 'ecookbook',
783 :f => ['spent_on'],
769 :f => ['spent_on'],
784 :op => {'spent_on' => '>='},
770 :op => {'spent_on' => '>='},
785 :v => {'spent_on' => ['2007-04-01']},
771 :v => {'spent_on' => ['2007-04-01']},
786 :c => ['spent_on', 'user']
772 :c => ['spent_on', 'user']
787 assert_response :success
773 assert_response :success
788
774
789 assert_select '#csv-export-options' do
775 assert_select '#csv-export-options' do
790 assert_select 'form[action=?][method=get]', '/projects/ecookbook/time_entries.csv' do
776 assert_select 'form[action=?][method=get]', '/projects/ecookbook/time_entries.csv' do
791 # filter
777 # filter
792 assert_select 'input[name=?][value=?]', 'f[]', 'spent_on'
778 assert_select 'input[name=?][value=?]', 'f[]', 'spent_on'
793 assert_select 'input[name=?][value=?]', 'op[spent_on]', '>='
779 assert_select 'input[name=?][value=?]', 'op[spent_on]', '>='
794 assert_select 'input[name=?][value=?]', 'v[spent_on][]', '2007-04-01'
780 assert_select 'input[name=?][value=?]', 'v[spent_on][]', '2007-04-01'
795 # columns
781 # columns
796 assert_select 'input[name=?][value=?]', 'c[]', 'spent_on'
782 assert_select 'input[name=?][value=?]', 'c[]', 'spent_on'
797 assert_select 'input[name=?][value=?]', 'c[]', 'user'
783 assert_select 'input[name=?][value=?]', 'c[]', 'user'
798 assert_select 'input[name=?]', 'c[]', 2
784 assert_select 'input[name=?]', 'c[]', 2
799 end
785 end
800 end
786 end
801 end
787 end
802
788
803 def test_index_cross_project_should_include_csv_export_dialog
789 def test_index_cross_project_should_include_csv_export_dialog
804 get :index
790 get :index
805 assert_response :success
791 assert_response :success
806
792
807 assert_select '#csv-export-options' do
793 assert_select '#csv-export-options' do
808 assert_select 'form[action=?][method=get]', '/time_entries.csv'
794 assert_select 'form[action=?][method=get]', '/time_entries.csv'
809 end
795 end
810 end
796 end
811
797
812 def test_index_at_issue_level_should_include_csv_export_dialog
813 get :index, :issue_id => 3
814 assert_response :success
815
816 assert_select '#csv-export-options' do
817 assert_select 'form[action=?][method=get]', '/issues/3/time_entries.csv'
818 end
819 end
820
821 def test_index_csv_all_projects
798 def test_index_csv_all_projects
822 with_settings :date_format => '%m/%d/%Y' do
799 with_settings :date_format => '%m/%d/%Y' do
823 get :index, :format => 'csv'
800 get :index, :format => 'csv'
824 assert_response :success
801 assert_response :success
825 assert_equal 'text/csv; header=present', response.content_type
802 assert_equal 'text/csv; header=present', response.content_type
826 end
803 end
827 end
804 end
828
805
829 def test_index_csv
806 def test_index_csv
830 with_settings :date_format => '%m/%d/%Y' do
807 with_settings :date_format => '%m/%d/%Y' do
831 get :index, :project_id => 1, :format => 'csv'
808 get :index, :project_id => 1, :format => 'csv'
832 assert_response :success
809 assert_response :success
833 assert_equal 'text/csv; header=present', response.content_type
810 assert_equal 'text/csv; header=present', response.content_type
834 end
811 end
835 end
812 end
836
813
837 def test_index_csv_should_fill_issue_column_with_tracker_id_and_subject
814 def test_index_csv_should_fill_issue_column_with_tracker_id_and_subject
838 issue = Issue.find(1)
815 issue = Issue.find(1)
839 entry = TimeEntry.generate!(:issue => issue, :comments => "Issue column content test")
816 entry = TimeEntry.generate!(:issue => issue, :comments => "Issue column content test")
840
817
841 get :index, :format => 'csv'
818 get :index, :format => 'csv'
842 line = response.body.split("\n").detect {|l| l.include?(entry.comments)}
819 line = response.body.split("\n").detect {|l| l.include?(entry.comments)}
843 assert_not_nil line
820 assert_not_nil line
844 assert_include "#{issue.tracker} #1: #{issue.subject}", line
821 assert_include "#{issue.tracker} #1: #{issue.subject}", line
845 end
822 end
846 end
823 end
@@ -1,66 +1,60
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class RoutingTimelogsTest < Redmine::RoutingTest
20 class RoutingTimelogsTest < Redmine::RoutingTest
21 def test_timelogs_global
21 def test_timelogs_global
22 should_route 'GET /time_entries' => 'timelog#index'
22 should_route 'GET /time_entries' => 'timelog#index'
23 should_route 'GET /time_entries.csv' => 'timelog#index', :format => 'csv'
23 should_route 'GET /time_entries.csv' => 'timelog#index', :format => 'csv'
24 should_route 'GET /time_entries.atom' => 'timelog#index', :format => 'atom'
24 should_route 'GET /time_entries.atom' => 'timelog#index', :format => 'atom'
25 should_route 'GET /time_entries/new' => 'timelog#new'
25 should_route 'GET /time_entries/new' => 'timelog#new'
26 should_route 'POST /time_entries/new' => 'timelog#new'
26 should_route 'POST /time_entries/new' => 'timelog#new'
27 should_route 'POST /time_entries' => 'timelog#create'
27 should_route 'POST /time_entries' => 'timelog#create'
28
28
29 should_route 'GET /time_entries/22/edit' => 'timelog#edit', :id => '22'
29 should_route 'GET /time_entries/22/edit' => 'timelog#edit', :id => '22'
30 should_route 'PUT /time_entries/22' => 'timelog#update', :id => '22'
30 should_route 'PUT /time_entries/22' => 'timelog#update', :id => '22'
31 should_route 'DELETE /time_entries/22' => 'timelog#destroy', :id => '22'
31 should_route 'DELETE /time_entries/22' => 'timelog#destroy', :id => '22'
32 end
32 end
33
33
34 def test_timelogs_scoped_under_project
34 def test_timelogs_scoped_under_project
35 should_route 'GET /projects/foo/time_entries' => 'timelog#index', :project_id => 'foo'
35 should_route 'GET /projects/foo/time_entries' => 'timelog#index', :project_id => 'foo'
36 should_route 'GET /projects/foo/time_entries.csv' => 'timelog#index', :project_id => 'foo', :format => 'csv'
36 should_route 'GET /projects/foo/time_entries.csv' => 'timelog#index', :project_id => 'foo', :format => 'csv'
37 should_route 'GET /projects/foo/time_entries.atom' => 'timelog#index', :project_id => 'foo', :format => 'atom'
37 should_route 'GET /projects/foo/time_entries.atom' => 'timelog#index', :project_id => 'foo', :format => 'atom'
38 should_route 'GET /projects/foo/time_entries/new' => 'timelog#new', :project_id => 'foo'
38 should_route 'GET /projects/foo/time_entries/new' => 'timelog#new', :project_id => 'foo'
39 should_route 'POST /projects/foo/time_entries' => 'timelog#create', :project_id => 'foo'
39 should_route 'POST /projects/foo/time_entries' => 'timelog#create', :project_id => 'foo'
40 end
40 end
41
41
42 def test_timelogs_scoped_under_issues
42 def test_timelogs_scoped_under_issues
43 should_route 'GET /issues/234/time_entries' => 'timelog#index', :issue_id => '234'
43 should_route 'GET /issues/234/time_entries/new' => 'timelog#new', :issue_id => '234'
44 should_route 'GET /issues/234/time_entries.csv' => 'timelog#index', :issue_id => '234', :format => 'csv'
45 should_route 'GET /issues/234/time_entries.atom' => 'timelog#index', :issue_id => '234', :format => 'atom'
46 should_route 'GET /issues/234/time_entries/new' => 'timelog#new', :issue_id => '234'
47 should_route 'POST /issues/234/time_entries' => 'timelog#create', :issue_id => '234'
44 should_route 'POST /issues/234/time_entries' => 'timelog#create', :issue_id => '234'
48 end
45 end
49
46
50 def test_timelogs_report
47 def test_timelogs_report
51 should_route 'GET /time_entries/report' => 'timelog#report'
48 should_route 'GET /time_entries/report' => 'timelog#report'
52 should_route 'GET /time_entries/report.csv' => 'timelog#report', :format => 'csv'
49 should_route 'GET /time_entries/report.csv' => 'timelog#report', :format => 'csv'
53
50
54 should_route 'GET /projects/foo/time_entries/report' => 'timelog#report', :project_id => 'foo'
51 should_route 'GET /projects/foo/time_entries/report' => 'timelog#report', :project_id => 'foo'
55 should_route 'GET /projects/foo/time_entries/report.csv' => 'timelog#report', :project_id => 'foo', :format => 'csv'
52 should_route 'GET /projects/foo/time_entries/report.csv' => 'timelog#report', :project_id => 'foo', :format => 'csv'
56
57 should_route 'GET /issues/234/time_entries/report' => 'timelog#report', :issue_id => '234'
58 should_route 'GET /issues/234/time_entries/report.csv' => 'timelog#report', :issue_id => '234', :format => 'csv'
59 end
53 end
60
54
61 def test_timelogs_bulk_edit
55 def test_timelogs_bulk_edit
62 should_route 'GET /time_entries/bulk_edit' => 'timelog#bulk_edit'
56 should_route 'GET /time_entries/bulk_edit' => 'timelog#bulk_edit'
63 should_route 'POST /time_entries/bulk_update' => 'timelog#bulk_update'
57 should_route 'POST /time_entries/bulk_update' => 'timelog#bulk_update'
64 should_route 'DELETE /time_entries/destroy' => 'timelog#destroy'
58 should_route 'DELETE /time_entries/destroy' => 'timelog#destroy'
65 end
59 end
66 end
60 end
@@ -1,47 +1,43
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require File.expand_path('../../../test_helper', __FILE__)
20 require File.expand_path('../../../test_helper', __FILE__)
21
21
22 class RoutesHelperTest < ActionView::TestCase
22 class RoutesHelperTest < ActionView::TestCase
23 fixtures :projects, :issues
23 fixtures :projects, :issues
24
24
25 include Rails.application.routes.url_helpers
25 include Rails.application.routes.url_helpers
26
26
27 def test_time_entries_path
27 def test_time_entries_path
28 assert_equal '/projects/ecookbook/time_entries', _time_entries_path(Project.find(1), nil)
28 assert_equal '/projects/ecookbook/time_entries', _time_entries_path(Project.find(1), nil)
29 assert_equal '/issues/1/time_entries', _time_entries_path(Project.find(1), Issue.find(1))
30 assert_equal '/issues/1/time_entries', _time_entries_path(nil, Issue.find(1))
31 assert_equal '/time_entries', _time_entries_path(nil, nil)
29 assert_equal '/time_entries', _time_entries_path(nil, nil)
32 end
30 end
33
31
34 def test_report_time_entries_path
32 def test_report_time_entries_path
35 assert_equal '/projects/ecookbook/time_entries/report', _report_time_entries_path(Project.find(1), nil)
33 assert_equal '/projects/ecookbook/time_entries/report', _report_time_entries_path(Project.find(1), nil)
36 assert_equal '/issues/1/time_entries/report', _report_time_entries_path(Project.find(1), Issue.find(1))
37 assert_equal '/issues/1/time_entries/report', _report_time_entries_path(nil, Issue.find(1))
38 assert_equal '/time_entries/report', _report_time_entries_path(nil, nil)
34 assert_equal '/time_entries/report', _report_time_entries_path(nil, nil)
39 end
35 end
40
36
41 def test_new_time_entry_path
37 def test_new_time_entry_path
42 assert_equal '/projects/ecookbook/time_entries/new', _new_time_entry_path(Project.find(1), nil)
38 assert_equal '/projects/ecookbook/time_entries/new', _new_time_entry_path(Project.find(1), nil)
43 assert_equal '/issues/1/time_entries/new', _new_time_entry_path(Project.find(1), Issue.find(1))
39 assert_equal '/issues/1/time_entries/new', _new_time_entry_path(Project.find(1), Issue.find(1))
44 assert_equal '/issues/1/time_entries/new', _new_time_entry_path(nil, Issue.find(1))
40 assert_equal '/issues/1/time_entries/new', _new_time_entry_path(nil, Issue.find(1))
45 assert_equal '/time_entries/new', _new_time_entry_path(nil, nil)
41 assert_equal '/time_entries/new', _new_time_entry_path(nil, nil)
46 end
42 end
47 end
43 end
General Comments 0
You need to be logged in to leave comments. Login now