##// END OF EJS Templates
Adds TimeEntryQuery for listing time entries....
Jean-Philippe Lang -
r10740:f8895a7cdd9c
parent child
Show More
@@ -0,0 +1,65
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 class TimeEntryQuery < Query
19
20 self.queried_class = TimeEntry
21
22 self.available_columns = [
23 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
24 QueryColumn.new(:spent_on, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"]),
25 QueryColumn.new(:user, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
26 QueryColumn.new(:activity, :sortable => "#{TimeEntryActivity.table_name}.position", :groupable => true),
27 QueryColumn.new(:issue, :sortable => "#{Issue.table_name}.id"),
28 QueryColumn.new(:comments),
29 QueryColumn.new(:hours, :sortable => "#{TimeEntry.table_name}.hours"),
30 ]
31
32 def initialize(attributes=nil, *args)
33 super attributes
34 self.filters ||= {}
35 add_filter('spent_on', '*') unless filters.present?
36 end
37
38 def available_filters
39 return @available_filters if @available_filters
40 @available_filters = {
41 "spent_on" => { :type => :date_past, :order => 0 }
42 }
43 @available_filters.each do |field, options|
44 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
45 end
46 @available_filters
47 end
48
49 def default_columns_names
50 @default_columns_names ||= [:project, :spent_on, :user, :activity, :issue, :comments, :hours]
51 end
52
53 # Accepts :from/:to params as shortcut filters
54 def build_from_params(params)
55 super
56 if params[:from].present? && params[:to].present?
57 add_filter('spent_on', '><', [params[:from], params[:to]])
58 elsif params[:from].present?
59 add_filter('spent_on', '>=', [params[:from]])
60 elsif params[:to].present?
61 add_filter('spent_on', '<=', [params[:to]])
62 end
63 self
64 end
65 end
@@ -1,347 +1,350
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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_project_for_new_time_entry, :only => [:create]
21 before_filter :find_project_for_new_time_entry, :only => [:create]
22 before_filter :find_time_entry, :only => [:show, :edit, :update]
22 before_filter :find_time_entry, :only => [:show, :edit, :update]
23 before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
23 before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
24 before_filter :authorize, :except => [:new, :index, :report]
24 before_filter :authorize, :except => [:new, :index, :report]
25
25
26 before_filter :find_optional_project, :only => [:index, :report]
26 before_filter :find_optional_project, :only => [:index, :report]
27 before_filter :find_optional_project_for_new_time_entry, :only => [:new]
27 before_filter :find_optional_project_for_new_time_entry, :only => [:new]
28 before_filter :authorize_global, :only => [:new, :index, :report]
28 before_filter :authorize_global, :only => [:new, :index, :report]
29
29
30 accept_rss_auth :index
30 accept_rss_auth :index
31 accept_api_auth :index, :show, :create, :update, :destroy
31 accept_api_auth :index, :show, :create, :update, :destroy
32
32
33 helper :sort
33 helper :sort
34 include SortHelper
34 include SortHelper
35 helper :issues
35 helper :issues
36 include TimelogHelper
36 include TimelogHelper
37 helper :custom_fields
37 helper :custom_fields
38 include CustomFieldsHelper
38 include CustomFieldsHelper
39 helper :queries
39
40
40 def index
41 def index
41 sort_init 'spent_on', 'desc'
42 @query = TimeEntryQuery.build_from_params(params, :name => '_')
42 sort_update 'spent_on' => ['spent_on', "#{TimeEntry.table_name}.created_on"],
43 scope = time_entry_scope
43 'user' => 'user_id',
44 'activity' => 'activity_id',
45 'project' => "#{Project.table_name}.name",
46 'issue' => 'issue_id',
47 'hours' => 'hours'
48
44
49 retrieve_date_range
45 sort_init(@query.sort_criteria.empty? ? [['spent_on', 'desc']] : @query.sort_criteria)
50
46 sort_update(@query.sortable_columns)
51 scope = TimeEntry.visible.spent_between(@from, @to)
52 if @issue
53 scope = scope.on_issue(@issue)
54 elsif @project
55 scope = scope.on_project(@project, Setting.display_subprojects_issues?)
56 end
57
47
58 respond_to do |format|
48 respond_to do |format|
59 format.html {
49 format.html {
60 # Paginate results
50 # Paginate results
61 @entry_count = scope.count
51 @entry_count = scope.count
62 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
52 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
63 @entries = scope.all(
53 @entries = scope.all(
64 :include => [:project, :activity, :user, {:issue => :tracker}],
54 :include => [:project, :activity, :user, {:issue => :tracker}],
65 :order => sort_clause,
55 :order => sort_clause,
66 :limit => @entry_pages.items_per_page,
56 :limit => @entry_pages.items_per_page,
67 :offset => @entry_pages.current.offset
57 :offset => @entry_pages.current.offset
68 )
58 )
69 @total_hours = scope.sum(:hours).to_f
59 @total_hours = scope.sum(:hours).to_f
70
60
71 render :layout => !request.xhr?
61 render :layout => !request.xhr?
72 }
62 }
73 format.api {
63 format.api {
74 @entry_count = scope.count
64 @entry_count = scope.count
75 @offset, @limit = api_offset_and_limit
65 @offset, @limit = api_offset_and_limit
76 @entries = scope.all(
66 @entries = scope.all(
77 :include => [:project, :activity, :user, {:issue => :tracker}],
67 :include => [:project, :activity, :user, {:issue => :tracker}],
78 :order => sort_clause,
68 :order => sort_clause,
79 :limit => @limit,
69 :limit => @limit,
80 :offset => @offset
70 :offset => @offset
81 )
71 )
82 }
72 }
83 format.atom {
73 format.atom {
84 entries = scope.all(
74 entries = scope.all(
85 :include => [:project, :activity, :user, {:issue => :tracker}],
75 :include => [:project, :activity, :user, {:issue => :tracker}],
86 :order => "#{TimeEntry.table_name}.created_on DESC",
76 :order => "#{TimeEntry.table_name}.created_on DESC",
87 :limit => Setting.feeds_limit.to_i
77 :limit => Setting.feeds_limit.to_i
88 )
78 )
89 render_feed(entries, :title => l(:label_spent_time))
79 render_feed(entries, :title => l(:label_spent_time))
90 }
80 }
91 format.csv {
81 format.csv {
92 # Export all entries
82 # Export all entries
93 @entries = scope.all(
83 @entries = scope.all(
94 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
84 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
95 :order => sort_clause
85 :order => sort_clause
96 )
86 )
97 send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv')
87 send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv')
98 }
88 }
99 end
89 end
100 end
90 end
101
91
102 def report
92 def report
103 retrieve_date_range
93 @query = TimeEntryQuery.build_from_params(params, :name => '_')
104 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], @from, @to)
94 scope = time_entry_scope
95
96 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], scope)
105
97
106 respond_to do |format|
98 respond_to do |format|
107 format.html { render :layout => !request.xhr? }
99 format.html { render :layout => !request.xhr? }
108 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
100 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
109 end
101 end
110 end
102 end
111
103
112 def show
104 def show
113 respond_to do |format|
105 respond_to do |format|
114 # TODO: Implement html response
106 # TODO: Implement html response
115 format.html { render :nothing => true, :status => 406 }
107 format.html { render :nothing => true, :status => 406 }
116 format.api
108 format.api
117 end
109 end
118 end
110 end
119
111
120 def new
112 def new
121 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
113 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
122 @time_entry.safe_attributes = params[:time_entry]
114 @time_entry.safe_attributes = params[:time_entry]
123 end
115 end
124
116
125 def create
117 def create
126 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
118 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
127 @time_entry.safe_attributes = params[:time_entry]
119 @time_entry.safe_attributes = params[:time_entry]
128
120
129 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
121 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
130
122
131 if @time_entry.save
123 if @time_entry.save
132 respond_to do |format|
124 respond_to do |format|
133 format.html {
125 format.html {
134 flash[:notice] = l(:notice_successful_create)
126 flash[:notice] = l(:notice_successful_create)
135 if params[:continue]
127 if params[:continue]
136 if params[:project_id]
128 if params[:project_id]
137 redirect_to :action => 'new', :project_id => @time_entry.project, :issue_id => @time_entry.issue,
129 redirect_to :action => 'new', :project_id => @time_entry.project, :issue_id => @time_entry.issue,
138 :time_entry => {:issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id},
130 :time_entry => {:issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id},
139 :back_url => params[:back_url]
131 :back_url => params[:back_url]
140 else
132 else
141 redirect_to :action => 'new',
133 redirect_to :action => 'new',
142 :time_entry => {:project_id => @time_entry.project_id, :issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id},
134 :time_entry => {:project_id => @time_entry.project_id, :issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id},
143 :back_url => params[:back_url]
135 :back_url => params[:back_url]
144 end
136 end
145 else
137 else
146 redirect_back_or_default :action => 'index', :project_id => @time_entry.project
138 redirect_back_or_default :action => 'index', :project_id => @time_entry.project
147 end
139 end
148 }
140 }
149 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
141 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
150 end
142 end
151 else
143 else
152 respond_to do |format|
144 respond_to do |format|
153 format.html { render :action => 'new' }
145 format.html { render :action => 'new' }
154 format.api { render_validation_errors(@time_entry) }
146 format.api { render_validation_errors(@time_entry) }
155 end
147 end
156 end
148 end
157 end
149 end
158
150
159 def edit
151 def edit
160 @time_entry.safe_attributes = params[:time_entry]
152 @time_entry.safe_attributes = params[:time_entry]
161 end
153 end
162
154
163 def update
155 def update
164 @time_entry.safe_attributes = params[:time_entry]
156 @time_entry.safe_attributes = params[:time_entry]
165
157
166 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
158 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
167
159
168 if @time_entry.save
160 if @time_entry.save
169 respond_to do |format|
161 respond_to do |format|
170 format.html {
162 format.html {
171 flash[:notice] = l(:notice_successful_update)
163 flash[:notice] = l(:notice_successful_update)
172 redirect_back_or_default :action => 'index', :project_id => @time_entry.project
164 redirect_back_or_default :action => 'index', :project_id => @time_entry.project
173 }
165 }
174 format.api { render_api_ok }
166 format.api { render_api_ok }
175 end
167 end
176 else
168 else
177 respond_to do |format|
169 respond_to do |format|
178 format.html { render :action => 'edit' }
170 format.html { render :action => 'edit' }
179 format.api { render_validation_errors(@time_entry) }
171 format.api { render_validation_errors(@time_entry) }
180 end
172 end
181 end
173 end
182 end
174 end
183
175
184 def bulk_edit
176 def bulk_edit
185 @available_activities = TimeEntryActivity.shared.active
177 @available_activities = TimeEntryActivity.shared.active
186 @custom_fields = TimeEntry.first.available_custom_fields
178 @custom_fields = TimeEntry.first.available_custom_fields
187 end
179 end
188
180
189 def bulk_update
181 def bulk_update
190 attributes = parse_params_for_bulk_time_entry_attributes(params)
182 attributes = parse_params_for_bulk_time_entry_attributes(params)
191
183
192 unsaved_time_entry_ids = []
184 unsaved_time_entry_ids = []
193 @time_entries.each do |time_entry|
185 @time_entries.each do |time_entry|
194 time_entry.reload
186 time_entry.reload
195 time_entry.safe_attributes = attributes
187 time_entry.safe_attributes = attributes
196 call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
188 call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
197 unless time_entry.save
189 unless time_entry.save
198 # Keep unsaved time_entry ids to display them in flash error
190 # Keep unsaved time_entry ids to display them in flash error
199 unsaved_time_entry_ids << time_entry.id
191 unsaved_time_entry_ids << time_entry.id
200 end
192 end
201 end
193 end
202 set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
194 set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
203 redirect_back_or_default({:controller => 'timelog', :action => 'index', :project_id => @projects.first})
195 redirect_back_or_default({:controller => 'timelog', :action => 'index', :project_id => @projects.first})
204 end
196 end
205
197
206 def destroy
198 def destroy
207 destroyed = TimeEntry.transaction do
199 destroyed = TimeEntry.transaction do
208 @time_entries.each do |t|
200 @time_entries.each do |t|
209 unless t.destroy && t.destroyed?
201 unless t.destroy && t.destroyed?
210 raise ActiveRecord::Rollback
202 raise ActiveRecord::Rollback
211 end
203 end
212 end
204 end
213 end
205 end
214
206
215 respond_to do |format|
207 respond_to do |format|
216 format.html {
208 format.html {
217 if destroyed
209 if destroyed
218 flash[:notice] = l(:notice_successful_delete)
210 flash[:notice] = l(:notice_successful_delete)
219 else
211 else
220 flash[:error] = l(:notice_unable_delete_time_entry)
212 flash[:error] = l(:notice_unable_delete_time_entry)
221 end
213 end
222 redirect_back_or_default(:action => 'index', :project_id => @projects.first)
214 redirect_back_or_default(:action => 'index', :project_id => @projects.first)
223 }
215 }
224 format.api {
216 format.api {
225 if destroyed
217 if destroyed
226 render_api_ok
218 render_api_ok
227 else
219 else
228 render_validation_errors(@time_entries)
220 render_validation_errors(@time_entries)
229 end
221 end
230 }
222 }
231 end
223 end
232 end
224 end
233
225
234 private
226 private
235 def find_time_entry
227 def find_time_entry
236 @time_entry = TimeEntry.find(params[:id])
228 @time_entry = TimeEntry.find(params[:id])
237 unless @time_entry.editable_by?(User.current)
229 unless @time_entry.editable_by?(User.current)
238 render_403
230 render_403
239 return false
231 return false
240 end
232 end
241 @project = @time_entry.project
233 @project = @time_entry.project
242 rescue ActiveRecord::RecordNotFound
234 rescue ActiveRecord::RecordNotFound
243 render_404
235 render_404
244 end
236 end
245
237
246 def find_time_entries
238 def find_time_entries
247 @time_entries = TimeEntry.find_all_by_id(params[:id] || params[:ids])
239 @time_entries = TimeEntry.find_all_by_id(params[:id] || params[:ids])
248 raise ActiveRecord::RecordNotFound if @time_entries.empty?
240 raise ActiveRecord::RecordNotFound if @time_entries.empty?
249 @projects = @time_entries.collect(&:project).compact.uniq
241 @projects = @time_entries.collect(&:project).compact.uniq
250 @project = @projects.first if @projects.size == 1
242 @project = @projects.first if @projects.size == 1
251 rescue ActiveRecord::RecordNotFound
243 rescue ActiveRecord::RecordNotFound
252 render_404
244 render_404
253 end
245 end
254
246
255 def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
247 def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
256 if unsaved_time_entry_ids.empty?
248 if unsaved_time_entry_ids.empty?
257 flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
249 flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
258 else
250 else
259 flash[:error] = l(:notice_failed_to_save_time_entries,
251 flash[:error] = l(:notice_failed_to_save_time_entries,
260 :count => unsaved_time_entry_ids.size,
252 :count => unsaved_time_entry_ids.size,
261 :total => time_entries.size,
253 :total => time_entries.size,
262 :ids => '#' + unsaved_time_entry_ids.join(', #'))
254 :ids => '#' + unsaved_time_entry_ids.join(', #'))
263 end
255 end
264 end
256 end
265
257
266 def find_optional_project_for_new_time_entry
258 def find_optional_project_for_new_time_entry
267 if (project_id = (params[:project_id] || params[:time_entry] && params[:time_entry][:project_id])).present?
259 if (project_id = (params[:project_id] || params[:time_entry] && params[:time_entry][:project_id])).present?
268 @project = Project.find(project_id)
260 @project = Project.find(project_id)
269 end
261 end
270 if (issue_id = (params[:issue_id] || params[:time_entry] && params[:time_entry][:issue_id])).present?
262 if (issue_id = (params[:issue_id] || params[:time_entry] && params[:time_entry][:issue_id])).present?
271 @issue = Issue.find(issue_id)
263 @issue = Issue.find(issue_id)
272 @project ||= @issue.project
264 @project ||= @issue.project
273 end
265 end
274 rescue ActiveRecord::RecordNotFound
266 rescue ActiveRecord::RecordNotFound
275 render_404
267 render_404
276 end
268 end
277
269
278 def find_project_for_new_time_entry
270 def find_project_for_new_time_entry
279 find_optional_project_for_new_time_entry
271 find_optional_project_for_new_time_entry
280 if @project.nil?
272 if @project.nil?
281 render_404
273 render_404
282 end
274 end
283 end
275 end
284
276
285 def find_optional_project
277 def find_optional_project
286 if !params[:issue_id].blank?
278 if !params[:issue_id].blank?
287 @issue = Issue.find(params[:issue_id])
279 @issue = Issue.find(params[:issue_id])
288 @project = @issue.project
280 @project = @issue.project
289 elsif !params[:project_id].blank?
281 elsif !params[:project_id].blank?
290 @project = Project.find(params[:project_id])
282 @project = Project.find(params[:project_id])
291 end
283 end
292 end
284 end
293
285
286 # Returns the TimeEntry scope for index and report actions
287 def time_entry_scope
288 scope = TimeEntry.visible.where(@query.statement)
289 if @issue
290 scope = scope.on_issue(@issue)
291 elsif @project
292 scope = scope.on_project(@project, Setting.display_subprojects_issues?)
293 end
294 scope
295 end
296
294 # Retrieves the date range based on predefined ranges or specific from/to param dates
297 # Retrieves the date range based on predefined ranges or specific from/to param dates
295 def retrieve_date_range
298 def retrieve_date_range
296 @free_period = false
299 @free_period = false
297 @from, @to = nil, nil
300 @from, @to = nil, nil
298
301
299 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
302 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
300 case params[:period].to_s
303 case params[:period].to_s
301 when 'today'
304 when 'today'
302 @from = @to = Date.today
305 @from = @to = Date.today
303 when 'yesterday'
306 when 'yesterday'
304 @from = @to = Date.today - 1
307 @from = @to = Date.today - 1
305 when 'current_week'
308 when 'current_week'
306 @from = Date.today - (Date.today.cwday - 1)%7
309 @from = Date.today - (Date.today.cwday - 1)%7
307 @to = @from + 6
310 @to = @from + 6
308 when 'last_week'
311 when 'last_week'
309 @from = Date.today - 7 - (Date.today.cwday - 1)%7
312 @from = Date.today - 7 - (Date.today.cwday - 1)%7
310 @to = @from + 6
313 @to = @from + 6
311 when 'last_2_weeks'
314 when 'last_2_weeks'
312 @from = Date.today - 14 - (Date.today.cwday - 1)%7
315 @from = Date.today - 14 - (Date.today.cwday - 1)%7
313 @to = @from + 13
316 @to = @from + 13
314 when '7_days'
317 when '7_days'
315 @from = Date.today - 7
318 @from = Date.today - 7
316 @to = Date.today
319 @to = Date.today
317 when 'current_month'
320 when 'current_month'
318 @from = Date.civil(Date.today.year, Date.today.month, 1)
321 @from = Date.civil(Date.today.year, Date.today.month, 1)
319 @to = (@from >> 1) - 1
322 @to = (@from >> 1) - 1
320 when 'last_month'
323 when 'last_month'
321 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
324 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
322 @to = (@from >> 1) - 1
325 @to = (@from >> 1) - 1
323 when '30_days'
326 when '30_days'
324 @from = Date.today - 30
327 @from = Date.today - 30
325 @to = Date.today
328 @to = Date.today
326 when 'current_year'
329 when 'current_year'
327 @from = Date.civil(Date.today.year, 1, 1)
330 @from = Date.civil(Date.today.year, 1, 1)
328 @to = Date.civil(Date.today.year, 12, 31)
331 @to = Date.civil(Date.today.year, 12, 31)
329 end
332 end
330 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
333 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
331 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
334 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
332 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
335 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
333 @free_period = true
336 @free_period = true
334 else
337 else
335 # default
338 # default
336 end
339 end
337
340
338 @from, @to = @to, @from if @from && @to && @from > @to
341 @from, @to = @to, @from if @from && @to && @from > @to
339 end
342 end
340
343
341 def parse_params_for_bulk_time_entry_attributes(params)
344 def parse_params_for_bulk_time_entry_attributes(params)
342 attributes = (params[:time_entry] || {}).reject {|k,v| v.blank?}
345 attributes = (params[:time_entry] || {}).reject {|k,v| v.blank?}
343 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
346 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
344 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
347 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
345 attributes
348 attributes
346 end
349 end
347 end
350 end
@@ -1,731 +1,769
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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, :default_order
19 attr_accessor :name, :sortable, :groupable, :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.default_order = options[:default_order]
29 self.default_order = options[:default_order]
30 @inline = options.key?(:inline) ? options[:inline] : true
30 @inline = options.key?(:inline) ? options[:inline] : true
31 @caption_key = options[:caption] || "field_#{name}"
31 @caption_key = options[:caption] || "field_#{name}"
32 end
32 end
33
33
34 def caption
34 def caption
35 l(@caption_key)
35 l(@caption_key)
36 end
36 end
37
37
38 # Returns true if the column is sortable, otherwise false
38 # Returns true if the column is sortable, otherwise false
39 def sortable?
39 def sortable?
40 !@sortable.nil?
40 !@sortable.nil?
41 end
41 end
42
42
43 def sortable
43 def sortable
44 @sortable.is_a?(Proc) ? @sortable.call : @sortable
44 @sortable.is_a?(Proc) ? @sortable.call : @sortable
45 end
45 end
46
46
47 def inline?
47 def inline?
48 @inline
48 @inline
49 end
49 end
50
50
51 def value(object)
51 def value(object)
52 object.send name
52 object.send name
53 end
53 end
54
54
55 def css_classes
55 def css_classes
56 name
56 name
57 end
57 end
58 end
58 end
59
59
60 class QueryCustomFieldColumn < QueryColumn
60 class QueryCustomFieldColumn < QueryColumn
61
61
62 def initialize(custom_field)
62 def initialize(custom_field)
63 self.name = "cf_#{custom_field.id}".to_sym
63 self.name = "cf_#{custom_field.id}".to_sym
64 self.sortable = custom_field.order_statement || false
64 self.sortable = custom_field.order_statement || false
65 self.groupable = custom_field.group_statement || false
65 self.groupable = custom_field.group_statement || false
66 @inline = true
66 @inline = true
67 @cf = custom_field
67 @cf = custom_field
68 end
68 end
69
69
70 def caption
70 def caption
71 @cf.name
71 @cf.name
72 end
72 end
73
73
74 def custom_field
74 def custom_field
75 @cf
75 @cf
76 end
76 end
77
77
78 def value(object)
78 def value(object)
79 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
79 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
80 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
80 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
81 end
81 end
82
82
83 def css_classes
83 def css_classes
84 @css_classes ||= "#{name} #{@cf.field_format}"
84 @css_classes ||= "#{name} #{@cf.field_format}"
85 end
85 end
86 end
86 end
87
87
88 class Query < ActiveRecord::Base
88 class Query < ActiveRecord::Base
89 class StatementInvalid < ::ActiveRecord::StatementInvalid
89 class StatementInvalid < ::ActiveRecord::StatementInvalid
90 end
90 end
91
91
92 belongs_to :project
92 belongs_to :project
93 belongs_to :user
93 belongs_to :user
94 serialize :filters
94 serialize :filters
95 serialize :column_names
95 serialize :column_names
96 serialize :sort_criteria, Array
96 serialize :sort_criteria, Array
97
97
98 attr_protected :project_id, :user_id
98 attr_protected :project_id, :user_id
99
99
100 validates_presence_of :name
100 validates_presence_of :name
101 validates_length_of :name, :maximum => 255
101 validates_length_of :name, :maximum => 255
102 validate :validate_query_filters
102 validate :validate_query_filters
103
103
104 class_attribute :operators
104 class_attribute :operators
105 self.operators = {
105 self.operators = {
106 "=" => :label_equals,
106 "=" => :label_equals,
107 "!" => :label_not_equals,
107 "!" => :label_not_equals,
108 "o" => :label_open_issues,
108 "o" => :label_open_issues,
109 "c" => :label_closed_issues,
109 "c" => :label_closed_issues,
110 "!*" => :label_none,
110 "!*" => :label_none,
111 "*" => :label_any,
111 "*" => :label_any,
112 ">=" => :label_greater_or_equal,
112 ">=" => :label_greater_or_equal,
113 "<=" => :label_less_or_equal,
113 "<=" => :label_less_or_equal,
114 "><" => :label_between,
114 "><" => :label_between,
115 "<t+" => :label_in_less_than,
115 "<t+" => :label_in_less_than,
116 ">t+" => :label_in_more_than,
116 ">t+" => :label_in_more_than,
117 "><t+"=> :label_in_the_next_days,
117 "><t+"=> :label_in_the_next_days,
118 "t+" => :label_in,
118 "t+" => :label_in,
119 "t" => :label_today,
119 "t" => :label_today,
120 "ld" => :label_yesterday,
120 "w" => :label_this_week,
121 "w" => :label_this_week,
122 "lw" => :label_last_week,
123 "l2w" => [:label_last_n_weeks, :count => 2],
124 "m" => :label_this_month,
125 "lm" => :label_last_month,
126 "y" => :label_this_year,
121 ">t-" => :label_less_than_ago,
127 ">t-" => :label_less_than_ago,
122 "<t-" => :label_more_than_ago,
128 "<t-" => :label_more_than_ago,
123 "><t-"=> :label_in_the_past_days,
129 "><t-"=> :label_in_the_past_days,
124 "t-" => :label_ago,
130 "t-" => :label_ago,
125 "~" => :label_contains,
131 "~" => :label_contains,
126 "!~" => :label_not_contains,
132 "!~" => :label_not_contains,
127 "=p" => :label_any_issues_in_project,
133 "=p" => :label_any_issues_in_project,
128 "=!p" => :label_any_issues_not_in_project,
134 "=!p" => :label_any_issues_not_in_project,
129 "!p" => :label_no_issues_in_project
135 "!p" => :label_no_issues_in_project
130 }
136 }
131
137
132 class_attribute :operators_by_filter_type
138 class_attribute :operators_by_filter_type
133 self.operators_by_filter_type = {
139 self.operators_by_filter_type = {
134 :list => [ "=", "!" ],
140 :list => [ "=", "!" ],
135 :list_status => [ "o", "=", "!", "c", "*" ],
141 :list_status => [ "o", "=", "!", "c", "*" ],
136 :list_optional => [ "=", "!", "!*", "*" ],
142 :list_optional => [ "=", "!", "!*", "*" ],
137 :list_subprojects => [ "*", "!*", "=" ],
143 :list_subprojects => [ "*", "!*", "=" ],
138 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "w", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
144 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
139 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "w", "!*", "*" ],
145 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
140 :string => [ "=", "~", "!", "!~", "!*", "*" ],
146 :string => [ "=", "~", "!", "!~", "!*", "*" ],
141 :text => [ "~", "!~", "!*", "*" ],
147 :text => [ "~", "!~", "!*", "*" ],
142 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
148 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
143 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
149 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
144 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]
150 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]
145 }
151 }
146
152
147 class_attribute :available_columns
153 class_attribute :available_columns
148 self.available_columns = []
154 self.available_columns = []
149
155
150 class_attribute :queried_class
156 class_attribute :queried_class
151
157
152 def queried_table_name
158 def queried_table_name
153 @queried_table_name ||= self.class.queried_class.table_name
159 @queried_table_name ||= self.class.queried_class.table_name
154 end
160 end
155
161
156 def initialize(attributes=nil, *args)
162 def initialize(attributes=nil, *args)
157 super attributes
163 super attributes
158 @is_for_all = project.nil?
164 @is_for_all = project.nil?
159 end
165 end
160
166
161 # Builds the query from the given params
167 # Builds the query from the given params
162 def build_from_params(params)
168 def build_from_params(params)
163 if params[:fields] || params[:f]
169 if params[:fields] || params[:f]
164 self.filters = {}
170 self.filters = {}
165 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
171 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
166 else
172 else
167 available_filters.keys.each do |field|
173 available_filters.keys.each do |field|
168 add_short_filter(field, params[field]) if params[field]
174 add_short_filter(field, params[field]) if params[field]
169 end
175 end
170 end
176 end
171 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
177 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
172 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
178 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
173 self
179 self
174 end
180 end
175
181
182 # Builds a new query from the given params and attributes
183 def self.build_from_params(params, attributes={})
184 new(attributes).build_from_params(params)
185 end
186
176 def validate_query_filters
187 def validate_query_filters
177 filters.each_key do |field|
188 filters.each_key do |field|
178 if values_for(field)
189 if values_for(field)
179 case type_for(field)
190 case type_for(field)
180 when :integer
191 when :integer
181 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
192 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
182 when :float
193 when :float
183 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
194 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
184 when :date, :date_past
195 when :date, :date_past
185 case operator_for(field)
196 case operator_for(field)
186 when "=", ">=", "<=", "><"
197 when "=", ">=", "<=", "><"
187 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
198 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
188 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
199 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
189 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
200 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
190 end
201 end
191 end
202 end
192 end
203 end
193
204
194 add_filter_error(field, :blank) unless
205 add_filter_error(field, :blank) unless
195 # filter requires one or more values
206 # filter requires one or more values
196 (values_for(field) and !values_for(field).first.blank?) or
207 (values_for(field) and !values_for(field).first.blank?) or
197 # filter doesn't require any value
208 # filter doesn't require any value
198 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
209 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y"].include? operator_for(field)
199 end if filters
210 end if filters
200 end
211 end
201
212
202 def add_filter_error(field, message)
213 def add_filter_error(field, message)
203 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
214 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
204 errors.add(:base, m)
215 errors.add(:base, m)
205 end
216 end
206
217
207 def editable_by?(user)
218 def editable_by?(user)
208 return false unless user
219 return false unless user
209 # Admin can edit them all and regular users can edit their private queries
220 # Admin can edit them all and regular users can edit their private queries
210 return true if user.admin? || (!is_public && self.user_id == user.id)
221 return true if user.admin? || (!is_public && self.user_id == user.id)
211 # Members can not edit public queries that are for all project (only admin is allowed to)
222 # Members can not edit public queries that are for all project (only admin is allowed to)
212 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
223 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
213 end
224 end
214
225
215 def trackers
226 def trackers
216 @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers
227 @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers
217 end
228 end
218
229
219 # Returns a hash of localized labels for all filter operators
230 # Returns a hash of localized labels for all filter operators
220 def self.operators_labels
231 def self.operators_labels
221 operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h}
232 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
222 end
233 end
223
234
224 # Returns a representation of the available filters for JSON serialization
235 # Returns a representation of the available filters for JSON serialization
225 def available_filters_as_json
236 def available_filters_as_json
226 json = {}
237 json = {}
227 available_filters.each do |field, options|
238 available_filters.each do |field, options|
228 json[field] = options.slice(:type, :name, :values).stringify_keys
239 json[field] = options.slice(:type, :name, :values).stringify_keys
229 end
240 end
230 json
241 json
231 end
242 end
232
243
233 def all_projects
244 def all_projects
234 @all_projects ||= Project.visible.all
245 @all_projects ||= Project.visible.all
235 end
246 end
236
247
237 def all_projects_values
248 def all_projects_values
238 return @all_projects_values if @all_projects_values
249 return @all_projects_values if @all_projects_values
239
250
240 values = []
251 values = []
241 Project.project_tree(all_projects) do |p, level|
252 Project.project_tree(all_projects) do |p, level|
242 prefix = (level > 0 ? ('--' * level + ' ') : '')
253 prefix = (level > 0 ? ('--' * level + ' ') : '')
243 values << ["#{prefix}#{p.name}", p.id.to_s]
254 values << ["#{prefix}#{p.name}", p.id.to_s]
244 end
255 end
245 @all_projects_values = values
256 @all_projects_values = values
246 end
257 end
247
258
248 def add_filter(field, operator, values)
259 def add_filter(field, operator, values=nil)
249 # values must be an array
260 # values must be an array
250 return unless values.nil? || values.is_a?(Array)
261 return unless values.nil? || values.is_a?(Array)
251 # check if field is defined as an available filter
262 # check if field is defined as an available filter
252 if available_filters.has_key? field
263 if available_filters.has_key? field
253 filter_options = available_filters[field]
264 filter_options = available_filters[field]
254 filters[field] = {:operator => operator, :values => (values || [''])}
265 filters[field] = {:operator => operator, :values => (values || [''])}
255 end
266 end
256 end
267 end
257
268
258 def add_short_filter(field, expression)
269 def add_short_filter(field, expression)
259 return unless expression && available_filters.has_key?(field)
270 return unless expression && available_filters.has_key?(field)
260 field_type = available_filters[field][:type]
271 field_type = available_filters[field][:type]
261 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
272 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
262 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
273 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
263 add_filter field, operator, $1.present? ? $1.split('|') : ['']
274 add_filter field, operator, $1.present? ? $1.split('|') : ['']
264 end || add_filter(field, '=', expression.split('|'))
275 end || add_filter(field, '=', expression.split('|'))
265 end
276 end
266
277
267 # Add multiple filters using +add_filter+
278 # Add multiple filters using +add_filter+
268 def add_filters(fields, operators, values)
279 def add_filters(fields, operators, values)
269 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
280 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
270 fields.each do |field|
281 fields.each do |field|
271 add_filter(field, operators[field], values && values[field])
282 add_filter(field, operators[field], values && values[field])
272 end
283 end
273 end
284 end
274 end
285 end
275
286
276 def has_filter?(field)
287 def has_filter?(field)
277 filters and filters[field]
288 filters and filters[field]
278 end
289 end
279
290
280 def type_for(field)
291 def type_for(field)
281 available_filters[field][:type] if available_filters.has_key?(field)
292 available_filters[field][:type] if available_filters.has_key?(field)
282 end
293 end
283
294
284 def operator_for(field)
295 def operator_for(field)
285 has_filter?(field) ? filters[field][:operator] : nil
296 has_filter?(field) ? filters[field][:operator] : nil
286 end
297 end
287
298
288 def values_for(field)
299 def values_for(field)
289 has_filter?(field) ? filters[field][:values] : nil
300 has_filter?(field) ? filters[field][:values] : nil
290 end
301 end
291
302
292 def value_for(field, index=0)
303 def value_for(field, index=0)
293 (values_for(field) || [])[index]
304 (values_for(field) || [])[index]
294 end
305 end
295
306
296 def label_for(field)
307 def label_for(field)
297 label = available_filters[field][:name] if available_filters.has_key?(field)
308 label = available_filters[field][:name] if available_filters.has_key?(field)
298 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
309 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
299 end
310 end
300
311
301 def self.add_available_column(column)
312 def self.add_available_column(column)
302 self.available_columns << (column) if column.is_a?(QueryColumn)
313 self.available_columns << (column) if column.is_a?(QueryColumn)
303 end
314 end
304
315
305 # Returns an array of columns that can be used to group the results
316 # Returns an array of columns that can be used to group the results
306 def groupable_columns
317 def groupable_columns
307 available_columns.select {|c| c.groupable}
318 available_columns.select {|c| c.groupable}
308 end
319 end
309
320
310 # Returns a Hash of columns and the key for sorting
321 # Returns a Hash of columns and the key for sorting
311 def sortable_columns
322 def sortable_columns
312 available_columns.inject({}) {|h, column|
323 available_columns.inject({}) {|h, column|
313 h[column.name.to_s] = column.sortable
324 h[column.name.to_s] = column.sortable
314 h
325 h
315 }
326 }
316 end
327 end
317
328
318 def columns
329 def columns
319 # preserve the column_names order
330 # preserve the column_names order
320 (has_default_columns? ? default_columns_names : column_names).collect do |name|
331 (has_default_columns? ? default_columns_names : column_names).collect do |name|
321 available_columns.find { |col| col.name == name }
332 available_columns.find { |col| col.name == name }
322 end.compact
333 end.compact
323 end
334 end
324
335
325 def inline_columns
336 def inline_columns
326 columns.select(&:inline?)
337 columns.select(&:inline?)
327 end
338 end
328
339
329 def block_columns
340 def block_columns
330 columns.reject(&:inline?)
341 columns.reject(&:inline?)
331 end
342 end
332
343
333 def available_inline_columns
344 def available_inline_columns
334 available_columns.select(&:inline?)
345 available_columns.select(&:inline?)
335 end
346 end
336
347
337 def available_block_columns
348 def available_block_columns
338 available_columns.reject(&:inline?)
349 available_columns.reject(&:inline?)
339 end
350 end
340
351
341 def default_columns_names
352 def default_columns_names
342 []
353 []
343 end
354 end
344
355
345 def column_names=(names)
356 def column_names=(names)
346 if names
357 if names
347 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
358 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
348 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
359 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
349 # Set column_names to nil if default columns
360 # Set column_names to nil if default columns
350 if names == default_columns_names
361 if names == default_columns_names
351 names = nil
362 names = nil
352 end
363 end
353 end
364 end
354 write_attribute(:column_names, names)
365 write_attribute(:column_names, names)
355 end
366 end
356
367
357 def has_column?(column)
368 def has_column?(column)
358 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
369 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
359 end
370 end
360
371
361 def has_default_columns?
372 def has_default_columns?
362 column_names.nil? || column_names.empty?
373 column_names.nil? || column_names.empty?
363 end
374 end
364
375
365 def sort_criteria=(arg)
376 def sort_criteria=(arg)
366 c = []
377 c = []
367 if arg.is_a?(Hash)
378 if arg.is_a?(Hash)
368 arg = arg.keys.sort.collect {|k| arg[k]}
379 arg = arg.keys.sort.collect {|k| arg[k]}
369 end
380 end
370 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
381 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
371 write_attribute(:sort_criteria, c)
382 write_attribute(:sort_criteria, c)
372 end
383 end
373
384
374 def sort_criteria
385 def sort_criteria
375 read_attribute(:sort_criteria) || []
386 read_attribute(:sort_criteria) || []
376 end
387 end
377
388
378 def sort_criteria_key(arg)
389 def sort_criteria_key(arg)
379 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
390 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
380 end
391 end
381
392
382 def sort_criteria_order(arg)
393 def sort_criteria_order(arg)
383 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
394 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
384 end
395 end
385
396
386 def sort_criteria_order_for(key)
397 def sort_criteria_order_for(key)
387 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
398 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
388 end
399 end
389
400
390 # Returns the SQL sort order that should be prepended for grouping
401 # Returns the SQL sort order that should be prepended for grouping
391 def group_by_sort_order
402 def group_by_sort_order
392 if grouped? && (column = group_by_column)
403 if grouped? && (column = group_by_column)
393 order = sort_criteria_order_for(column.name) || column.default_order
404 order = sort_criteria_order_for(column.name) || column.default_order
394 column.sortable.is_a?(Array) ?
405 column.sortable.is_a?(Array) ?
395 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
406 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
396 "#{column.sortable} #{order}"
407 "#{column.sortable} #{order}"
397 end
408 end
398 end
409 end
399
410
400 # Returns true if the query is a grouped query
411 # Returns true if the query is a grouped query
401 def grouped?
412 def grouped?
402 !group_by_column.nil?
413 !group_by_column.nil?
403 end
414 end
404
415
405 def group_by_column
416 def group_by_column
406 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
417 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
407 end
418 end
408
419
409 def group_by_statement
420 def group_by_statement
410 group_by_column.try(:groupable)
421 group_by_column.try(:groupable)
411 end
422 end
412
423
413 def project_statement
424 def project_statement
414 project_clauses = []
425 project_clauses = []
415 if project && !project.descendants.active.empty?
426 if project && !project.descendants.active.empty?
416 ids = [project.id]
427 ids = [project.id]
417 if has_filter?("subproject_id")
428 if has_filter?("subproject_id")
418 case operator_for("subproject_id")
429 case operator_for("subproject_id")
419 when '='
430 when '='
420 # include the selected subprojects
431 # include the selected subprojects
421 ids += values_for("subproject_id").each(&:to_i)
432 ids += values_for("subproject_id").each(&:to_i)
422 when '!*'
433 when '!*'
423 # main project only
434 # main project only
424 else
435 else
425 # all subprojects
436 # all subprojects
426 ids += project.descendants.collect(&:id)
437 ids += project.descendants.collect(&:id)
427 end
438 end
428 elsif Setting.display_subprojects_issues?
439 elsif Setting.display_subprojects_issues?
429 ids += project.descendants.collect(&:id)
440 ids += project.descendants.collect(&:id)
430 end
441 end
431 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
442 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
432 elsif project
443 elsif project
433 project_clauses << "#{Project.table_name}.id = %d" % project.id
444 project_clauses << "#{Project.table_name}.id = %d" % project.id
434 end
445 end
435 project_clauses.any? ? project_clauses.join(' AND ') : nil
446 project_clauses.any? ? project_clauses.join(' AND ') : nil
436 end
447 end
437
448
438 def statement
449 def statement
439 # filters clauses
450 # filters clauses
440 filters_clauses = []
451 filters_clauses = []
441 filters.each_key do |field|
452 filters.each_key do |field|
442 next if field == "subproject_id"
453 next if field == "subproject_id"
443 v = values_for(field).clone
454 v = values_for(field).clone
444 next unless v and !v.empty?
455 next unless v and !v.empty?
445 operator = operator_for(field)
456 operator = operator_for(field)
446
457
447 # "me" value subsitution
458 # "me" value subsitution
448 if %w(assigned_to_id author_id watcher_id).include?(field)
459 if %w(assigned_to_id author_id watcher_id).include?(field)
449 if v.delete("me")
460 if v.delete("me")
450 if User.current.logged?
461 if User.current.logged?
451 v.push(User.current.id.to_s)
462 v.push(User.current.id.to_s)
452 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
463 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
453 else
464 else
454 v.push("0")
465 v.push("0")
455 end
466 end
456 end
467 end
457 end
468 end
458
469
459 if field == 'project_id'
470 if field == 'project_id'
460 if v.delete('mine')
471 if v.delete('mine')
461 v += User.current.memberships.map(&:project_id).map(&:to_s)
472 v += User.current.memberships.map(&:project_id).map(&:to_s)
462 end
473 end
463 end
474 end
464
475
465 if field =~ /cf_(\d+)$/
476 if field =~ /cf_(\d+)$/
466 # custom field
477 # custom field
467 filters_clauses << sql_for_custom_field(field, operator, v, $1)
478 filters_clauses << sql_for_custom_field(field, operator, v, $1)
468 elsif respond_to?("sql_for_#{field}_field")
479 elsif respond_to?("sql_for_#{field}_field")
469 # specific statement
480 # specific statement
470 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
481 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
471 else
482 else
472 # regular field
483 # regular field
473 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
484 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
474 end
485 end
475 end if filters and valid?
486 end if filters and valid?
476
487
477 filters_clauses << project_statement
488 filters_clauses << project_statement
478 filters_clauses.reject!(&:blank?)
489 filters_clauses.reject!(&:blank?)
479
490
480 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
491 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
481 end
492 end
482
493
483 private
494 private
484
495
485 def sql_for_custom_field(field, operator, value, custom_field_id)
496 def sql_for_custom_field(field, operator, value, custom_field_id)
486 db_table = CustomValue.table_name
497 db_table = CustomValue.table_name
487 db_field = 'value'
498 db_field = 'value'
488 filter = @available_filters[field]
499 filter = @available_filters[field]
489 return nil unless filter
500 return nil unless filter
490 if filter[:format] == 'user'
501 if filter[:format] == 'user'
491 if value.delete('me')
502 if value.delete('me')
492 value.push User.current.id.to_s
503 value.push User.current.id.to_s
493 end
504 end
494 end
505 end
495 not_in = nil
506 not_in = nil
496 if operator == '!'
507 if operator == '!'
497 # Makes ! operator work for custom fields with multiple values
508 # Makes ! operator work for custom fields with multiple values
498 operator = '='
509 operator = '='
499 not_in = 'NOT'
510 not_in = 'NOT'
500 end
511 end
501 customized_key = "id"
512 customized_key = "id"
502 customized_class = queried_class
513 customized_class = queried_class
503 if field =~ /^(.+)\.cf_/
514 if field =~ /^(.+)\.cf_/
504 assoc = $1
515 assoc = $1
505 customized_key = "#{assoc}_id"
516 customized_key = "#{assoc}_id"
506 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
517 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
507 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
518 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
508 end
519 end
509 "#{queried_table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} 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} WHERE " +
520 "#{queried_table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} 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} WHERE " +
510 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
521 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
511 end
522 end
512
523
513 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
524 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
514 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
525 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
515 sql = ''
526 sql = ''
516 case operator
527 case operator
517 when "="
528 when "="
518 if value.any?
529 if value.any?
519 case type_for(field)
530 case type_for(field)
520 when :date, :date_past
531 when :date, :date_past
521 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
532 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
522 when :integer
533 when :integer
523 if is_custom_filter
534 if is_custom_filter
524 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
535 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
525 else
536 else
526 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
537 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
527 end
538 end
528 when :float
539 when :float
529 if is_custom_filter
540 if is_custom_filter
530 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
541 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
531 else
542 else
532 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
543 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
533 end
544 end
534 else
545 else
535 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
546 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
536 end
547 end
537 else
548 else
538 # IN an empty set
549 # IN an empty set
539 sql = "1=0"
550 sql = "1=0"
540 end
551 end
541 when "!"
552 when "!"
542 if value.any?
553 if value.any?
543 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
554 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
544 else
555 else
545 # NOT IN an empty set
556 # NOT IN an empty set
546 sql = "1=1"
557 sql = "1=1"
547 end
558 end
548 when "!*"
559 when "!*"
549 sql = "#{db_table}.#{db_field} IS NULL"
560 sql = "#{db_table}.#{db_field} IS NULL"
550 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
561 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
551 when "*"
562 when "*"
552 sql = "#{db_table}.#{db_field} IS NOT NULL"
563 sql = "#{db_table}.#{db_field} IS NOT NULL"
553 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
564 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
554 when ">="
565 when ">="
555 if [:date, :date_past].include?(type_for(field))
566 if [:date, :date_past].include?(type_for(field))
556 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
567 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
557 else
568 else
558 if is_custom_filter
569 if is_custom_filter
559 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
570 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
560 else
571 else
561 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
572 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
562 end
573 end
563 end
574 end
564 when "<="
575 when "<="
565 if [:date, :date_past].include?(type_for(field))
576 if [:date, :date_past].include?(type_for(field))
566 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
577 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
567 else
578 else
568 if is_custom_filter
579 if is_custom_filter
569 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
580 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
570 else
581 else
571 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
582 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
572 end
583 end
573 end
584 end
574 when "><"
585 when "><"
575 if [:date, :date_past].include?(type_for(field))
586 if [:date, :date_past].include?(type_for(field))
576 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
587 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
577 else
588 else
578 if is_custom_filter
589 if is_custom_filter
579 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
590 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
580 else
591 else
581 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
592 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
582 end
593 end
583 end
594 end
584 when "o"
595 when "o"
585 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
596 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
586 when "c"
597 when "c"
587 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
598 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
588 when "><t-"
599 when "><t-"
589 # between today - n days and today
600 # between today - n days and today
590 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
601 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
591 when ">t-"
602 when ">t-"
592 # >= today - n days
603 # >= today - n days
593 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
604 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
594 when "<t-"
605 when "<t-"
595 # <= today - n days
606 # <= today - n days
596 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
607 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
597 when "t-"
608 when "t-"
598 # = n days in past
609 # = n days in past
599 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
610 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
600 when "><t+"
611 when "><t+"
601 # between today and today + n days
612 # between today and today + n days
602 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
613 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
603 when ">t+"
614 when ">t+"
604 # >= today + n days
615 # >= today + n days
605 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
616 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
606 when "<t+"
617 when "<t+"
607 # <= today + n days
618 # <= today + n days
608 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
619 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
609 when "t+"
620 when "t+"
610 # = today + n days
621 # = today + n days
611 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
622 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
612 when "t"
623 when "t"
613 # = today
624 # = today
614 sql = relative_date_clause(db_table, db_field, 0, 0)
625 sql = relative_date_clause(db_table, db_field, 0, 0)
626 when "ld"
627 # = yesterday
628 sql = relative_date_clause(db_table, db_field, -1, -1)
615 when "w"
629 when "w"
616 # = this week
630 # = this week
617 first_day_of_week = l(:general_first_day_of_week).to_i
631 first_day_of_week = l(:general_first_day_of_week).to_i
618 day_of_week = Date.today.cwday
632 day_of_week = Date.today.cwday
619 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
633 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
620 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
634 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
635 when "lw"
636 # = last week
637 first_day_of_week = l(:general_first_day_of_week).to_i
638 day_of_week = Date.today.cwday
639 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
640 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1)
641 when "l2w"
642 # = last 2 weeks
643 first_day_of_week = l(:general_first_day_of_week).to_i
644 day_of_week = Date.today.cwday
645 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
646 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1)
647 when "m"
648 # = this month
649 date = Date.today
650 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month)
651 when "lm"
652 # = last month
653 date = Date.today.prev_month
654 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month)
655 when "y"
656 # = this year
657 date = Date.today
658 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year)
621 when "~"
659 when "~"
622 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
660 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
623 when "!~"
661 when "!~"
624 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
662 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
625 else
663 else
626 raise "Unknown query operator #{operator}"
664 raise "Unknown query operator #{operator}"
627 end
665 end
628
666
629 return sql
667 return sql
630 end
668 end
631
669
632 def add_custom_fields_filters(custom_fields, assoc=nil)
670 def add_custom_fields_filters(custom_fields, assoc=nil)
633 return unless custom_fields.present?
671 return unless custom_fields.present?
634 @available_filters ||= {}
672 @available_filters ||= {}
635
673
636 custom_fields.select(&:is_filter?).each do |field|
674 custom_fields.select(&:is_filter?).each do |field|
637 case field.field_format
675 case field.field_format
638 when "text"
676 when "text"
639 options = { :type => :text, :order => 20 }
677 options = { :type => :text, :order => 20 }
640 when "list"
678 when "list"
641 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
679 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
642 when "date"
680 when "date"
643 options = { :type => :date, :order => 20 }
681 options = { :type => :date, :order => 20 }
644 when "bool"
682 when "bool"
645 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
683 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
646 when "int"
684 when "int"
647 options = { :type => :integer, :order => 20 }
685 options = { :type => :integer, :order => 20 }
648 when "float"
686 when "float"
649 options = { :type => :float, :order => 20 }
687 options = { :type => :float, :order => 20 }
650 when "user", "version"
688 when "user", "version"
651 next unless project
689 next unless project
652 values = field.possible_values_options(project)
690 values = field.possible_values_options(project)
653 if User.current.logged? && field.field_format == 'user'
691 if User.current.logged? && field.field_format == 'user'
654 values.unshift ["<< #{l(:label_me)} >>", "me"]
692 values.unshift ["<< #{l(:label_me)} >>", "me"]
655 end
693 end
656 options = { :type => :list_optional, :values => values, :order => 20}
694 options = { :type => :list_optional, :values => values, :order => 20}
657 else
695 else
658 options = { :type => :string, :order => 20 }
696 options = { :type => :string, :order => 20 }
659 end
697 end
660 filter_id = "cf_#{field.id}"
698 filter_id = "cf_#{field.id}"
661 filter_name = field.name
699 filter_name = field.name
662 if assoc.present?
700 if assoc.present?
663 filter_id = "#{assoc}.#{filter_id}"
701 filter_id = "#{assoc}.#{filter_id}"
664 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
702 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
665 end
703 end
666 @available_filters[filter_id] = options.merge({
704 @available_filters[filter_id] = options.merge({
667 :name => filter_name,
705 :name => filter_name,
668 :format => field.field_format,
706 :format => field.field_format,
669 :field => field
707 :field => field
670 })
708 })
671 end
709 end
672 end
710 end
673
711
674 def add_associations_custom_fields_filters(*associations)
712 def add_associations_custom_fields_filters(*associations)
675 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
713 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
676 associations.each do |assoc|
714 associations.each do |assoc|
677 association_klass = queried_class.reflect_on_association(assoc).klass
715 association_klass = queried_class.reflect_on_association(assoc).klass
678 fields_by_class.each do |field_class, fields|
716 fields_by_class.each do |field_class, fields|
679 if field_class.customized_class <= association_klass
717 if field_class.customized_class <= association_klass
680 add_custom_fields_filters(fields, assoc)
718 add_custom_fields_filters(fields, assoc)
681 end
719 end
682 end
720 end
683 end
721 end
684 end
722 end
685
723
686 # Returns a SQL clause for a date or datetime field.
724 # Returns a SQL clause for a date or datetime field.
687 def date_clause(table, field, from, to)
725 def date_clause(table, field, from, to)
688 s = []
726 s = []
689 if from
727 if from
690 from_yesterday = from - 1
728 from_yesterday = from - 1
691 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
729 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
692 if self.class.default_timezone == :utc
730 if self.class.default_timezone == :utc
693 from_yesterday_time = from_yesterday_time.utc
731 from_yesterday_time = from_yesterday_time.utc
694 end
732 end
695 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
733 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
696 end
734 end
697 if to
735 if to
698 to_time = Time.local(to.year, to.month, to.day)
736 to_time = Time.local(to.year, to.month, to.day)
699 if self.class.default_timezone == :utc
737 if self.class.default_timezone == :utc
700 to_time = to_time.utc
738 to_time = to_time.utc
701 end
739 end
702 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
740 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
703 end
741 end
704 s.join(' AND ')
742 s.join(' AND ')
705 end
743 end
706
744
707 # Returns a SQL clause for a date or datetime field using relative dates.
745 # Returns a SQL clause for a date or datetime field using relative dates.
708 def relative_date_clause(table, field, days_from, days_to)
746 def relative_date_clause(table, field, days_from, days_to)
709 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
747 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
710 end
748 end
711
749
712 # Additional joins required for the given sort options
750 # Additional joins required for the given sort options
713 def joins_for_order_statement(order_options)
751 def joins_for_order_statement(order_options)
714 joins = []
752 joins = []
715
753
716 if order_options
754 if order_options
717 if order_options.include?('authors')
755 if order_options.include?('authors')
718 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
756 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
719 end
757 end
720 order_options.scan(/cf_\d+/).uniq.each do |name|
758 order_options.scan(/cf_\d+/).uniq.each do |name|
721 column = available_columns.detect {|c| c.name.to_s == name}
759 column = available_columns.detect {|c| c.name.to_s == name}
722 join = column && column.custom_field.join_for_order_statement
760 join = column && column.custom_field.join_for_order_statement
723 if join
761 if join
724 joins << join
762 joins << join
725 end
763 end
726 end
764 end
727 end
765 end
728
766
729 joins.any? ? joins.join(' ') : nil
767 joins.any? ? joins.join(' ') : nil
730 end
768 end
731 end
769 end
@@ -1,42 +1,23
1 <fieldset id="date-range" class="collapsible">
1 <div id="query_form_content" class="hide-when-print">
2 <legend onclick="toggleFieldset(this);"><%= l(:label_date_range) %></legend>
2 <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
3 <div>
3 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
4 <p>
4 <div style="<%= @query.new_record? ? "" : "display: none;" %>">
5 <%= label_tag "period_type_list", l(:description_date_range_list), :class => "hidden-for-sighted" %>
5 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
6 <%= radio_button_tag 'period_type', '1', !@free_period, :onclick => '$("#from,#to").attr("disabled", true);$("#period").removeAttr("disabled");', :id => "period_type_list"%>
6 </div>
7 <%= select_tag 'period', options_for_period_select(params[:period]),
8 :onchange => 'this.form.submit();',
9 :onfocus => '$("#period_type_1").attr("checked", true);',
10 :disabled => @free_period %>
11 </p>
12 <p>
13 <%= label_tag "period_type_interval", l(:description_date_range_interval), :class => "hidden-for-sighted" %>
14 <%= radio_button_tag 'period_type', '2', @free_period, :onclick => '$("#from,#to").removeAttr("disabled");$("#period").attr("disabled", true);', :id => "period_type_interval" %>
15 <%= l(:label_date_from_to,
16 :start => ((label_tag "from", l(:description_date_from), :class => "hidden-for-sighted") +
17 text_field_tag('from', @from, :size => 10, :disabled => !@free_period) + calendar_for('from')),
18 :end => ((label_tag "to", l(:description_date_to), :class => "hidden-for-sighted") +
19 text_field_tag('to', @to, :size => 10, :disabled => !@free_period) + calendar_for('to'))).html_safe %>
20 </p>
21 </div>
22 </fieldset>
7 </fieldset>
23 <p class="buttons">
8 </div>
9
10 <p class="buttons hide-when-print">
24 <%= link_to_function l(:button_apply), '$("#query_form").submit()', :class => 'icon icon-checked' %>
11 <%= link_to_function l(:button_apply), '$("#query_form").submit()', :class => 'icon icon-checked' %>
25 <%= link_to l(:button_clear), {:controller => controller_name, :action => action_name, :project_id => @project, :issue_id => @issue}, :class => 'icon icon-reload' %>
12 <%= link_to l(:button_clear), {:project_id => @project, :issue_id => @issue}, :class => 'icon icon-reload' %>
26 </p>
13 </p>
27
14
28 <div class="tabs">
15 <div class="tabs">
29 <% url_params = @free_period ? { :from => @from, :to => @to } : { :period => params[:period] } %>
16 <% query_params = params.slice(:f, :op, :v, :sort) %>
30 <ul>
17 <ul>
31 <li><%= link_to(l(:label_details), url_params.merge({:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue }),
18 <li><%= link_to(l(:label_details), query_params.merge({:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue }),
32 :class => (action_name == 'index' ? 'selected' : nil)) %></li>
19 :class => (action_name == 'index' ? 'selected' : nil)) %></li>
33 <li><%= link_to(l(:label_report), url_params.merge({:controller => 'timelog', :action => 'report', :project_id => @project, :issue_id => @issue}),
20 <li><%= link_to(l(:label_report), query_params.merge({:controller => 'timelog', :action => 'report', :project_id => @project, :issue_id => @issue}),
34 :class => (action_name == 'report' ? 'selected' : nil)) %></li>
21 :class => (action_name == 'report' ? 'selected' : nil)) %></li>
35 </ul>
22 </ul>
36 </div>
23 </div>
37
38 <%= javascript_tag do %>
39 $('#from, #to').change(function(){
40 $('#period_type_interval').attr('checked', true); $('#from,#to').removeAttr('disabled'); $('#period').attr('disabled', true);
41 });
42 <% end %>
@@ -1,164 +1,153
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 module Redmine
18 module Redmine
19 module Helpers
19 module Helpers
20 class TimeReport
20 class TimeReport
21 attr_reader :criteria, :columns, :from, :to, :hours, :total_hours, :periods
21 attr_reader :criteria, :columns, :hours, :total_hours, :periods
22
22
23 def initialize(project, issue, criteria, columns, from, to)
23 def initialize(project, issue, criteria, columns, time_entry_scope)
24 @project = project
24 @project = project
25 @issue = issue
25 @issue = issue
26
26
27 @criteria = criteria || []
27 @criteria = criteria || []
28 @criteria = @criteria.select{|criteria| available_criteria.has_key? criteria}
28 @criteria = @criteria.select{|criteria| available_criteria.has_key? criteria}
29 @criteria.uniq!
29 @criteria.uniq!
30 @criteria = @criteria[0,3]
30 @criteria = @criteria[0,3]
31
31
32 @columns = (columns && %w(year month week day).include?(columns)) ? columns : 'month'
32 @columns = (columns && %w(year month week day).include?(columns)) ? columns : 'month'
33 @from = from
33 @scope = time_entry_scope
34 @to = to
35
34
36 run
35 run
37 end
36 end
38
37
39 def available_criteria
38 def available_criteria
40 @available_criteria || load_available_criteria
39 @available_criteria || load_available_criteria
41 end
40 end
42
41
43 private
42 private
44
43
45 def run
44 def run
46 unless @criteria.empty?
45 unless @criteria.empty?
47 scope = TimeEntry.visible.spent_between(@from, @to)
48 if @issue
49 scope = scope.on_issue(@issue)
50 elsif @project
51 scope = scope.on_project(@project, Setting.display_subprojects_issues?)
52 end
53 time_columns = %w(tyear tmonth tweek spent_on)
46 time_columns = %w(tyear tmonth tweek spent_on)
54 @hours = []
47 @hours = []
55 scope.sum(:hours, :include => :issue, :group => @criteria.collect{|criteria| @available_criteria[criteria][:sql]} + time_columns).each do |hash, hours|
48 @scope.sum(:hours, :include => :issue, :group => @criteria.collect{|criteria| @available_criteria[criteria][:sql]} + time_columns).each do |hash, hours|
56 h = {'hours' => hours}
49 h = {'hours' => hours}
57 (@criteria + time_columns).each_with_index do |name, i|
50 (@criteria + time_columns).each_with_index do |name, i|
58 h[name] = hash[i]
51 h[name] = hash[i]
59 end
52 end
60 @hours << h
53 @hours << h
61 end
54 end
62
55
63 @hours.each do |row|
56 @hours.each do |row|
64 case @columns
57 case @columns
65 when 'year'
58 when 'year'
66 row['year'] = row['tyear']
59 row['year'] = row['tyear']
67 when 'month'
60 when 'month'
68 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
61 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
69 when 'week'
62 when 'week'
70 row['week'] = "#{row['tyear']}-#{row['tweek']}"
63 row['week'] = "#{row['tyear']}-#{row['tweek']}"
71 when 'day'
64 when 'day'
72 row['day'] = "#{row['spent_on']}"
65 row['day'] = "#{row['spent_on']}"
73 end
66 end
74 end
67 end
75
68
76 if @from.nil?
69 min = @hours.collect {|row| row['spent_on']}.min
77 min = @hours.collect {|row| row['spent_on']}.min
70 @from = min ? min.to_date : Date.today
78 @from = min ? min.to_date : Date.today
79 end
80
71
81 if @to.nil?
72 max = @hours.collect {|row| row['spent_on']}.max
82 max = @hours.collect {|row| row['spent_on']}.max
73 @to = max ? max.to_date : Date.today
83 @to = max ? max.to_date : Date.today
84 end
85
74
86 @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
75 @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
87
76
88 @periods = []
77 @periods = []
89 # Date#at_beginning_of_ not supported in Rails 1.2.x
78 # Date#at_beginning_of_ not supported in Rails 1.2.x
90 date_from = @from.to_time
79 date_from = @from.to_time
91 # 100 columns max
80 # 100 columns max
92 while date_from <= @to.to_time && @periods.length < 100
81 while date_from <= @to.to_time && @periods.length < 100
93 case @columns
82 case @columns
94 when 'year'
83 when 'year'
95 @periods << "#{date_from.year}"
84 @periods << "#{date_from.year}"
96 date_from = (date_from + 1.year).at_beginning_of_year
85 date_from = (date_from + 1.year).at_beginning_of_year
97 when 'month'
86 when 'month'
98 @periods << "#{date_from.year}-#{date_from.month}"
87 @periods << "#{date_from.year}-#{date_from.month}"
99 date_from = (date_from + 1.month).at_beginning_of_month
88 date_from = (date_from + 1.month).at_beginning_of_month
100 when 'week'
89 when 'week'
101 @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
90 @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
102 date_from = (date_from + 7.day).at_beginning_of_week
91 date_from = (date_from + 7.day).at_beginning_of_week
103 when 'day'
92 when 'day'
104 @periods << "#{date_from.to_date}"
93 @periods << "#{date_from.to_date}"
105 date_from = date_from + 1.day
94 date_from = date_from + 1.day
106 end
95 end
107 end
96 end
108 end
97 end
109 end
98 end
110
99
111 def load_available_criteria
100 def load_available_criteria
112 @available_criteria = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
101 @available_criteria = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
113 :klass => Project,
102 :klass => Project,
114 :label => :label_project},
103 :label => :label_project},
115 'status' => {:sql => "#{Issue.table_name}.status_id",
104 'status' => {:sql => "#{Issue.table_name}.status_id",
116 :klass => IssueStatus,
105 :klass => IssueStatus,
117 :label => :field_status},
106 :label => :field_status},
118 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
107 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
119 :klass => Version,
108 :klass => Version,
120 :label => :label_version},
109 :label => :label_version},
121 'category' => {:sql => "#{Issue.table_name}.category_id",
110 'category' => {:sql => "#{Issue.table_name}.category_id",
122 :klass => IssueCategory,
111 :klass => IssueCategory,
123 :label => :field_category},
112 :label => :field_category},
124 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
113 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
125 :klass => User,
114 :klass => User,
126 :label => :label_member},
115 :label => :label_member},
127 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
116 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
128 :klass => Tracker,
117 :klass => Tracker,
129 :label => :label_tracker},
118 :label => :label_tracker},
130 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
119 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
131 :klass => TimeEntryActivity,
120 :klass => TimeEntryActivity,
132 :label => :label_activity},
121 :label => :label_activity},
133 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
122 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
134 :klass => Issue,
123 :klass => Issue,
135 :label => :label_issue}
124 :label => :label_issue}
136 }
125 }
137
126
138 # Add list and boolean custom fields as available criteria
127 # Add list and boolean custom fields as available criteria
139 custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
128 custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
140 custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
129 custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
141 @available_criteria["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id ORDER BY c.value LIMIT 1)",
130 @available_criteria["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id ORDER BY c.value LIMIT 1)",
142 :format => cf.field_format,
131 :format => cf.field_format,
143 :label => cf.name}
132 :label => cf.name}
144 end if @project
133 end if @project
145
134
146 # Add list and boolean time entry custom fields
135 # Add list and boolean time entry custom fields
147 TimeEntryCustomField.all.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
136 TimeEntryCustomField.all.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
148 @available_criteria["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id ORDER BY c.value LIMIT 1)",
137 @available_criteria["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id ORDER BY c.value LIMIT 1)",
149 :format => cf.field_format,
138 :format => cf.field_format,
150 :label => cf.name}
139 :label => cf.name}
151 end
140 end
152
141
153 # Add list and boolean time entry activity custom fields
142 # Add list and boolean time entry activity custom fields
154 TimeEntryActivityCustomField.all.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
143 TimeEntryActivityCustomField.all.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
155 @available_criteria["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Enumeration' AND c.customized_id = #{TimeEntry.table_name}.activity_id ORDER BY c.value LIMIT 1)",
144 @available_criteria["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Enumeration' AND c.customized_id = #{TimeEntry.table_name}.activity_id ORDER BY c.value LIMIT 1)",
156 :format => cf.field_format,
145 :format => cf.field_format,
157 :label => cf.name}
146 :label => cf.name}
158 end
147 end
159
148
160 @available_criteria
149 @available_criteria
161 end
150 end
162 end
151 end
163 end
152 end
164 end
153 end
@@ -1,605 +1,611
1 /* Redmine - project management software
1 /* Redmine - project management software
2 Copyright (C) 2006-2012 Jean-Philippe Lang */
2 Copyright (C) 2006-2012 Jean-Philippe Lang */
3
3
4 function checkAll(id, checked) {
4 function checkAll(id, checked) {
5 if (checked) {
5 if (checked) {
6 $('#'+id).find('input[type=checkbox]').attr('checked', true);
6 $('#'+id).find('input[type=checkbox]').attr('checked', true);
7 } else {
7 } else {
8 $('#'+id).find('input[type=checkbox]').removeAttr('checked');
8 $('#'+id).find('input[type=checkbox]').removeAttr('checked');
9 }
9 }
10 }
10 }
11
11
12 function toggleCheckboxesBySelector(selector) {
12 function toggleCheckboxesBySelector(selector) {
13 var all_checked = true;
13 var all_checked = true;
14 $(selector).each(function(index) {
14 $(selector).each(function(index) {
15 if (!$(this).is(':checked')) { all_checked = false; }
15 if (!$(this).is(':checked')) { all_checked = false; }
16 });
16 });
17 $(selector).attr('checked', !all_checked)
17 $(selector).attr('checked', !all_checked)
18 }
18 }
19
19
20 function showAndScrollTo(id, focus) {
20 function showAndScrollTo(id, focus) {
21 $('#'+id).show();
21 $('#'+id).show();
22 if (focus!=null) {
22 if (focus!=null) {
23 $('#'+focus).focus();
23 $('#'+focus).focus();
24 }
24 }
25 $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100);
25 $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100);
26 }
26 }
27
27
28 function toggleRowGroup(el) {
28 function toggleRowGroup(el) {
29 var tr = $(el).parents('tr').first();
29 var tr = $(el).parents('tr').first();
30 var n = tr.next();
30 var n = tr.next();
31 tr.toggleClass('open');
31 tr.toggleClass('open');
32 while (n.length && !n.hasClass('group')) {
32 while (n.length && !n.hasClass('group')) {
33 n.toggle();
33 n.toggle();
34 n = n.next('tr');
34 n = n.next('tr');
35 }
35 }
36 }
36 }
37
37
38 function collapseAllRowGroups(el) {
38 function collapseAllRowGroups(el) {
39 var tbody = $(el).parents('tbody').first();
39 var tbody = $(el).parents('tbody').first();
40 tbody.children('tr').each(function(index) {
40 tbody.children('tr').each(function(index) {
41 if ($(this).hasClass('group')) {
41 if ($(this).hasClass('group')) {
42 $(this).removeClass('open');
42 $(this).removeClass('open');
43 } else {
43 } else {
44 $(this).hide();
44 $(this).hide();
45 }
45 }
46 });
46 });
47 }
47 }
48
48
49 function expandAllRowGroups(el) {
49 function expandAllRowGroups(el) {
50 var tbody = $(el).parents('tbody').first();
50 var tbody = $(el).parents('tbody').first();
51 tbody.children('tr').each(function(index) {
51 tbody.children('tr').each(function(index) {
52 if ($(this).hasClass('group')) {
52 if ($(this).hasClass('group')) {
53 $(this).addClass('open');
53 $(this).addClass('open');
54 } else {
54 } else {
55 $(this).show();
55 $(this).show();
56 }
56 }
57 });
57 });
58 }
58 }
59
59
60 function toggleAllRowGroups(el) {
60 function toggleAllRowGroups(el) {
61 var tr = $(el).parents('tr').first();
61 var tr = $(el).parents('tr').first();
62 if (tr.hasClass('open')) {
62 if (tr.hasClass('open')) {
63 collapseAllRowGroups(el);
63 collapseAllRowGroups(el);
64 } else {
64 } else {
65 expandAllRowGroups(el);
65 expandAllRowGroups(el);
66 }
66 }
67 }
67 }
68
68
69 function toggleFieldset(el) {
69 function toggleFieldset(el) {
70 var fieldset = $(el).parents('fieldset').first();
70 var fieldset = $(el).parents('fieldset').first();
71 fieldset.toggleClass('collapsed');
71 fieldset.toggleClass('collapsed');
72 fieldset.children('div').toggle();
72 fieldset.children('div').toggle();
73 }
73 }
74
74
75 function hideFieldset(el) {
75 function hideFieldset(el) {
76 var fieldset = $(el).parents('fieldset').first();
76 var fieldset = $(el).parents('fieldset').first();
77 fieldset.toggleClass('collapsed');
77 fieldset.toggleClass('collapsed');
78 fieldset.children('div').hide();
78 fieldset.children('div').hide();
79 }
79 }
80
80
81 function initFilters(){
81 function initFilters(){
82 $('#add_filter_select').change(function(){
82 $('#add_filter_select').change(function(){
83 addFilter($(this).val(), '', []);
83 addFilter($(this).val(), '', []);
84 });
84 });
85 $('#filters-table td.field input[type=checkbox]').each(function(){
85 $('#filters-table td.field input[type=checkbox]').each(function(){
86 toggleFilter($(this).val());
86 toggleFilter($(this).val());
87 });
87 });
88 $('#filters-table td.field input[type=checkbox]').live('click',function(){
88 $('#filters-table td.field input[type=checkbox]').live('click',function(){
89 toggleFilter($(this).val());
89 toggleFilter($(this).val());
90 });
90 });
91 $('#filters-table .toggle-multiselect').live('click',function(){
91 $('#filters-table .toggle-multiselect').live('click',function(){
92 toggleMultiSelect($(this).siblings('select'));
92 toggleMultiSelect($(this).siblings('select'));
93 });
93 });
94 $('#filters-table input[type=text]').live('keypress', function(e){
94 $('#filters-table input[type=text]').live('keypress', function(e){
95 if (e.keyCode == 13) submit_query_form("query_form");
95 if (e.keyCode == 13) submit_query_form("query_form");
96 });
96 });
97 }
97 }
98
98
99 function addFilter(field, operator, values) {
99 function addFilter(field, operator, values) {
100 var fieldId = field.replace('.', '_');
100 var fieldId = field.replace('.', '_');
101 var tr = $('#tr_'+fieldId);
101 var tr = $('#tr_'+fieldId);
102 if (tr.length > 0) {
102 if (tr.length > 0) {
103 tr.show();
103 tr.show();
104 } else {
104 } else {
105 buildFilterRow(field, operator, values);
105 buildFilterRow(field, operator, values);
106 }
106 }
107 $('#cb_'+fieldId).attr('checked', true);
107 $('#cb_'+fieldId).attr('checked', true);
108 toggleFilter(field);
108 toggleFilter(field);
109 $('#add_filter_select').val('').children('option').each(function(){
109 $('#add_filter_select').val('').children('option').each(function(){
110 if ($(this).attr('value') == field) {
110 if ($(this).attr('value') == field) {
111 $(this).attr('disabled', true);
111 $(this).attr('disabled', true);
112 }
112 }
113 });
113 });
114 }
114 }
115
115
116 function buildFilterRow(field, operator, values) {
116 function buildFilterRow(field, operator, values) {
117 var fieldId = field.replace('.', '_');
117 var fieldId = field.replace('.', '_');
118 var filterTable = $("#filters-table");
118 var filterTable = $("#filters-table");
119 var filterOptions = availableFilters[field];
119 var filterOptions = availableFilters[field];
120 var operators = operatorByType[filterOptions['type']];
120 var operators = operatorByType[filterOptions['type']];
121 var filterValues = filterOptions['values'];
121 var filterValues = filterOptions['values'];
122 var i, select;
122 var i, select;
123
123
124 var tr = $('<tr class="filter">').attr('id', 'tr_'+fieldId).html(
124 var tr = $('<tr class="filter">').attr('id', 'tr_'+fieldId).html(
125 '<td class="field"><input checked="checked" id="cb_'+fieldId+'" name="f[]" value="'+field+'" type="checkbox"><label for="cb_'+fieldId+'"> '+filterOptions['name']+'</label></td>' +
125 '<td class="field"><input checked="checked" id="cb_'+fieldId+'" name="f[]" value="'+field+'" type="checkbox"><label for="cb_'+fieldId+'"> '+filterOptions['name']+'</label></td>' +
126 '<td class="operator"><select id="operators_'+fieldId+'" name="op['+field+']"></td>' +
126 '<td class="operator"><select id="operators_'+fieldId+'" name="op['+field+']"></td>' +
127 '<td class="values"></td>'
127 '<td class="values"></td>'
128 );
128 );
129 filterTable.append(tr);
129 filterTable.append(tr);
130
130
131 select = tr.find('td.operator select');
131 select = tr.find('td.operator select');
132 for (i=0;i<operators.length;i++){
132 for (i=0;i<operators.length;i++){
133 var option = $('<option>').val(operators[i]).text(operatorLabels[operators[i]]);
133 var option = $('<option>').val(operators[i]).text(operatorLabels[operators[i]]);
134 if (operators[i] == operator) {option.attr('selected', true)};
134 if (operators[i] == operator) {option.attr('selected', true)};
135 select.append(option);
135 select.append(option);
136 }
136 }
137 select.change(function(){toggleOperator(field)});
137 select.change(function(){toggleOperator(field)});
138
138
139 switch (filterOptions['type']){
139 switch (filterOptions['type']){
140 case "list":
140 case "list":
141 case "list_optional":
141 case "list_optional":
142 case "list_status":
142 case "list_status":
143 case "list_subprojects":
143 case "list_subprojects":
144 tr.find('td.values').append(
144 tr.find('td.values').append(
145 '<span style="display:none;"><select class="value" id="values_'+fieldId+'_1" name="v['+field+'][]"></select>' +
145 '<span style="display:none;"><select class="value" id="values_'+fieldId+'_1" name="v['+field+'][]"></select>' +
146 ' <span class="toggle-multiselect">&nbsp;</span></span>'
146 ' <span class="toggle-multiselect">&nbsp;</span></span>'
147 );
147 );
148 select = tr.find('td.values select');
148 select = tr.find('td.values select');
149 if (values.length > 1) {select.attr('multiple', true)};
149 if (values.length > 1) {select.attr('multiple', true)};
150 for (i=0;i<filterValues.length;i++){
150 for (i=0;i<filterValues.length;i++){
151 var filterValue = filterValues[i];
151 var filterValue = filterValues[i];
152 var option = $('<option>');
152 var option = $('<option>');
153 if ($.isArray(filterValue)) {
153 if ($.isArray(filterValue)) {
154 option.val(filterValue[1]).text(filterValue[0]);
154 option.val(filterValue[1]).text(filterValue[0]);
155 if ($.inArray(filterValue[1], values) > -1) {option.attr('selected', true);}
155 if ($.inArray(filterValue[1], values) > -1) {option.attr('selected', true);}
156 } else {
156 } else {
157 option.val(filterValue).text(filterValue);
157 option.val(filterValue).text(filterValue);
158 if ($.inArray(filterValue, values) > -1) {option.attr('selected', true);}
158 if ($.inArray(filterValue, values) > -1) {option.attr('selected', true);}
159 }
159 }
160 select.append(option);
160 select.append(option);
161 }
161 }
162 break;
162 break;
163 case "date":
163 case "date":
164 case "date_past":
164 case "date_past":
165 tr.find('td.values').append(
165 tr.find('td.values').append(
166 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" /></span>' +
166 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" /></span>' +
167 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" /></span>' +
167 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" /></span>' +
168 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="3" class="value" /> '+labelDayPlural+'</span>'
168 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="3" class="value" /> '+labelDayPlural+'</span>'
169 );
169 );
170 $('#values_'+fieldId+'_1').val(values[0]).datepicker(datepickerOptions);
170 $('#values_'+fieldId+'_1').val(values[0]).datepicker(datepickerOptions);
171 $('#values_'+fieldId+'_2').val(values[1]).datepicker(datepickerOptions);
171 $('#values_'+fieldId+'_2').val(values[1]).datepicker(datepickerOptions);
172 $('#values_'+fieldId).val(values[0]);
172 $('#values_'+fieldId).val(values[0]);
173 break;
173 break;
174 case "string":
174 case "string":
175 case "text":
175 case "text":
176 tr.find('td.values').append(
176 tr.find('td.values').append(
177 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="30" class="value" /></span>'
177 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="30" class="value" /></span>'
178 );
178 );
179 $('#values_'+fieldId).val(values[0]);
179 $('#values_'+fieldId).val(values[0]);
180 break;
180 break;
181 case "relation":
181 case "relation":
182 tr.find('td.values').append(
182 tr.find('td.values').append(
183 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="6" class="value" /></span>' +
183 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="6" class="value" /></span>' +
184 '<span style="display:none;"><select class="value" name="v['+field+'][]" id="values_'+fieldId+'_1"></select></span>'
184 '<span style="display:none;"><select class="value" name="v['+field+'][]" id="values_'+fieldId+'_1"></select></span>'
185 );
185 );
186 $('#values_'+fieldId).val(values[0]);
186 $('#values_'+fieldId).val(values[0]);
187 select = tr.find('td.values select');
187 select = tr.find('td.values select');
188 for (i=0;i<allProjects.length;i++){
188 for (i=0;i<allProjects.length;i++){
189 var filterValue = allProjects[i];
189 var filterValue = allProjects[i];
190 var option = $('<option>');
190 var option = $('<option>');
191 option.val(filterValue[1]).text(filterValue[0]);
191 option.val(filterValue[1]).text(filterValue[0]);
192 if (values[0] == filterValue[1]) {option.attr('selected', true)};
192 if (values[0] == filterValue[1]) {option.attr('selected', true)};
193 select.append(option);
193 select.append(option);
194 }
194 }
195 case "integer":
195 case "integer":
196 case "float":
196 case "float":
197 tr.find('td.values').append(
197 tr.find('td.values').append(
198 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="6" class="value" /></span>' +
198 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="6" class="value" /></span>' +
199 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="6" class="value" /></span>'
199 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="6" class="value" /></span>'
200 );
200 );
201 $('#values_'+fieldId+'_1').val(values[0]);
201 $('#values_'+fieldId+'_1').val(values[0]);
202 $('#values_'+fieldId+'_2').val(values[1]);
202 $('#values_'+fieldId+'_2').val(values[1]);
203 break;
203 break;
204 }
204 }
205 }
205 }
206
206
207 function toggleFilter(field) {
207 function toggleFilter(field) {
208 var fieldId = field.replace('.', '_');
208 var fieldId = field.replace('.', '_');
209 if ($('#cb_' + fieldId).is(':checked')) {
209 if ($('#cb_' + fieldId).is(':checked')) {
210 $("#operators_" + fieldId).show().removeAttr('disabled');
210 $("#operators_" + fieldId).show().removeAttr('disabled');
211 toggleOperator(field);
211 toggleOperator(field);
212 } else {
212 } else {
213 $("#operators_" + fieldId).hide().attr('disabled', true);
213 $("#operators_" + fieldId).hide().attr('disabled', true);
214 enableValues(field, []);
214 enableValues(field, []);
215 }
215 }
216 }
216 }
217
217
218 function enableValues(field, indexes) {
218 function enableValues(field, indexes) {
219 var fieldId = field.replace('.', '_');
219 var fieldId = field.replace('.', '_');
220 $('#tr_'+fieldId+' td.values .value').each(function(index) {
220 $('#tr_'+fieldId+' td.values .value').each(function(index) {
221 if ($.inArray(index, indexes) >= 0) {
221 if ($.inArray(index, indexes) >= 0) {
222 $(this).removeAttr('disabled');
222 $(this).removeAttr('disabled');
223 $(this).parents('span').first().show();
223 $(this).parents('span').first().show();
224 } else {
224 } else {
225 $(this).val('');
225 $(this).val('');
226 $(this).attr('disabled', true);
226 $(this).attr('disabled', true);
227 $(this).parents('span').first().hide();
227 $(this).parents('span').first().hide();
228 }
228 }
229
229
230 if ($(this).hasClass('group')) {
230 if ($(this).hasClass('group')) {
231 $(this).addClass('open');
231 $(this).addClass('open');
232 } else {
232 } else {
233 $(this).show();
233 $(this).show();
234 }
234 }
235 });
235 });
236 }
236 }
237
237
238 function toggleOperator(field) {
238 function toggleOperator(field) {
239 var fieldId = field.replace('.', '_');
239 var fieldId = field.replace('.', '_');
240 var operator = $("#operators_" + fieldId);
240 var operator = $("#operators_" + fieldId);
241 switch (operator.val()) {
241 switch (operator.val()) {
242 case "!*":
242 case "!*":
243 case "*":
243 case "*":
244 case "t":
244 case "t":
245 case "ld":
245 case "w":
246 case "w":
247 case "lw":
248 case "l2w":
249 case "m":
250 case "lm":
251 case "y":
246 case "o":
252 case "o":
247 case "c":
253 case "c":
248 enableValues(field, []);
254 enableValues(field, []);
249 break;
255 break;
250 case "><":
256 case "><":
251 enableValues(field, [0,1]);
257 enableValues(field, [0,1]);
252 break;
258 break;
253 case "<t+":
259 case "<t+":
254 case ">t+":
260 case ">t+":
255 case "><t+":
261 case "><t+":
256 case "t+":
262 case "t+":
257 case ">t-":
263 case ">t-":
258 case "<t-":
264 case "<t-":
259 case "><t-":
265 case "><t-":
260 case "t-":
266 case "t-":
261 enableValues(field, [2]);
267 enableValues(field, [2]);
262 break;
268 break;
263 case "=p":
269 case "=p":
264 case "=!p":
270 case "=!p":
265 case "!p":
271 case "!p":
266 enableValues(field, [1]);
272 enableValues(field, [1]);
267 break;
273 break;
268 default:
274 default:
269 enableValues(field, [0]);
275 enableValues(field, [0]);
270 break;
276 break;
271 }
277 }
272 }
278 }
273
279
274 function toggleMultiSelect(el) {
280 function toggleMultiSelect(el) {
275 if (el.attr('multiple')) {
281 if (el.attr('multiple')) {
276 el.removeAttr('multiple');
282 el.removeAttr('multiple');
277 } else {
283 } else {
278 el.attr('multiple', true);
284 el.attr('multiple', true);
279 }
285 }
280 }
286 }
281
287
282 function submit_query_form(id) {
288 function submit_query_form(id) {
283 selectAllOptions("selected_columns");
289 selectAllOptions("selected_columns");
284 $('#'+id).submit();
290 $('#'+id).submit();
285 }
291 }
286
292
287 var fileFieldCount = 1;
293 var fileFieldCount = 1;
288 function addFileField() {
294 function addFileField() {
289 var fields = $('#attachments_fields');
295 var fields = $('#attachments_fields');
290 if (fields.children().length >= 10) return false;
296 if (fields.children().length >= 10) return false;
291 fileFieldCount++;
297 fileFieldCount++;
292 var s = fields.children('span').first().clone();
298 var s = fields.children('span').first().clone();
293 s.children('input.file').attr('name', "attachments[" + fileFieldCount + "][file]").val('');
299 s.children('input.file').attr('name', "attachments[" + fileFieldCount + "][file]").val('');
294 s.children('input.description').attr('name', "attachments[" + fileFieldCount + "][description]").val('');
300 s.children('input.description').attr('name', "attachments[" + fileFieldCount + "][description]").val('');
295 fields.append(s);
301 fields.append(s);
296 }
302 }
297
303
298 function removeFileField(el) {
304 function removeFileField(el) {
299 var fields = $('#attachments_fields');
305 var fields = $('#attachments_fields');
300 var s = $(el).parents('span').first();
306 var s = $(el).parents('span').first();
301 if (fields.children().length > 1) {
307 if (fields.children().length > 1) {
302 s.remove();
308 s.remove();
303 } else {
309 } else {
304 s.children('input.file').val('');
310 s.children('input.file').val('');
305 s.children('input.description').val('');
311 s.children('input.description').val('');
306 }
312 }
307 }
313 }
308
314
309 function checkFileSize(el, maxSize, message) {
315 function checkFileSize(el, maxSize, message) {
310 var files = el.files;
316 var files = el.files;
311 if (files) {
317 if (files) {
312 for (var i=0; i<files.length; i++) {
318 for (var i=0; i<files.length; i++) {
313 if (files[i].size > maxSize) {
319 if (files[i].size > maxSize) {
314 alert(message);
320 alert(message);
315 el.value = "";
321 el.value = "";
316 }
322 }
317 }
323 }
318 }
324 }
319 }
325 }
320
326
321 function showTab(name) {
327 function showTab(name) {
322 $('div#content .tab-content').hide();
328 $('div#content .tab-content').hide();
323 $('div.tabs a').removeClass('selected');
329 $('div.tabs a').removeClass('selected');
324 $('#tab-content-' + name).show();
330 $('#tab-content-' + name).show();
325 $('#tab-' + name).addClass('selected');
331 $('#tab-' + name).addClass('selected');
326 return false;
332 return false;
327 }
333 }
328
334
329 function moveTabRight(el) {
335 function moveTabRight(el) {
330 var lis = $(el).parents('div.tabs').first().find('ul').children();
336 var lis = $(el).parents('div.tabs').first().find('ul').children();
331 var tabsWidth = 0;
337 var tabsWidth = 0;
332 var i = 0;
338 var i = 0;
333 lis.each(function(){
339 lis.each(function(){
334 if ($(this).is(':visible')) {
340 if ($(this).is(':visible')) {
335 tabsWidth += $(this).width() + 6;
341 tabsWidth += $(this).width() + 6;
336 }
342 }
337 });
343 });
338 if (tabsWidth < $(el).parents('div.tabs').first().width() - 60) { return; }
344 if (tabsWidth < $(el).parents('div.tabs').first().width() - 60) { return; }
339 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
345 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
340 lis.eq(i).hide();
346 lis.eq(i).hide();
341 }
347 }
342
348
343 function moveTabLeft(el) {
349 function moveTabLeft(el) {
344 var lis = $(el).parents('div.tabs').first().find('ul').children();
350 var lis = $(el).parents('div.tabs').first().find('ul').children();
345 var i = 0;
351 var i = 0;
346 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
352 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
347 if (i>0) {
353 if (i>0) {
348 lis.eq(i-1).show();
354 lis.eq(i-1).show();
349 }
355 }
350 }
356 }
351
357
352 function displayTabsButtons() {
358 function displayTabsButtons() {
353 var lis;
359 var lis;
354 var tabsWidth = 0;
360 var tabsWidth = 0;
355 var el;
361 var el;
356 $('div.tabs').each(function() {
362 $('div.tabs').each(function() {
357 el = $(this);
363 el = $(this);
358 lis = el.find('ul').children();
364 lis = el.find('ul').children();
359 lis.each(function(){
365 lis.each(function(){
360 if ($(this).is(':visible')) {
366 if ($(this).is(':visible')) {
361 tabsWidth += $(this).width() + 6;
367 tabsWidth += $(this).width() + 6;
362 }
368 }
363 });
369 });
364 if ((tabsWidth < el.width() - 60) && (lis.first().is(':visible'))) {
370 if ((tabsWidth < el.width() - 60) && (lis.first().is(':visible'))) {
365 el.find('div.tabs-buttons').hide();
371 el.find('div.tabs-buttons').hide();
366 } else {
372 } else {
367 el.find('div.tabs-buttons').show();
373 el.find('div.tabs-buttons').show();
368 }
374 }
369 });
375 });
370 }
376 }
371
377
372 function setPredecessorFieldsVisibility() {
378 function setPredecessorFieldsVisibility() {
373 var relationType = $('#relation_relation_type');
379 var relationType = $('#relation_relation_type');
374 if (relationType.val() == "precedes" || relationType.val() == "follows") {
380 if (relationType.val() == "precedes" || relationType.val() == "follows") {
375 $('#predecessor_fields').show();
381 $('#predecessor_fields').show();
376 } else {
382 } else {
377 $('#predecessor_fields').hide();
383 $('#predecessor_fields').hide();
378 }
384 }
379 }
385 }
380
386
381 function showModal(id, width) {
387 function showModal(id, width) {
382 var el = $('#'+id).first();
388 var el = $('#'+id).first();
383 if (el.length == 0 || el.is(':visible')) {return;}
389 if (el.length == 0 || el.is(':visible')) {return;}
384 var title = el.find('h3.title').text();
390 var title = el.find('h3.title').text();
385 el.dialog({
391 el.dialog({
386 width: width,
392 width: width,
387 modal: true,
393 modal: true,
388 resizable: false,
394 resizable: false,
389 dialogClass: 'modal',
395 dialogClass: 'modal',
390 title: title
396 title: title
391 });
397 });
392 el.find("input[type=text], input[type=submit]").first().focus();
398 el.find("input[type=text], input[type=submit]").first().focus();
393 }
399 }
394
400
395 function hideModal(el) {
401 function hideModal(el) {
396 var modal;
402 var modal;
397 if (el) {
403 if (el) {
398 modal = $(el).parents('.ui-dialog-content');
404 modal = $(el).parents('.ui-dialog-content');
399 } else {
405 } else {
400 modal = $('#ajax-modal');
406 modal = $('#ajax-modal');
401 }
407 }
402 modal.dialog("close");
408 modal.dialog("close");
403 }
409 }
404
410
405 function submitPreview(url, form, target) {
411 function submitPreview(url, form, target) {
406 $.ajax({
412 $.ajax({
407 url: url,
413 url: url,
408 type: 'post',
414 type: 'post',
409 data: $('#'+form).serialize(),
415 data: $('#'+form).serialize(),
410 success: function(data){
416 success: function(data){
411 $('#'+target).html(data);
417 $('#'+target).html(data);
412 }
418 }
413 });
419 });
414 }
420 }
415
421
416 function collapseScmEntry(id) {
422 function collapseScmEntry(id) {
417 $('.'+id).each(function() {
423 $('.'+id).each(function() {
418 if ($(this).hasClass('open')) {
424 if ($(this).hasClass('open')) {
419 collapseScmEntry($(this).attr('id'));
425 collapseScmEntry($(this).attr('id'));
420 }
426 }
421 $(this).hide();
427 $(this).hide();
422 });
428 });
423 $('#'+id).removeClass('open');
429 $('#'+id).removeClass('open');
424 }
430 }
425
431
426 function expandScmEntry(id) {
432 function expandScmEntry(id) {
427 $('.'+id).each(function() {
433 $('.'+id).each(function() {
428 $(this).show();
434 $(this).show();
429 if ($(this).hasClass('loaded') && !$(this).hasClass('collapsed')) {
435 if ($(this).hasClass('loaded') && !$(this).hasClass('collapsed')) {
430 expandScmEntry($(this).attr('id'));
436 expandScmEntry($(this).attr('id'));
431 }
437 }
432 });
438 });
433 $('#'+id).addClass('open');
439 $('#'+id).addClass('open');
434 }
440 }
435
441
436 function scmEntryClick(id, url) {
442 function scmEntryClick(id, url) {
437 el = $('#'+id);
443 el = $('#'+id);
438 if (el.hasClass('open')) {
444 if (el.hasClass('open')) {
439 collapseScmEntry(id);
445 collapseScmEntry(id);
440 el.addClass('collapsed');
446 el.addClass('collapsed');
441 return false;
447 return false;
442 } else if (el.hasClass('loaded')) {
448 } else if (el.hasClass('loaded')) {
443 expandScmEntry(id);
449 expandScmEntry(id);
444 el.removeClass('collapsed');
450 el.removeClass('collapsed');
445 return false;
451 return false;
446 }
452 }
447 if (el.hasClass('loading')) {
453 if (el.hasClass('loading')) {
448 return false;
454 return false;
449 }
455 }
450 el.addClass('loading');
456 el.addClass('loading');
451 $.ajax({
457 $.ajax({
452 url: url,
458 url: url,
453 success: function(data){
459 success: function(data){
454 el.after(data);
460 el.after(data);
455 el.addClass('open').addClass('loaded').removeClass('loading');
461 el.addClass('open').addClass('loaded').removeClass('loading');
456 }
462 }
457 });
463 });
458 return true;
464 return true;
459 }
465 }
460
466
461 function randomKey(size) {
467 function randomKey(size) {
462 var chars = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z');
468 var chars = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z');
463 var key = '';
469 var key = '';
464 for (i = 0; i < size; i++) {
470 for (i = 0; i < size; i++) {
465 key += chars[Math.floor(Math.random() * chars.length)];
471 key += chars[Math.floor(Math.random() * chars.length)];
466 }
472 }
467 return key;
473 return key;
468 }
474 }
469
475
470 // Can't use Rails' remote select because we need the form data
476 // Can't use Rails' remote select because we need the form data
471 function updateIssueFrom(url) {
477 function updateIssueFrom(url) {
472 $.ajax({
478 $.ajax({
473 url: url,
479 url: url,
474 type: 'post',
480 type: 'post',
475 data: $('#issue-form').serialize()
481 data: $('#issue-form').serialize()
476 });
482 });
477 }
483 }
478
484
479 function updateBulkEditFrom(url) {
485 function updateBulkEditFrom(url) {
480 $.ajax({
486 $.ajax({
481 url: url,
487 url: url,
482 type: 'post',
488 type: 'post',
483 data: $('#bulk_edit_form').serialize()
489 data: $('#bulk_edit_form').serialize()
484 });
490 });
485 }
491 }
486
492
487 function observeAutocompleteField(fieldId, url) {
493 function observeAutocompleteField(fieldId, url) {
488 $(document).ready(function() {
494 $(document).ready(function() {
489 $('#'+fieldId).autocomplete({
495 $('#'+fieldId).autocomplete({
490 source: url,
496 source: url,
491 minLength: 2
497 minLength: 2
492 });
498 });
493 });
499 });
494 }
500 }
495
501
496 function observeSearchfield(fieldId, targetId, url) {
502 function observeSearchfield(fieldId, targetId, url) {
497 $('#'+fieldId).each(function() {
503 $('#'+fieldId).each(function() {
498 var $this = $(this);
504 var $this = $(this);
499 $this.attr('data-value-was', $this.val());
505 $this.attr('data-value-was', $this.val());
500 var check = function() {
506 var check = function() {
501 var val = $this.val();
507 var val = $this.val();
502 if ($this.attr('data-value-was') != val){
508 if ($this.attr('data-value-was') != val){
503 $this.attr('data-value-was', val);
509 $this.attr('data-value-was', val);
504 $.ajax({
510 $.ajax({
505 url: url,
511 url: url,
506 type: 'get',
512 type: 'get',
507 data: {q: $this.val()},
513 data: {q: $this.val()},
508 success: function(data){ $('#'+targetId).html(data); },
514 success: function(data){ $('#'+targetId).html(data); },
509 beforeSend: function(){ $this.addClass('ajax-loading'); },
515 beforeSend: function(){ $this.addClass('ajax-loading'); },
510 complete: function(){ $this.removeClass('ajax-loading'); }
516 complete: function(){ $this.removeClass('ajax-loading'); }
511 });
517 });
512 }
518 }
513 };
519 };
514 var reset = function() {
520 var reset = function() {
515 if (timer) {
521 if (timer) {
516 clearInterval(timer);
522 clearInterval(timer);
517 timer = setInterval(check, 300);
523 timer = setInterval(check, 300);
518 }
524 }
519 };
525 };
520 var timer = setInterval(check, 300);
526 var timer = setInterval(check, 300);
521 $this.bind('keyup click mousemove', reset);
527 $this.bind('keyup click mousemove', reset);
522 });
528 });
523 }
529 }
524
530
525 function observeProjectModules() {
531 function observeProjectModules() {
526 var f = function() {
532 var f = function() {
527 /* Hides trackers and issues custom fields on the new project form when issue_tracking module is disabled */
533 /* Hides trackers and issues custom fields on the new project form when issue_tracking module is disabled */
528 if ($('#project_enabled_module_names_issue_tracking').attr('checked')) {
534 if ($('#project_enabled_module_names_issue_tracking').attr('checked')) {
529 $('#project_trackers').show();
535 $('#project_trackers').show();
530 }else{
536 }else{
531 $('#project_trackers').hide();
537 $('#project_trackers').hide();
532 }
538 }
533 };
539 };
534
540
535 $(window).load(f);
541 $(window).load(f);
536 $('#project_enabled_module_names_issue_tracking').change(f);
542 $('#project_enabled_module_names_issue_tracking').change(f);
537 }
543 }
538
544
539 function initMyPageSortable(list, url) {
545 function initMyPageSortable(list, url) {
540 $('#list-'+list).sortable({
546 $('#list-'+list).sortable({
541 connectWith: '.block-receiver',
547 connectWith: '.block-receiver',
542 tolerance: 'pointer',
548 tolerance: 'pointer',
543 update: function(){
549 update: function(){
544 $.ajax({
550 $.ajax({
545 url: url,
551 url: url,
546 type: 'post',
552 type: 'post',
547 data: {'blocks': $.map($('#list-'+list).children(), function(el){return $(el).attr('id');})}
553 data: {'blocks': $.map($('#list-'+list).children(), function(el){return $(el).attr('id');})}
548 });
554 });
549 }
555 }
550 });
556 });
551 $("#list-top, #list-left, #list-right").disableSelection();
557 $("#list-top, #list-left, #list-right").disableSelection();
552 }
558 }
553
559
554 var warnLeavingUnsavedMessage;
560 var warnLeavingUnsavedMessage;
555 function warnLeavingUnsaved(message) {
561 function warnLeavingUnsaved(message) {
556 warnLeavingUnsavedMessage = message;
562 warnLeavingUnsavedMessage = message;
557
563
558 $('form').submit(function(){
564 $('form').submit(function(){
559 $('textarea').removeData('changed');
565 $('textarea').removeData('changed');
560 });
566 });
561 $('textarea').change(function(){
567 $('textarea').change(function(){
562 $(this).data('changed', 'changed');
568 $(this).data('changed', 'changed');
563 });
569 });
564 window.onbeforeunload = function(){
570 window.onbeforeunload = function(){
565 var warn = false;
571 var warn = false;
566 $('textarea').blur().each(function(){
572 $('textarea').blur().each(function(){
567 if ($(this).data('changed')) {
573 if ($(this).data('changed')) {
568 warn = true;
574 warn = true;
569 }
575 }
570 });
576 });
571 if (warn) {return warnLeavingUnsavedMessage;}
577 if (warn) {return warnLeavingUnsavedMessage;}
572 };
578 };
573 };
579 };
574
580
575 $(document).ready(function(){
581 $(document).ready(function(){
576 $('#ajax-indicator').bind('ajaxSend', function(){
582 $('#ajax-indicator').bind('ajaxSend', function(){
577 if ($('.ajax-loading').length == 0) {
583 if ($('.ajax-loading').length == 0) {
578 $('#ajax-indicator').show();
584 $('#ajax-indicator').show();
579 }
585 }
580 });
586 });
581 $('#ajax-indicator').bind('ajaxStop', function(){
587 $('#ajax-indicator').bind('ajaxStop', function(){
582 $('#ajax-indicator').hide();
588 $('#ajax-indicator').hide();
583 });
589 });
584 });
590 });
585
591
586 function hideOnLoad() {
592 function hideOnLoad() {
587 $('.hol').hide();
593 $('.hol').hide();
588 }
594 }
589
595
590 function addFormObserversForDoubleSubmit() {
596 function addFormObserversForDoubleSubmit() {
591 $('form[method=post]').each(function() {
597 $('form[method=post]').each(function() {
592 if (!$(this).hasClass('multiple-submit')) {
598 if (!$(this).hasClass('multiple-submit')) {
593 $(this).submit(function(form_submission) {
599 $(this).submit(function(form_submission) {
594 if ($(form_submission.target).attr('data-submitted')) {
600 if ($(form_submission.target).attr('data-submitted')) {
595 form_submission.preventDefault();
601 form_submission.preventDefault();
596 } else {
602 } else {
597 $(form_submission.target).attr('data-submitted', true);
603 $(form_submission.target).attr('data-submitted', true);
598 }
604 }
599 });
605 });
600 }
606 }
601 });
607 });
602 }
608 }
603
609
604 $(document).ready(hideOnLoad);
610 $(document).ready(hideOnLoad);
605 $(document).ready(addFormObserversForDoubleSubmit);
611 $(document).ready(addFormObserversForDoubleSubmit);
@@ -1,334 +1,332
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # Redmine - project management software
2 # Redmine - project management software
3 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 # Copyright (C) 2006-2012 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 :issues, :time_entries, :users, :trackers, :enumerations,
25 :issues, :time_entries, :users, :trackers, :enumerations,
26 :issue_statuses, :custom_fields, :custom_values
26 :issue_statuses, :custom_fields, :custom_values
27
27
28 include Redmine::I18n
28 include Redmine::I18n
29
29
30 def setup
30 def setup
31 Setting.default_language = "en"
31 Setting.default_language = "en"
32 end
32 end
33
33
34 def test_report_at_project_level
34 def test_report_at_project_level
35 get :report, :project_id => 'ecookbook'
35 get :report, :project_id => 'ecookbook'
36 assert_response :success
36 assert_response :success
37 assert_template 'report'
37 assert_template 'report'
38 assert_tag :form,
38 assert_tag :form,
39 :attributes => {:action => "/projects/ecookbook/time_entries/report", :id => 'query_form'}
39 :attributes => {:action => "/projects/ecookbook/time_entries/report", :id => 'query_form'}
40 end
40 end
41
41
42 def test_report_all_projects
42 def test_report_all_projects
43 get :report
43 get :report
44 assert_response :success
44 assert_response :success
45 assert_template 'report'
45 assert_template 'report'
46 assert_tag :form,
46 assert_tag :form,
47 :attributes => {:action => "/time_entries/report", :id => 'query_form'}
47 :attributes => {:action => "/time_entries/report", :id => 'query_form'}
48 end
48 end
49
49
50 def test_report_all_projects_denied
50 def test_report_all_projects_denied
51 r = Role.anonymous
51 r = Role.anonymous
52 r.permissions.delete(:view_time_entries)
52 r.permissions.delete(:view_time_entries)
53 r.permissions_will_change!
53 r.permissions_will_change!
54 r.save
54 r.save
55 get :report
55 get :report
56 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Ftime_entries%2Freport'
56 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Ftime_entries%2Freport'
57 end
57 end
58
58
59 def test_report_all_projects_one_criteria
59 def test_report_all_projects_one_criteria
60 get :report, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criteria => ['project']
60 get :report, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criteria => ['project']
61 assert_response :success
61 assert_response :success
62 assert_template 'report'
62 assert_template 'report'
63 assert_not_nil assigns(:report)
63 assert_not_nil assigns(:report)
64 assert_equal "8.65", "%.2f" % assigns(:report).total_hours
64 assert_equal "8.65", "%.2f" % assigns(:report).total_hours
65 end
65 end
66
66
67 def test_report_all_time
67 def test_report_all_time
68 get :report, :project_id => 1, :criteria => ['project', 'issue']
68 get :report, :project_id => 1, :criteria => ['project', 'issue']
69 assert_response :success
69 assert_response :success
70 assert_template 'report'
70 assert_template 'report'
71 assert_not_nil assigns(:report)
71 assert_not_nil assigns(:report)
72 assert_equal "162.90", "%.2f" % assigns(:report).total_hours
72 assert_equal "162.90", "%.2f" % assigns(:report).total_hours
73 end
73 end
74
74
75 def test_report_all_time_by_day
75 def test_report_all_time_by_day
76 get :report, :project_id => 1, :criteria => ['project', 'issue'], :columns => 'day'
76 get :report, :project_id => 1, :criteria => ['project', 'issue'], :columns => 'day'
77 assert_response :success
77 assert_response :success
78 assert_template 'report'
78 assert_template 'report'
79 assert_not_nil assigns(:report)
79 assert_not_nil assigns(:report)
80 assert_equal "162.90", "%.2f" % assigns(:report).total_hours
80 assert_equal "162.90", "%.2f" % assigns(:report).total_hours
81 assert_tag :tag => 'th', :content => '2007-03-12'
81 assert_tag :tag => 'th', :content => '2007-03-12'
82 end
82 end
83
83
84 def test_report_one_criteria
84 def test_report_one_criteria
85 get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criteria => ['project']
85 get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criteria => ['project']
86 assert_response :success
86 assert_response :success
87 assert_template 'report'
87 assert_template 'report'
88 assert_not_nil assigns(:report)
88 assert_not_nil assigns(:report)
89 assert_equal "8.65", "%.2f" % assigns(:report).total_hours
89 assert_equal "8.65", "%.2f" % assigns(:report).total_hours
90 end
90 end
91
91
92 def test_report_two_criteria
92 def test_report_two_criteria
93 get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criteria => ["member", "activity"]
93 get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criteria => ["member", "activity"]
94 assert_response :success
94 assert_response :success
95 assert_template 'report'
95 assert_template 'report'
96 assert_not_nil assigns(:report)
96 assert_not_nil assigns(:report)
97 assert_equal "162.90", "%.2f" % assigns(:report).total_hours
97 assert_equal "162.90", "%.2f" % assigns(:report).total_hours
98 end
98 end
99
99
100 def test_report_custom_field_criteria_with_multiple_values
100 def test_report_custom_field_criteria_with_multiple_values
101 field = TimeEntryCustomField.create!(:name => 'multi', :field_format => 'list', :possible_values => ['value1', 'value2'])
101 field = TimeEntryCustomField.create!(:name => 'multi', :field_format => 'list', :possible_values => ['value1', 'value2'])
102 entry = TimeEntry.create!(:project => Project.find(1), :hours => 1, :activity_id => 10, :user => User.find(2), :spent_on => Date.today)
102 entry = TimeEntry.create!(:project => Project.find(1), :hours => 1, :activity_id => 10, :user => User.find(2), :spent_on => Date.today)
103 CustomValue.create!(:customized => entry, :custom_field => field, :value => 'value1')
103 CustomValue.create!(:customized => entry, :custom_field => field, :value => 'value1')
104 CustomValue.create!(:customized => entry, :custom_field => field, :value => 'value2')
104 CustomValue.create!(:customized => entry, :custom_field => field, :value => 'value2')
105
105
106 get :report, :project_id => 1, :columns => 'day', :criteria => ["cf_#{field.id}"]
106 get :report, :project_id => 1, :columns => 'day', :criteria => ["cf_#{field.id}"]
107 assert_response :success
107 assert_response :success
108 end
108 end
109
109
110 def test_report_one_day
110 def test_report_one_day
111 get :report, :project_id => 1, :columns => 'day', :from => "2007-03-23", :to => "2007-03-23", :criteria => ["member", "activity"]
111 get :report, :project_id => 1, :columns => 'day', :from => "2007-03-23", :to => "2007-03-23", :criteria => ["member", "activity"]
112 assert_response :success
112 assert_response :success
113 assert_template 'report'
113 assert_template 'report'
114 assert_not_nil assigns(:report)
114 assert_not_nil assigns(:report)
115 assert_equal "4.25", "%.2f" % assigns(:report).total_hours
115 assert_equal "4.25", "%.2f" % assigns(:report).total_hours
116 end
116 end
117
117
118 def test_report_at_issue_level
118 def test_report_at_issue_level
119 get :report, :project_id => 1, :issue_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criteria => ["member", "activity"]
119 get :report, :project_id => 1, :issue_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criteria => ["member", "activity"]
120 assert_response :success
120 assert_response :success
121 assert_template 'report'
121 assert_template 'report'
122 assert_not_nil assigns(:report)
122 assert_not_nil assigns(:report)
123 assert_equal "154.25", "%.2f" % assigns(:report).total_hours
123 assert_equal "154.25", "%.2f" % assigns(:report).total_hours
124 assert_tag :form,
124 assert_tag :form,
125 :attributes => {:action => "/projects/ecookbook/issues/1/time_entries/report", :id => 'query_form'}
125 :attributes => {:action => "/projects/ecookbook/issues/1/time_entries/report", :id => 'query_form'}
126 end
126 end
127
127
128 def test_report_custom_field_criteria
128 def test_report_custom_field_criteria
129 get :report, :project_id => 1, :criteria => ['project', 'cf_1', 'cf_7']
129 get :report, :project_id => 1, :criteria => ['project', 'cf_1', 'cf_7']
130 assert_response :success
130 assert_response :success
131 assert_template 'report'
131 assert_template 'report'
132 assert_not_nil assigns(:report)
132 assert_not_nil assigns(:report)
133 assert_equal 3, assigns(:report).criteria.size
133 assert_equal 3, assigns(:report).criteria.size
134 assert_equal "162.90", "%.2f" % assigns(:report).total_hours
134 assert_equal "162.90", "%.2f" % assigns(:report).total_hours
135 # Custom field column
135 # Custom field column
136 assert_tag :tag => 'th', :content => 'Database'
136 assert_tag :tag => 'th', :content => 'Database'
137 # Custom field row
137 # Custom field row
138 assert_tag :tag => 'td', :content => 'MySQL',
138 assert_tag :tag => 'td', :content => 'MySQL',
139 :sibling => { :tag => 'td', :attributes => { :class => 'hours' },
139 :sibling => { :tag => 'td', :attributes => { :class => 'hours' },
140 :child => { :tag => 'span', :attributes => { :class => 'hours hours-int' },
140 :child => { :tag => 'span', :attributes => { :class => 'hours hours-int' },
141 :content => '1' }}
141 :content => '1' }}
142 # Second custom field column
142 # Second custom field column
143 assert_tag :tag => 'th', :content => 'Billable'
143 assert_tag :tag => 'th', :content => 'Billable'
144 end
144 end
145
145
146 def test_report_one_criteria_no_result
146 def test_report_one_criteria_no_result
147 get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criteria => ['project']
147 get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criteria => ['project']
148 assert_response :success
148 assert_response :success
149 assert_template 'report'
149 assert_template 'report'
150 assert_not_nil assigns(:report)
150 assert_not_nil assigns(:report)
151 assert_equal "0.00", "%.2f" % assigns(:report).total_hours
151 assert_equal "0.00", "%.2f" % assigns(:report).total_hours
152 end
152 end
153
153
154 def test_report_status_criterion
154 def test_report_status_criterion
155 get :report, :project_id => 1, :criteria => ['status']
155 get :report, :project_id => 1, :criteria => ['status']
156 assert_response :success
156 assert_response :success
157 assert_template 'report'
157 assert_template 'report'
158 assert_tag :tag => 'th', :content => 'Status'
158 assert_tag :tag => 'th', :content => 'Status'
159 assert_tag :tag => 'td', :content => 'New'
159 assert_tag :tag => 'td', :content => 'New'
160 end
160 end
161
161
162 def test_report_all_projects_csv_export
162 def test_report_all_projects_csv_export
163 get :report, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30",
163 get :report, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30",
164 :criteria => ["project", "member", "activity"], :format => "csv"
164 :criteria => ["project", "member", "activity"], :format => "csv"
165 assert_response :success
165 assert_response :success
166 assert_equal 'text/csv; header=present', @response.content_type
166 assert_equal 'text/csv; header=present', @response.content_type
167 lines = @response.body.chomp.split("\n")
167 lines = @response.body.chomp.split("\n")
168 # Headers
168 # Headers
169 assert_equal 'Project,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total',
169 assert_equal 'Project,Member,Activity,2007-3,2007-4,Total', lines.first
170 lines.first
171 # Total row
170 # Total row
172 assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last
171 assert_equal 'Total,"","",154.25,8.65,162.90', lines.last
173 end
172 end
174
173
175 def test_report_csv_export
174 def test_report_csv_export
176 get :report, :project_id => 1, :columns => 'month',
175 get :report, :project_id => 1, :columns => 'month',
177 :from => "2007-01-01", :to => "2007-06-30",
176 :from => "2007-01-01", :to => "2007-06-30",
178 :criteria => ["project", "member", "activity"], :format => "csv"
177 :criteria => ["project", "member", "activity"], :format => "csv"
179 assert_response :success
178 assert_response :success
180 assert_equal 'text/csv; header=present', @response.content_type
179 assert_equal 'text/csv; header=present', @response.content_type
181 lines = @response.body.chomp.split("\n")
180 lines = @response.body.chomp.split("\n")
182 # Headers
181 # Headers
183 assert_equal 'Project,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total',
182 assert_equal 'Project,Member,Activity,2007-3,2007-4,Total', lines.first
184 lines.first
185 # Total row
183 # Total row
186 assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last
184 assert_equal 'Total,"","",154.25,8.65,162.90', lines.last
187 end
185 end
188
186
189 def test_csv_big_5
187 def test_csv_big_5
190 Setting.default_language = "zh-TW"
188 Setting.default_language = "zh-TW"
191 str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88"
189 str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88"
192 str_big5 = "\xa4@\xa4\xeb"
190 str_big5 = "\xa4@\xa4\xeb"
193 if str_utf8.respond_to?(:force_encoding)
191 if str_utf8.respond_to?(:force_encoding)
194 str_utf8.force_encoding('UTF-8')
192 str_utf8.force_encoding('UTF-8')
195 str_big5.force_encoding('Big5')
193 str_big5.force_encoding('Big5')
196 end
194 end
197 user = User.find_by_id(3)
195 user = User.find_by_id(3)
198 user.firstname = str_utf8
196 user.firstname = str_utf8
199 user.lastname = "test-lastname"
197 user.lastname = "test-lastname"
200 assert user.save
198 assert user.save
201 comments = "test_csv_big_5"
199 comments = "test_csv_big_5"
202 te1 = TimeEntry.create(:spent_on => '2011-11-11',
200 te1 = TimeEntry.create(:spent_on => '2011-11-11',
203 :hours => 7.3,
201 :hours => 7.3,
204 :project => Project.find(1),
202 :project => Project.find(1),
205 :user => user,
203 :user => user,
206 :activity => TimeEntryActivity.find_by_name('Design'),
204 :activity => TimeEntryActivity.find_by_name('Design'),
207 :comments => comments)
205 :comments => comments)
208
206
209 te2 = TimeEntry.find_by_comments(comments)
207 te2 = TimeEntry.find_by_comments(comments)
210 assert_not_nil te2
208 assert_not_nil te2
211 assert_equal 7.3, te2.hours
209 assert_equal 7.3, te2.hours
212 assert_equal 3, te2.user_id
210 assert_equal 3, te2.user_id
213
211
214 get :report, :project_id => 1, :columns => 'day',
212 get :report, :project_id => 1, :columns => 'day',
215 :from => "2011-11-11", :to => "2011-11-11",
213 :from => "2011-11-11", :to => "2011-11-11",
216 :criteria => ["member"], :format => "csv"
214 :criteria => ["member"], :format => "csv"
217 assert_response :success
215 assert_response :success
218 assert_equal 'text/csv; header=present', @response.content_type
216 assert_equal 'text/csv; header=present', @response.content_type
219 lines = @response.body.chomp.split("\n")
217 lines = @response.body.chomp.split("\n")
220 # Headers
218 # Headers
221 s1 = "\xa6\xa8\xad\xfb,2011-11-11,\xc1`\xadp"
219 s1 = "\xa6\xa8\xad\xfb,2011-11-11,\xc1`\xadp"
222 s2 = "\xc1`\xadp"
220 s2 = "\xc1`\xadp"
223 if s1.respond_to?(:force_encoding)
221 if s1.respond_to?(:force_encoding)
224 s1.force_encoding('Big5')
222 s1.force_encoding('Big5')
225 s2.force_encoding('Big5')
223 s2.force_encoding('Big5')
226 end
224 end
227 assert_equal s1, lines.first
225 assert_equal s1, lines.first
228 # Total row
226 # Total row
229 assert_equal "#{str_big5} #{user.lastname},7.30,7.30", lines[1]
227 assert_equal "#{str_big5} #{user.lastname},7.30,7.30", lines[1]
230 assert_equal "#{s2},7.30,7.30", lines[2]
228 assert_equal "#{s2},7.30,7.30", lines[2]
231
229
232 str_tw = "Traditional Chinese (\xe7\xb9\x81\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87)"
230 str_tw = "Traditional Chinese (\xe7\xb9\x81\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87)"
233 if str_tw.respond_to?(:force_encoding)
231 if str_tw.respond_to?(:force_encoding)
234 str_tw.force_encoding('UTF-8')
232 str_tw.force_encoding('UTF-8')
235 end
233 end
236 assert_equal str_tw, l(:general_lang_name)
234 assert_equal str_tw, l(:general_lang_name)
237 assert_equal 'Big5', l(:general_csv_encoding)
235 assert_equal 'Big5', l(:general_csv_encoding)
238 assert_equal ',', l(:general_csv_separator)
236 assert_equal ',', l(:general_csv_separator)
239 assert_equal '.', l(:general_csv_decimal_separator)
237 assert_equal '.', l(:general_csv_decimal_separator)
240 end
238 end
241
239
242 def test_csv_cannot_convert_should_be_replaced_big_5
240 def test_csv_cannot_convert_should_be_replaced_big_5
243 Setting.default_language = "zh-TW"
241 Setting.default_language = "zh-TW"
244 str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85"
242 str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85"
245 if str_utf8.respond_to?(:force_encoding)
243 if str_utf8.respond_to?(:force_encoding)
246 str_utf8.force_encoding('UTF-8')
244 str_utf8.force_encoding('UTF-8')
247 end
245 end
248 user = User.find_by_id(3)
246 user = User.find_by_id(3)
249 user.firstname = str_utf8
247 user.firstname = str_utf8
250 user.lastname = "test-lastname"
248 user.lastname = "test-lastname"
251 assert user.save
249 assert user.save
252 comments = "test_replaced"
250 comments = "test_replaced"
253 te1 = TimeEntry.create(:spent_on => '2011-11-11',
251 te1 = TimeEntry.create(:spent_on => '2011-11-11',
254 :hours => 7.3,
252 :hours => 7.3,
255 :project => Project.find(1),
253 :project => Project.find(1),
256 :user => user,
254 :user => user,
257 :activity => TimeEntryActivity.find_by_name('Design'),
255 :activity => TimeEntryActivity.find_by_name('Design'),
258 :comments => comments)
256 :comments => comments)
259
257
260 te2 = TimeEntry.find_by_comments(comments)
258 te2 = TimeEntry.find_by_comments(comments)
261 assert_not_nil te2
259 assert_not_nil te2
262 assert_equal 7.3, te2.hours
260 assert_equal 7.3, te2.hours
263 assert_equal 3, te2.user_id
261 assert_equal 3, te2.user_id
264
262
265 get :report, :project_id => 1, :columns => 'day',
263 get :report, :project_id => 1, :columns => 'day',
266 :from => "2011-11-11", :to => "2011-11-11",
264 :from => "2011-11-11", :to => "2011-11-11",
267 :criteria => ["member"], :format => "csv"
265 :criteria => ["member"], :format => "csv"
268 assert_response :success
266 assert_response :success
269 assert_equal 'text/csv; header=present', @response.content_type
267 assert_equal 'text/csv; header=present', @response.content_type
270 lines = @response.body.chomp.split("\n")
268 lines = @response.body.chomp.split("\n")
271 # Headers
269 # Headers
272 s1 = "\xa6\xa8\xad\xfb,2011-11-11,\xc1`\xadp"
270 s1 = "\xa6\xa8\xad\xfb,2011-11-11,\xc1`\xadp"
273 if s1.respond_to?(:force_encoding)
271 if s1.respond_to?(:force_encoding)
274 s1.force_encoding('Big5')
272 s1.force_encoding('Big5')
275 end
273 end
276 assert_equal s1, lines.first
274 assert_equal s1, lines.first
277 # Total row
275 # Total row
278 s2 = ""
276 s2 = ""
279 if s2.respond_to?(:force_encoding)
277 if s2.respond_to?(:force_encoding)
280 s2 = "\xa5H?"
278 s2 = "\xa5H?"
281 s2.force_encoding('Big5')
279 s2.force_encoding('Big5')
282 elsif RUBY_PLATFORM == 'java'
280 elsif RUBY_PLATFORM == 'java'
283 s2 = "??"
281 s2 = "??"
284 else
282 else
285 s2 = "\xa5H???"
283 s2 = "\xa5H???"
286 end
284 end
287 assert_equal "#{s2} #{user.lastname},7.30,7.30", lines[1]
285 assert_equal "#{s2} #{user.lastname},7.30,7.30", lines[1]
288 end
286 end
289
287
290 def test_csv_fr
288 def test_csv_fr
291 with_settings :default_language => "fr" do
289 with_settings :default_language => "fr" do
292 str1 = "test_csv_fr"
290 str1 = "test_csv_fr"
293 user = User.find_by_id(3)
291 user = User.find_by_id(3)
294 te1 = TimeEntry.create(:spent_on => '2011-11-11',
292 te1 = TimeEntry.create(:spent_on => '2011-11-11',
295 :hours => 7.3,
293 :hours => 7.3,
296 :project => Project.find(1),
294 :project => Project.find(1),
297 :user => user,
295 :user => user,
298 :activity => TimeEntryActivity.find_by_name('Design'),
296 :activity => TimeEntryActivity.find_by_name('Design'),
299 :comments => str1)
297 :comments => str1)
300
298
301 te2 = TimeEntry.find_by_comments(str1)
299 te2 = TimeEntry.find_by_comments(str1)
302 assert_not_nil te2
300 assert_not_nil te2
303 assert_equal 7.3, te2.hours
301 assert_equal 7.3, te2.hours
304 assert_equal 3, te2.user_id
302 assert_equal 3, te2.user_id
305
303
306 get :report, :project_id => 1, :columns => 'day',
304 get :report, :project_id => 1, :columns => 'day',
307 :from => "2011-11-11", :to => "2011-11-11",
305 :from => "2011-11-11", :to => "2011-11-11",
308 :criteria => ["member"], :format => "csv"
306 :criteria => ["member"], :format => "csv"
309 assert_response :success
307 assert_response :success
310 assert_equal 'text/csv; header=present', @response.content_type
308 assert_equal 'text/csv; header=present', @response.content_type
311 lines = @response.body.chomp.split("\n")
309 lines = @response.body.chomp.split("\n")
312 # Headers
310 # Headers
313 s1 = "Membre;2011-11-11;Total"
311 s1 = "Membre;2011-11-11;Total"
314 s2 = "Total"
312 s2 = "Total"
315 if s1.respond_to?(:force_encoding)
313 if s1.respond_to?(:force_encoding)
316 s1.force_encoding('ISO-8859-1')
314 s1.force_encoding('ISO-8859-1')
317 s2.force_encoding('ISO-8859-1')
315 s2.force_encoding('ISO-8859-1')
318 end
316 end
319 assert_equal s1, lines.first
317 assert_equal s1, lines.first
320 # Total row
318 # Total row
321 assert_equal "#{user.firstname} #{user.lastname};7,30;7,30", lines[1]
319 assert_equal "#{user.firstname} #{user.lastname};7,30;7,30", lines[1]
322 assert_equal "#{s2};7,30;7,30", lines[2]
320 assert_equal "#{s2};7,30;7,30", lines[2]
323
321
324 str_fr = "Fran\xc3\xa7ais"
322 str_fr = "Fran\xc3\xa7ais"
325 if str_fr.respond_to?(:force_encoding)
323 if str_fr.respond_to?(:force_encoding)
326 str_fr.force_encoding('UTF-8')
324 str_fr.force_encoding('UTF-8')
327 end
325 end
328 assert_equal str_fr, l(:general_lang_name)
326 assert_equal str_fr, l(:general_lang_name)
329 assert_equal 'ISO-8859-1', l(:general_csv_encoding)
327 assert_equal 'ISO-8859-1', l(:general_csv_encoding)
330 assert_equal ';', l(:general_csv_separator)
328 assert_equal ';', l(:general_csv_separator)
331 assert_equal ',', l(:general_csv_decimal_separator)
329 assert_equal ',', l(:general_csv_decimal_separator)
332 end
330 end
333 end
331 end
334 end
332 end
@@ -1,779 +1,705
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # Redmine - project management software
2 # Redmine - project management software
3 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 # Copyright (C) 2006-2012 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
26
27 include Redmine::I18n
27 include Redmine::I18n
28
28
29 def test_new_with_project_id
29 def test_new_with_project_id
30 @request.session[:user_id] = 3
30 @request.session[:user_id] = 3
31 get :new, :project_id => 1
31 get :new, :project_id => 1
32 assert_response :success
32 assert_response :success
33 assert_template 'new'
33 assert_template 'new'
34 assert_select 'select[name=?]', 'time_entry[project_id]', 0
34 assert_select 'select[name=?]', 'time_entry[project_id]', 0
35 assert_select 'input[name=?][value=1][type=hidden]', 'time_entry[project_id]'
35 assert_select 'input[name=?][value=1][type=hidden]', 'time_entry[project_id]'
36 end
36 end
37
37
38 def test_new_with_issue_id
38 def test_new_with_issue_id
39 @request.session[:user_id] = 3
39 @request.session[:user_id] = 3
40 get :new, :issue_id => 2
40 get :new, :issue_id => 2
41 assert_response :success
41 assert_response :success
42 assert_template 'new'
42 assert_template 'new'
43 assert_select 'select[name=?]', 'time_entry[project_id]', 0
43 assert_select 'select[name=?]', 'time_entry[project_id]', 0
44 assert_select 'input[name=?][value=1][type=hidden]', 'time_entry[project_id]'
44 assert_select 'input[name=?][value=1][type=hidden]', 'time_entry[project_id]'
45 end
45 end
46
46
47 def test_new_without_project
47 def test_new_without_project
48 @request.session[:user_id] = 3
48 @request.session[:user_id] = 3
49 get :new
49 get :new
50 assert_response :success
50 assert_response :success
51 assert_template 'new'
51 assert_template 'new'
52 assert_select 'select[name=?]', 'time_entry[project_id]'
52 assert_select 'select[name=?]', 'time_entry[project_id]'
53 assert_select 'input[name=?]', 'time_entry[project_id]', 0
53 assert_select 'input[name=?]', 'time_entry[project_id]', 0
54 end
54 end
55
55
56 def test_new_without_project_should_prefill_the_form
56 def test_new_without_project_should_prefill_the_form
57 @request.session[:user_id] = 3
57 @request.session[:user_id] = 3
58 get :new, :time_entry => {:project_id => '1'}
58 get :new, :time_entry => {:project_id => '1'}
59 assert_response :success
59 assert_response :success
60 assert_template 'new'
60 assert_template 'new'
61 assert_select 'select[name=?]', 'time_entry[project_id]' do
61 assert_select 'select[name=?]', 'time_entry[project_id]' do
62 assert_select 'option[value=1][selected=selected]'
62 assert_select 'option[value=1][selected=selected]'
63 end
63 end
64 assert_select 'input[name=?]', 'time_entry[project_id]', 0
64 assert_select 'input[name=?]', 'time_entry[project_id]', 0
65 end
65 end
66
66
67 def test_new_without_project_should_deny_without_permission
67 def test_new_without_project_should_deny_without_permission
68 Role.all.each {|role| role.remove_permission! :log_time}
68 Role.all.each {|role| role.remove_permission! :log_time}
69 @request.session[:user_id] = 3
69 @request.session[:user_id] = 3
70
70
71 get :new
71 get :new
72 assert_response 403
72 assert_response 403
73 end
73 end
74
74
75 def test_new_should_select_default_activity
75 def test_new_should_select_default_activity
76 @request.session[:user_id] = 3
76 @request.session[:user_id] = 3
77 get :new, :project_id => 1
77 get :new, :project_id => 1
78 assert_response :success
78 assert_response :success
79 assert_select 'select[name=?]', 'time_entry[activity_id]' do
79 assert_select 'select[name=?]', 'time_entry[activity_id]' do
80 assert_select 'option[selected=selected]', :text => 'Development'
80 assert_select 'option[selected=selected]', :text => 'Development'
81 end
81 end
82 end
82 end
83
83
84 def test_new_should_only_show_active_time_entry_activities
84 def test_new_should_only_show_active_time_entry_activities
85 @request.session[:user_id] = 3
85 @request.session[:user_id] = 3
86 get :new, :project_id => 1
86 get :new, :project_id => 1
87 assert_response :success
87 assert_response :success
88 assert_no_tag 'option', :content => 'Inactive Activity'
88 assert_no_tag 'option', :content => 'Inactive Activity'
89 end
89 end
90
90
91 def test_get_edit_existing_time
91 def test_get_edit_existing_time
92 @request.session[:user_id] = 2
92 @request.session[:user_id] = 2
93 get :edit, :id => 2, :project_id => nil
93 get :edit, :id => 2, :project_id => nil
94 assert_response :success
94 assert_response :success
95 assert_template 'edit'
95 assert_template 'edit'
96 # Default activity selected
96 # Default activity selected
97 assert_tag :tag => 'form', :attributes => { :action => '/projects/ecookbook/time_entries/2' }
97 assert_tag :tag => 'form', :attributes => { :action => '/projects/ecookbook/time_entries/2' }
98 end
98 end
99
99
100 def test_get_edit_with_an_existing_time_entry_with_inactive_activity
100 def test_get_edit_with_an_existing_time_entry_with_inactive_activity
101 te = TimeEntry.find(1)
101 te = TimeEntry.find(1)
102 te.activity = TimeEntryActivity.find_by_name("Inactive Activity")
102 te.activity = TimeEntryActivity.find_by_name("Inactive Activity")
103 te.save!
103 te.save!
104
104
105 @request.session[:user_id] = 1
105 @request.session[:user_id] = 1
106 get :edit, :project_id => 1, :id => 1
106 get :edit, :project_id => 1, :id => 1
107 assert_response :success
107 assert_response :success
108 assert_template 'edit'
108 assert_template 'edit'
109 # Blank option since nothing is pre-selected
109 # Blank option since nothing is pre-selected
110 assert_tag :tag => 'option', :content => '--- Please select ---'
110 assert_tag :tag => 'option', :content => '--- Please select ---'
111 end
111 end
112
112
113 def test_post_create
113 def test_post_create
114 # TODO: should POST to issues’ time log instead of project. change form
114 # TODO: should POST to issues’ time log instead of project. change form
115 # and routing
115 # and routing
116 @request.session[:user_id] = 3
116 @request.session[:user_id] = 3
117 post :create, :project_id => 1,
117 post :create, :project_id => 1,
118 :time_entry => {:comments => 'Some work on TimelogControllerTest',
118 :time_entry => {:comments => 'Some work on TimelogControllerTest',
119 # Not the default activity
119 # Not the default activity
120 :activity_id => '11',
120 :activity_id => '11',
121 :spent_on => '2008-03-14',
121 :spent_on => '2008-03-14',
122 :issue_id => '1',
122 :issue_id => '1',
123 :hours => '7.3'}
123 :hours => '7.3'}
124 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
124 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
125
125
126 i = Issue.find(1)
126 i = Issue.find(1)
127 t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
127 t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
128 assert_not_nil t
128 assert_not_nil t
129 assert_equal 11, t.activity_id
129 assert_equal 11, t.activity_id
130 assert_equal 7.3, t.hours
130 assert_equal 7.3, t.hours
131 assert_equal 3, t.user_id
131 assert_equal 3, t.user_id
132 assert_equal i, t.issue
132 assert_equal i, t.issue
133 assert_equal i.project, t.project
133 assert_equal i.project, t.project
134 end
134 end
135
135
136 def test_post_create_with_blank_issue
136 def test_post_create_with_blank_issue
137 # TODO: should POST to issues’ time log instead of project. change form
137 # TODO: should POST to issues’ time log instead of project. change form
138 # and routing
138 # and routing
139 @request.session[:user_id] = 3
139 @request.session[:user_id] = 3
140 post :create, :project_id => 1,
140 post :create, :project_id => 1,
141 :time_entry => {:comments => 'Some work on TimelogControllerTest',
141 :time_entry => {:comments => 'Some work on TimelogControllerTest',
142 # Not the default activity
142 # Not the default activity
143 :activity_id => '11',
143 :activity_id => '11',
144 :issue_id => '',
144 :issue_id => '',
145 :spent_on => '2008-03-14',
145 :spent_on => '2008-03-14',
146 :hours => '7.3'}
146 :hours => '7.3'}
147 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
147 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
148
148
149 t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
149 t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
150 assert_not_nil t
150 assert_not_nil t
151 assert_equal 11, t.activity_id
151 assert_equal 11, t.activity_id
152 assert_equal 7.3, t.hours
152 assert_equal 7.3, t.hours
153 assert_equal 3, t.user_id
153 assert_equal 3, t.user_id
154 end
154 end
155
155
156 def test_create_and_continue
156 def test_create_and_continue
157 @request.session[:user_id] = 2
157 @request.session[:user_id] = 2
158 post :create, :project_id => 1,
158 post :create, :project_id => 1,
159 :time_entry => {:activity_id => '11',
159 :time_entry => {:activity_id => '11',
160 :issue_id => '',
160 :issue_id => '',
161 :spent_on => '2008-03-14',
161 :spent_on => '2008-03-14',
162 :hours => '7.3'},
162 :hours => '7.3'},
163 :continue => '1'
163 :continue => '1'
164 assert_redirected_to '/projects/ecookbook/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D='
164 assert_redirected_to '/projects/ecookbook/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D='
165 end
165 end
166
166
167 def test_create_and_continue_with_issue_id
167 def test_create_and_continue_with_issue_id
168 @request.session[:user_id] = 2
168 @request.session[:user_id] = 2
169 post :create, :project_id => 1,
169 post :create, :project_id => 1,
170 :time_entry => {:activity_id => '11',
170 :time_entry => {:activity_id => '11',
171 :issue_id => '1',
171 :issue_id => '1',
172 :spent_on => '2008-03-14',
172 :spent_on => '2008-03-14',
173 :hours => '7.3'},
173 :hours => '7.3'},
174 :continue => '1'
174 :continue => '1'
175 assert_redirected_to '/projects/ecookbook/issues/1/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=1'
175 assert_redirected_to '/projects/ecookbook/issues/1/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=1'
176 end
176 end
177
177
178 def test_create_and_continue_without_project
178 def test_create_and_continue_without_project
179 @request.session[:user_id] = 2
179 @request.session[:user_id] = 2
180 post :create, :time_entry => {:project_id => '1',
180 post :create, :time_entry => {:project_id => '1',
181 :activity_id => '11',
181 :activity_id => '11',
182 :issue_id => '',
182 :issue_id => '',
183 :spent_on => '2008-03-14',
183 :spent_on => '2008-03-14',
184 :hours => '7.3'},
184 :hours => '7.3'},
185 :continue => '1'
185 :continue => '1'
186
186
187 assert_redirected_to '/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=&time_entry%5Bproject_id%5D=1'
187 assert_redirected_to '/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=&time_entry%5Bproject_id%5D=1'
188 end
188 end
189
189
190 def test_create_without_log_time_permission_should_be_denied
190 def test_create_without_log_time_permission_should_be_denied
191 @request.session[:user_id] = 2
191 @request.session[:user_id] = 2
192 Role.find_by_name('Manager').remove_permission! :log_time
192 Role.find_by_name('Manager').remove_permission! :log_time
193 post :create, :project_id => 1,
193 post :create, :project_id => 1,
194 :time_entry => {:activity_id => '11',
194 :time_entry => {:activity_id => '11',
195 :issue_id => '',
195 :issue_id => '',
196 :spent_on => '2008-03-14',
196 :spent_on => '2008-03-14',
197 :hours => '7.3'}
197 :hours => '7.3'}
198
198
199 assert_response 403
199 assert_response 403
200 end
200 end
201
201
202 def test_create_with_failure
202 def test_create_with_failure
203 @request.session[:user_id] = 2
203 @request.session[:user_id] = 2
204 post :create, :project_id => 1,
204 post :create, :project_id => 1,
205 :time_entry => {:activity_id => '',
205 :time_entry => {:activity_id => '',
206 :issue_id => '',
206 :issue_id => '',
207 :spent_on => '2008-03-14',
207 :spent_on => '2008-03-14',
208 :hours => '7.3'}
208 :hours => '7.3'}
209
209
210 assert_response :success
210 assert_response :success
211 assert_template 'new'
211 assert_template 'new'
212 end
212 end
213
213
214 def test_create_without_project
214 def test_create_without_project
215 @request.session[:user_id] = 2
215 @request.session[:user_id] = 2
216 assert_difference 'TimeEntry.count' do
216 assert_difference 'TimeEntry.count' do
217 post :create, :time_entry => {:project_id => '1',
217 post :create, :time_entry => {:project_id => '1',
218 :activity_id => '11',
218 :activity_id => '11',
219 :issue_id => '',
219 :issue_id => '',
220 :spent_on => '2008-03-14',
220 :spent_on => '2008-03-14',
221 :hours => '7.3'}
221 :hours => '7.3'}
222 end
222 end
223
223
224 assert_redirected_to '/projects/ecookbook/time_entries'
224 assert_redirected_to '/projects/ecookbook/time_entries'
225 time_entry = TimeEntry.first(:order => 'id DESC')
225 time_entry = TimeEntry.first(:order => 'id DESC')
226 assert_equal 1, time_entry.project_id
226 assert_equal 1, time_entry.project_id
227 end
227 end
228
228
229 def test_create_without_project_should_fail_with_issue_not_inside_project
229 def test_create_without_project_should_fail_with_issue_not_inside_project
230 @request.session[:user_id] = 2
230 @request.session[:user_id] = 2
231 assert_no_difference 'TimeEntry.count' do
231 assert_no_difference 'TimeEntry.count' do
232 post :create, :time_entry => {:project_id => '1',
232 post :create, :time_entry => {:project_id => '1',
233 :activity_id => '11',
233 :activity_id => '11',
234 :issue_id => '5',
234 :issue_id => '5',
235 :spent_on => '2008-03-14',
235 :spent_on => '2008-03-14',
236 :hours => '7.3'}
236 :hours => '7.3'}
237 end
237 end
238
238
239 assert_response :success
239 assert_response :success
240 assert assigns(:time_entry).errors[:issue_id].present?
240 assert assigns(:time_entry).errors[:issue_id].present?
241 end
241 end
242
242
243 def test_create_without_project_should_deny_without_permission
243 def test_create_without_project_should_deny_without_permission
244 @request.session[:user_id] = 2
244 @request.session[:user_id] = 2
245 Project.find(3).disable_module!(:time_tracking)
245 Project.find(3).disable_module!(:time_tracking)
246
246
247 assert_no_difference 'TimeEntry.count' do
247 assert_no_difference 'TimeEntry.count' do
248 post :create, :time_entry => {:project_id => '3',
248 post :create, :time_entry => {:project_id => '3',
249 :activity_id => '11',
249 :activity_id => '11',
250 :issue_id => '',
250 :issue_id => '',
251 :spent_on => '2008-03-14',
251 :spent_on => '2008-03-14',
252 :hours => '7.3'}
252 :hours => '7.3'}
253 end
253 end
254
254
255 assert_response 403
255 assert_response 403
256 end
256 end
257
257
258 def test_create_without_project_with_failure
258 def test_create_without_project_with_failure
259 @request.session[:user_id] = 2
259 @request.session[:user_id] = 2
260 assert_no_difference 'TimeEntry.count' do
260 assert_no_difference 'TimeEntry.count' do
261 post :create, :time_entry => {:project_id => '1',
261 post :create, :time_entry => {:project_id => '1',
262 :activity_id => '11',
262 :activity_id => '11',
263 :issue_id => '',
263 :issue_id => '',
264 :spent_on => '2008-03-14',
264 :spent_on => '2008-03-14',
265 :hours => ''}
265 :hours => ''}
266 end
266 end
267
267
268 assert_response :success
268 assert_response :success
269 assert_tag 'select', :attributes => {:name => 'time_entry[project_id]'},
269 assert_tag 'select', :attributes => {:name => 'time_entry[project_id]'},
270 :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}}
270 :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}}
271 end
271 end
272
272
273 def test_update
273 def test_update
274 entry = TimeEntry.find(1)
274 entry = TimeEntry.find(1)
275 assert_equal 1, entry.issue_id
275 assert_equal 1, entry.issue_id
276 assert_equal 2, entry.user_id
276 assert_equal 2, entry.user_id
277
277
278 @request.session[:user_id] = 1
278 @request.session[:user_id] = 1
279 put :update, :id => 1,
279 put :update, :id => 1,
280 :time_entry => {:issue_id => '2',
280 :time_entry => {:issue_id => '2',
281 :hours => '8'}
281 :hours => '8'}
282 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
282 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
283 entry.reload
283 entry.reload
284
284
285 assert_equal 8, entry.hours
285 assert_equal 8, entry.hours
286 assert_equal 2, entry.issue_id
286 assert_equal 2, entry.issue_id
287 assert_equal 2, entry.user_id
287 assert_equal 2, entry.user_id
288 end
288 end
289
289
290 def test_get_bulk_edit
290 def test_get_bulk_edit
291 @request.session[:user_id] = 2
291 @request.session[:user_id] = 2
292 get :bulk_edit, :ids => [1, 2]
292 get :bulk_edit, :ids => [1, 2]
293 assert_response :success
293 assert_response :success
294 assert_template 'bulk_edit'
294 assert_template 'bulk_edit'
295
295
296 # System wide custom field
296 # System wide custom field
297 assert_tag :select, :attributes => {:name => 'time_entry[custom_field_values][10]'}
297 assert_tag :select, :attributes => {:name => 'time_entry[custom_field_values][10]'}
298
298
299 # Activities
299 # Activities
300 assert_select 'select[name=?]', 'time_entry[activity_id]' do
300 assert_select 'select[name=?]', 'time_entry[activity_id]' do
301 assert_select 'option[value=]', :text => '(No change)'
301 assert_select 'option[value=]', :text => '(No change)'
302 assert_select 'option[value=9]', :text => 'Design'
302 assert_select 'option[value=9]', :text => 'Design'
303 end
303 end
304 end
304 end
305
305
306 def test_get_bulk_edit_on_different_projects
306 def test_get_bulk_edit_on_different_projects
307 @request.session[:user_id] = 2
307 @request.session[:user_id] = 2
308 get :bulk_edit, :ids => [1, 2, 6]
308 get :bulk_edit, :ids => [1, 2, 6]
309 assert_response :success
309 assert_response :success
310 assert_template 'bulk_edit'
310 assert_template 'bulk_edit'
311 end
311 end
312
312
313 def test_bulk_update
313 def test_bulk_update
314 @request.session[:user_id] = 2
314 @request.session[:user_id] = 2
315 # update time entry activity
315 # update time entry activity
316 post :bulk_update, :ids => [1, 2], :time_entry => { :activity_id => 9}
316 post :bulk_update, :ids => [1, 2], :time_entry => { :activity_id => 9}
317
317
318 assert_response 302
318 assert_response 302
319 # check that the issues were updated
319 # check that the issues were updated
320 assert_equal [9, 9], TimeEntry.find_all_by_id([1, 2]).collect {|i| i.activity_id}
320 assert_equal [9, 9], TimeEntry.find_all_by_id([1, 2]).collect {|i| i.activity_id}
321 end
321 end
322
322
323 def test_bulk_update_with_failure
323 def test_bulk_update_with_failure
324 @request.session[:user_id] = 2
324 @request.session[:user_id] = 2
325 post :bulk_update, :ids => [1, 2], :time_entry => { :hours => 'A'}
325 post :bulk_update, :ids => [1, 2], :time_entry => { :hours => 'A'}
326
326
327 assert_response 302
327 assert_response 302
328 assert_match /Failed to save 2 time entrie/, flash[:error]
328 assert_match /Failed to save 2 time entrie/, flash[:error]
329 end
329 end
330
330
331 def test_bulk_update_on_different_projects
331 def test_bulk_update_on_different_projects
332 @request.session[:user_id] = 2
332 @request.session[:user_id] = 2
333 # makes user a manager on the other project
333 # makes user a manager on the other project
334 Member.create!(:user_id => 2, :project_id => 3, :role_ids => [1])
334 Member.create!(:user_id => 2, :project_id => 3, :role_ids => [1])
335
335
336 # update time entry activity
336 # update time entry activity
337 post :bulk_update, :ids => [1, 2, 4], :time_entry => { :activity_id => 9 }
337 post :bulk_update, :ids => [1, 2, 4], :time_entry => { :activity_id => 9 }
338
338
339 assert_response 302
339 assert_response 302
340 # check that the issues were updated
340 # check that the issues were updated
341 assert_equal [9, 9, 9], TimeEntry.find_all_by_id([1, 2, 4]).collect {|i| i.activity_id}
341 assert_equal [9, 9, 9], TimeEntry.find_all_by_id([1, 2, 4]).collect {|i| i.activity_id}
342 end
342 end
343
343
344 def test_bulk_update_on_different_projects_without_rights
344 def test_bulk_update_on_different_projects_without_rights
345 @request.session[:user_id] = 3
345 @request.session[:user_id] = 3
346 user = User.find(3)
346 user = User.find(3)
347 action = { :controller => "timelog", :action => "bulk_update" }
347 action = { :controller => "timelog", :action => "bulk_update" }
348 assert user.allowed_to?(action, TimeEntry.find(1).project)
348 assert user.allowed_to?(action, TimeEntry.find(1).project)
349 assert ! user.allowed_to?(action, TimeEntry.find(5).project)
349 assert ! user.allowed_to?(action, TimeEntry.find(5).project)
350 post :bulk_update, :ids => [1, 5], :time_entry => { :activity_id => 9 }
350 post :bulk_update, :ids => [1, 5], :time_entry => { :activity_id => 9 }
351 assert_response 403
351 assert_response 403
352 end
352 end
353
353
354 def test_bulk_update_custom_field
354 def test_bulk_update_custom_field
355 @request.session[:user_id] = 2
355 @request.session[:user_id] = 2
356 post :bulk_update, :ids => [1, 2], :time_entry => { :custom_field_values => {'10' => '0'} }
356 post :bulk_update, :ids => [1, 2], :time_entry => { :custom_field_values => {'10' => '0'} }
357
357
358 assert_response 302
358 assert_response 302
359 assert_equal ["0", "0"], TimeEntry.find_all_by_id([1, 2]).collect {|i| i.custom_value_for(10).value}
359 assert_equal ["0", "0"], TimeEntry.find_all_by_id([1, 2]).collect {|i| i.custom_value_for(10).value}
360 end
360 end
361
361
362 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
362 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
363 @request.session[:user_id] = 2
363 @request.session[:user_id] = 2
364 post :bulk_update, :ids => [1,2], :back_url => '/time_entries'
364 post :bulk_update, :ids => [1,2], :back_url => '/time_entries'
365
365
366 assert_response :redirect
366 assert_response :redirect
367 assert_redirected_to '/time_entries'
367 assert_redirected_to '/time_entries'
368 end
368 end
369
369
370 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
370 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
371 @request.session[:user_id] = 2
371 @request.session[:user_id] = 2
372 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
372 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
373
373
374 assert_response :redirect
374 assert_response :redirect
375 assert_redirected_to :controller => 'timelog', :action => 'index', :project_id => Project.find(1).identifier
375 assert_redirected_to :controller => 'timelog', :action => 'index', :project_id => Project.find(1).identifier
376 end
376 end
377
377
378 def test_post_bulk_update_without_edit_permission_should_be_denied
378 def test_post_bulk_update_without_edit_permission_should_be_denied
379 @request.session[:user_id] = 2
379 @request.session[:user_id] = 2
380 Role.find_by_name('Manager').remove_permission! :edit_time_entries
380 Role.find_by_name('Manager').remove_permission! :edit_time_entries
381 post :bulk_update, :ids => [1,2]
381 post :bulk_update, :ids => [1,2]
382
382
383 assert_response 403
383 assert_response 403
384 end
384 end
385
385
386 def test_destroy
386 def test_destroy
387 @request.session[:user_id] = 2
387 @request.session[:user_id] = 2
388 delete :destroy, :id => 1
388 delete :destroy, :id => 1
389 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
389 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
390 assert_equal I18n.t(:notice_successful_delete), flash[:notice]
390 assert_equal I18n.t(:notice_successful_delete), flash[:notice]
391 assert_nil TimeEntry.find_by_id(1)
391 assert_nil TimeEntry.find_by_id(1)
392 end
392 end
393
393
394 def test_destroy_should_fail
394 def test_destroy_should_fail
395 # simulate that this fails (e.g. due to a plugin), see #5700
395 # simulate that this fails (e.g. due to a plugin), see #5700
396 TimeEntry.any_instance.expects(:destroy).returns(false)
396 TimeEntry.any_instance.expects(:destroy).returns(false)
397
397
398 @request.session[:user_id] = 2
398 @request.session[:user_id] = 2
399 delete :destroy, :id => 1
399 delete :destroy, :id => 1
400 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
400 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
401 assert_equal I18n.t(:notice_unable_delete_time_entry), flash[:error]
401 assert_equal I18n.t(:notice_unable_delete_time_entry), flash[:error]
402 assert_not_nil TimeEntry.find_by_id(1)
402 assert_not_nil TimeEntry.find_by_id(1)
403 end
403 end
404
404
405 def test_index_all_projects
405 def test_index_all_projects
406 get :index
406 get :index
407 assert_response :success
407 assert_response :success
408 assert_template 'index'
408 assert_template 'index'
409 assert_not_nil assigns(:total_hours)
409 assert_not_nil assigns(:total_hours)
410 assert_equal "162.90", "%.2f" % assigns(:total_hours)
410 assert_equal "162.90", "%.2f" % assigns(:total_hours)
411 assert_tag :form,
411 assert_tag :form,
412 :attributes => {:action => "/time_entries", :id => 'query_form'}
412 :attributes => {:action => "/time_entries", :id => 'query_form'}
413 end
413 end
414
414
415 def test_index_all_projects_should_show_log_time_link
415 def test_index_all_projects_should_show_log_time_link
416 @request.session[:user_id] = 2
416 @request.session[:user_id] = 2
417 get :index
417 get :index
418 assert_response :success
418 assert_response :success
419 assert_template 'index'
419 assert_template 'index'
420 assert_tag 'a', :attributes => {:href => '/time_entries/new'}, :content => /Log time/
420 assert_tag 'a', :attributes => {:href => '/time_entries/new'}, :content => /Log time/
421 end
421 end
422
422
423 def test_index_at_project_level
423 def test_index_at_project_level
424 get :index, :project_id => 'ecookbook'
424 get :index, :project_id => 'ecookbook'
425 assert_response :success
425 assert_response :success
426 assert_template 'index'
426 assert_template 'index'
427 assert_not_nil assigns(:entries)
427 assert_not_nil assigns(:entries)
428 assert_equal 4, assigns(:entries).size
428 assert_equal 4, assigns(:entries).size
429 # project and subproject
429 # project and subproject
430 assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
430 assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
431 assert_not_nil assigns(:total_hours)
431 assert_not_nil assigns(:total_hours)
432 assert_equal "162.90", "%.2f" % assigns(:total_hours)
432 assert_equal "162.90", "%.2f" % assigns(:total_hours)
433 # display all time by default
434 assert_nil assigns(:from)
435 assert_nil assigns(:to)
436 assert_tag :form,
433 assert_tag :form,
437 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
434 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
438 end
435 end
439
436
440 def test_index_at_project_level_with_date_range
437 def test_index_at_project_level_with_date_range
441 get :index, :project_id => 'ecookbook', :from => '2007-03-20', :to => '2007-04-30'
438 get :index, :project_id => 'ecookbook',
439 :f => ['spent_on'],
440 :op => {'spent_on' => '><'},
441 :v => {'spent_on' => ['2007-03-20', '2007-04-30']}
442 assert_response :success
442 assert_response :success
443 assert_template 'index'
443 assert_template 'index'
444 assert_not_nil assigns(:entries)
444 assert_not_nil assigns(:entries)
445 assert_equal 3, assigns(:entries).size
445 assert_equal 3, assigns(:entries).size
446 assert_not_nil assigns(:total_hours)
446 assert_not_nil assigns(:total_hours)
447 assert_equal "12.90", "%.2f" % assigns(:total_hours)
447 assert_equal "12.90", "%.2f" % assigns(:total_hours)
448 assert_equal '2007-03-20'.to_date, assigns(:from)
449 assert_equal '2007-04-30'.to_date, assigns(:to)
450 assert_tag :form,
448 assert_tag :form,
451 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
449 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
452 end
450 end
453
451
454 def test_index_at_project_level_with_period
452 def test_index_at_project_level_with_date_range_using_from_and_to_params
455 get :index, :project_id => 'ecookbook', :period => '7_days'
453 get :index, :project_id => 'ecookbook', :from => '2007-03-20', :to => '2007-04-30'
456 assert_response :success
454 assert_response :success
457 assert_template 'index'
455 assert_template 'index'
458 assert_not_nil assigns(:entries)
456 assert_not_nil assigns(:entries)
457 assert_equal 3, assigns(:entries).size
459 assert_not_nil assigns(:total_hours)
458 assert_not_nil assigns(:total_hours)
460 assert_equal Date.today - 7, assigns(:from)
459 assert_equal "12.90", "%.2f" % assigns(:total_hours)
461 assert_equal Date.today, assigns(:to)
462 assert_tag :form,
460 assert_tag :form,
463 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
461 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
464 end
462 end
465
463
466 def test_index_one_day
464 def test_index_at_project_level_with_period
467 get :index, :project_id => 'ecookbook', :from => "2007-03-23", :to => "2007-03-23"
465 get :index, :project_id => 'ecookbook',
466 :f => ['spent_on'],
467 :op => {'spent_on' => '>t-'},
468 :v => {'spent_on' => ['7']}
468 assert_response :success
469 assert_response :success
469 assert_template 'index'
470 assert_template 'index'
471 assert_not_nil assigns(:entries)
470 assert_not_nil assigns(:total_hours)
472 assert_not_nil assigns(:total_hours)
471 assert_equal "4.25", "%.2f" % assigns(:total_hours)
472 assert_tag :form,
473 assert_tag :form,
473 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
474 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
474 end
475 end
475
476
476 def test_index_from_a_date
477 get :index, :project_id => 'ecookbook', :from => "2007-03-23", :to => ""
478 assert_equal '2007-03-23'.to_date, assigns(:from)
479 assert_nil assigns(:to)
480 end
481
482 def test_index_to_a_date
483 get :index, :project_id => 'ecookbook', :from => "", :to => "2007-03-23"
484 assert_nil assigns(:from)
485 assert_equal '2007-03-23'.to_date, assigns(:to)
486 end
487
488 def test_index_today
489 Date.stubs(:today).returns('2011-12-15'.to_date)
490 get :index, :period => 'today'
491 assert_equal '2011-12-15'.to_date, assigns(:from)
492 assert_equal '2011-12-15'.to_date, assigns(:to)
493 end
494
495 def test_index_yesterday
496 Date.stubs(:today).returns('2011-12-15'.to_date)
497 get :index, :period => 'yesterday'
498 assert_equal '2011-12-14'.to_date, assigns(:from)
499 assert_equal '2011-12-14'.to_date, assigns(:to)
500 end
501
502 def test_index_current_week
503 Date.stubs(:today).returns('2011-12-15'.to_date)
504 get :index, :period => 'current_week'
505 assert_equal '2011-12-12'.to_date, assigns(:from)
506 assert_equal '2011-12-18'.to_date, assigns(:to)
507 end
508
509 def test_index_last_week
510 Date.stubs(:today).returns('2011-12-15'.to_date)
511 get :index, :period => 'last_week'
512 assert_equal '2011-12-05'.to_date, assigns(:from)
513 assert_equal '2011-12-11'.to_date, assigns(:to)
514 end
515
516 def test_index_last_2_week
517 Date.stubs(:today).returns('2011-12-15'.to_date)
518 get :index, :period => 'last_2_weeks'
519 assert_equal '2011-11-28'.to_date, assigns(:from)
520 assert_equal '2011-12-11'.to_date, assigns(:to)
521 end
522
523 def test_index_7_days
524 Date.stubs(:today).returns('2011-12-15'.to_date)
525 get :index, :period => '7_days'
526 assert_equal '2011-12-08'.to_date, assigns(:from)
527 assert_equal '2011-12-15'.to_date, assigns(:to)
528 end
529
530 def test_index_current_month
531 Date.stubs(:today).returns('2011-12-15'.to_date)
532 get :index, :period => 'current_month'
533 assert_equal '2011-12-01'.to_date, assigns(:from)
534 assert_equal '2011-12-31'.to_date, assigns(:to)
535 end
536
537 def test_index_last_month
538 Date.stubs(:today).returns('2011-12-15'.to_date)
539 get :index, :period => 'last_month'
540 assert_equal '2011-11-01'.to_date, assigns(:from)
541 assert_equal '2011-11-30'.to_date, assigns(:to)
542 end
543
544 def test_index_30_days
545 Date.stubs(:today).returns('2011-12-15'.to_date)
546 get :index, :period => '30_days'
547 assert_equal '2011-11-15'.to_date, assigns(:from)
548 assert_equal '2011-12-15'.to_date, assigns(:to)
549 end
550
551 def test_index_current_year
552 Date.stubs(:today).returns('2011-12-15'.to_date)
553 get :index, :period => 'current_year'
554 assert_equal '2011-01-01'.to_date, assigns(:from)
555 assert_equal '2011-12-31'.to_date, assigns(:to)
556 end
557
558 def test_index_at_issue_level
477 def test_index_at_issue_level
559 get :index, :issue_id => 1
478 get :index, :issue_id => 1
560 assert_response :success
479 assert_response :success
561 assert_template 'index'
480 assert_template 'index'
562 assert_not_nil assigns(:entries)
481 assert_not_nil assigns(:entries)
563 assert_equal 2, assigns(:entries).size
482 assert_equal 2, assigns(:entries).size
564 assert_not_nil assigns(:total_hours)
483 assert_not_nil assigns(:total_hours)
565 assert_equal 154.25, assigns(:total_hours)
484 assert_equal 154.25, assigns(:total_hours)
566 # display all time
485 # display all time
567 assert_nil assigns(:from)
486 assert_nil assigns(:from)
568 assert_nil assigns(:to)
487 assert_nil assigns(:to)
569 # TODO: remove /projects/:project_id/issues/:issue_id/time_entries routes
488 # TODO: remove /projects/:project_id/issues/:issue_id/time_entries routes
570 # to use /issues/:issue_id/time_entries
489 # to use /issues/:issue_id/time_entries
571 assert_tag :form,
490 assert_tag :form,
572 :attributes => {:action => "/projects/ecookbook/issues/1/time_entries", :id => 'query_form'}
491 :attributes => {:action => "/projects/ecookbook/issues/1/time_entries", :id => 'query_form'}
573 end
492 end
574
493
575 def test_index_should_sort_by_spent_on_and_created_on
494 def test_index_should_sort_by_spent_on_and_created_on
576 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)
495 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)
577 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)
496 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)
578 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)
497 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)
579
498
580 get :index, :project_id => 1, :from => '2012-06-15', :to => '2012-06-16'
499 get :index, :project_id => 1,
500 :f => ['spent_on'],
501 :op => {'spent_on' => '><'},
502 :v => {'spent_on' => ['2012-06-15', '2012-06-16']}
581 assert_response :success
503 assert_response :success
582 assert_equal [t2, t1, t3], assigns(:entries)
504 assert_equal [t2, t1, t3], assigns(:entries)
583
505
584 get :index, :project_id => 1, :from => '2012-06-15', :to => '2012-06-16', :sort => 'spent_on'
506 get :index, :project_id => 1,
507 :f => ['spent_on'],
508 :op => {'spent_on' => '><'},
509 :v => {'spent_on' => ['2012-06-15', '2012-06-16']},
510 :sort => 'spent_on'
585 assert_response :success
511 assert_response :success
586 assert_equal [t3, t1, t2], assigns(:entries)
512 assert_equal [t3, t1, t2], assigns(:entries)
587 end
513 end
588
514
589 def test_index_atom_feed
515 def test_index_atom_feed
590 get :index, :project_id => 1, :format => 'atom'
516 get :index, :project_id => 1, :format => 'atom'
591 assert_response :success
517 assert_response :success
592 assert_equal 'application/atom+xml', @response.content_type
518 assert_equal 'application/atom+xml', @response.content_type
593 assert_not_nil assigns(:items)
519 assert_not_nil assigns(:items)
594 assert assigns(:items).first.is_a?(TimeEntry)
520 assert assigns(:items).first.is_a?(TimeEntry)
595 end
521 end
596
522
597 def test_index_all_projects_csv_export
523 def test_index_all_projects_csv_export
598 Setting.date_format = '%m/%d/%Y'
524 Setting.date_format = '%m/%d/%Y'
599 get :index, :format => 'csv'
525 get :index, :format => 'csv'
600 assert_response :success
526 assert_response :success
601 assert_equal 'text/csv; header=present', @response.content_type
527 assert_equal 'text/csv; header=present', @response.content_type
602 assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment,Overtime\n")
528 assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment,Overtime\n")
603 assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\",\"\"\n")
529 assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\",\"\"\n")
604 end
530 end
605
531
606 def test_index_csv_export
532 def test_index_csv_export
607 Setting.date_format = '%m/%d/%Y'
533 Setting.date_format = '%m/%d/%Y'
608 get :index, :project_id => 1, :format => 'csv'
534 get :index, :project_id => 1, :format => 'csv'
609 assert_response :success
535 assert_response :success
610 assert_equal 'text/csv; header=present', @response.content_type
536 assert_equal 'text/csv; header=present', @response.content_type
611 assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment,Overtime\n")
537 assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment,Overtime\n")
612 assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\",\"\"\n")
538 assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\",\"\"\n")
613 end
539 end
614
540
615 def test_index_csv_export_with_multi_custom_field
541 def test_index_csv_export_with_multi_custom_field
616 field = TimeEntryCustomField.create!(:name => 'Test', :field_format => 'list',
542 field = TimeEntryCustomField.create!(:name => 'Test', :field_format => 'list',
617 :multiple => true, :possible_values => ['value1', 'value2'])
543 :multiple => true, :possible_values => ['value1', 'value2'])
618 entry = TimeEntry.find(1)
544 entry = TimeEntry.find(1)
619 entry.custom_field_values = {field.id => ['value1', 'value2']}
545 entry.custom_field_values = {field.id => ['value1', 'value2']}
620 entry.save!
546 entry.save!
621
547
622 get :index, :project_id => 1, :format => 'csv'
548 get :index, :project_id => 1, :format => 'csv'
623 assert_response :success
549 assert_response :success
624 assert_include '"value1, value2"', @response.body
550 assert_include '"value1, value2"', @response.body
625 end
551 end
626
552
627 def test_csv_big_5
553 def test_csv_big_5
628 user = User.find_by_id(3)
554 user = User.find_by_id(3)
629 user.language = "zh-TW"
555 user.language = "zh-TW"
630 assert user.save
556 assert user.save
631 str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88"
557 str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88"
632 str_big5 = "\xa4@\xa4\xeb"
558 str_big5 = "\xa4@\xa4\xeb"
633 if str_utf8.respond_to?(:force_encoding)
559 if str_utf8.respond_to?(:force_encoding)
634 str_utf8.force_encoding('UTF-8')
560 str_utf8.force_encoding('UTF-8')
635 str_big5.force_encoding('Big5')
561 str_big5.force_encoding('Big5')
636 end
562 end
637 @request.session[:user_id] = 3
563 @request.session[:user_id] = 3
638 post :create, :project_id => 1,
564 post :create, :project_id => 1,
639 :time_entry => {:comments => str_utf8,
565 :time_entry => {:comments => str_utf8,
640 # Not the default activity
566 # Not the default activity
641 :activity_id => '11',
567 :activity_id => '11',
642 :issue_id => '',
568 :issue_id => '',
643 :spent_on => '2011-11-10',
569 :spent_on => '2011-11-10',
644 :hours => '7.3'}
570 :hours => '7.3'}
645 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
571 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
646
572
647 t = TimeEntry.find_by_comments(str_utf8)
573 t = TimeEntry.find_by_comments(str_utf8)
648 assert_not_nil t
574 assert_not_nil t
649 assert_equal 11, t.activity_id
575 assert_equal 11, t.activity_id
650 assert_equal 7.3, t.hours
576 assert_equal 7.3, t.hours
651 assert_equal 3, t.user_id
577 assert_equal 3, t.user_id
652
578
653 get :index, :project_id => 1, :format => 'csv',
579 get :index, :project_id => 1, :format => 'csv',
654 :from => '2011-11-10', :to => '2011-11-10'
580 :from => '2011-11-10', :to => '2011-11-10'
655 assert_response :success
581 assert_response :success
656 assert_equal 'text/csv; header=present', @response.content_type
582 assert_equal 'text/csv; header=present', @response.content_type
657 ar = @response.body.chomp.split("\n")
583 ar = @response.body.chomp.split("\n")
658 s1 = "\xa4\xe9\xb4\xc1"
584 s1 = "\xa4\xe9\xb4\xc1"
659 if str_utf8.respond_to?(:force_encoding)
585 if str_utf8.respond_to?(:force_encoding)
660 s1.force_encoding('Big5')
586 s1.force_encoding('Big5')
661 end
587 end
662 assert ar[0].include?(s1)
588 assert ar[0].include?(s1)
663 assert ar[1].include?(str_big5)
589 assert ar[1].include?(str_big5)
664 end
590 end
665
591
666 def test_csv_cannot_convert_should_be_replaced_big_5
592 def test_csv_cannot_convert_should_be_replaced_big_5
667 user = User.find_by_id(3)
593 user = User.find_by_id(3)
668 user.language = "zh-TW"
594 user.language = "zh-TW"
669 assert user.save
595 assert user.save
670 str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85"
596 str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85"
671 if str_utf8.respond_to?(:force_encoding)
597 if str_utf8.respond_to?(:force_encoding)
672 str_utf8.force_encoding('UTF-8')
598 str_utf8.force_encoding('UTF-8')
673 end
599 end
674 @request.session[:user_id] = 3
600 @request.session[:user_id] = 3
675 post :create, :project_id => 1,
601 post :create, :project_id => 1,
676 :time_entry => {:comments => str_utf8,
602 :time_entry => {:comments => str_utf8,
677 # Not the default activity
603 # Not the default activity
678 :activity_id => '11',
604 :activity_id => '11',
679 :issue_id => '',
605 :issue_id => '',
680 :spent_on => '2011-11-10',
606 :spent_on => '2011-11-10',
681 :hours => '7.3'}
607 :hours => '7.3'}
682 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
608 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
683
609
684 t = TimeEntry.find_by_comments(str_utf8)
610 t = TimeEntry.find_by_comments(str_utf8)
685 assert_not_nil t
611 assert_not_nil t
686 assert_equal 11, t.activity_id
612 assert_equal 11, t.activity_id
687 assert_equal 7.3, t.hours
613 assert_equal 7.3, t.hours
688 assert_equal 3, t.user_id
614 assert_equal 3, t.user_id
689
615
690 get :index, :project_id => 1, :format => 'csv',
616 get :index, :project_id => 1, :format => 'csv',
691 :from => '2011-11-10', :to => '2011-11-10'
617 :from => '2011-11-10', :to => '2011-11-10'
692 assert_response :success
618 assert_response :success
693 assert_equal 'text/csv; header=present', @response.content_type
619 assert_equal 'text/csv; header=present', @response.content_type
694 ar = @response.body.chomp.split("\n")
620 ar = @response.body.chomp.split("\n")
695 s1 = "\xa4\xe9\xb4\xc1"
621 s1 = "\xa4\xe9\xb4\xc1"
696 if str_utf8.respond_to?(:force_encoding)
622 if str_utf8.respond_to?(:force_encoding)
697 s1.force_encoding('Big5')
623 s1.force_encoding('Big5')
698 end
624 end
699 assert ar[0].include?(s1)
625 assert ar[0].include?(s1)
700 s2 = ar[1].split(",")[8]
626 s2 = ar[1].split(",")[8]
701 if s2.respond_to?(:force_encoding)
627 if s2.respond_to?(:force_encoding)
702 s3 = "\xa5H?"
628 s3 = "\xa5H?"
703 s3.force_encoding('Big5')
629 s3.force_encoding('Big5')
704 assert_equal s3, s2
630 assert_equal s3, s2
705 elsif RUBY_PLATFORM == 'java'
631 elsif RUBY_PLATFORM == 'java'
706 assert_equal "??", s2
632 assert_equal "??", s2
707 else
633 else
708 assert_equal "\xa5H???", s2
634 assert_equal "\xa5H???", s2
709 end
635 end
710 end
636 end
711
637
712 def test_csv_tw
638 def test_csv_tw
713 with_settings :default_language => "zh-TW" do
639 with_settings :default_language => "zh-TW" do
714 str1 = "test_csv_tw"
640 str1 = "test_csv_tw"
715 user = User.find_by_id(3)
641 user = User.find_by_id(3)
716 te1 = TimeEntry.create(:spent_on => '2011-11-10',
642 te1 = TimeEntry.create(:spent_on => '2011-11-10',
717 :hours => 999.9,
643 :hours => 999.9,
718 :project => Project.find(1),
644 :project => Project.find(1),
719 :user => user,
645 :user => user,
720 :activity => TimeEntryActivity.find_by_name('Design'),
646 :activity => TimeEntryActivity.find_by_name('Design'),
721 :comments => str1)
647 :comments => str1)
722 te2 = TimeEntry.find_by_comments(str1)
648 te2 = TimeEntry.find_by_comments(str1)
723 assert_not_nil te2
649 assert_not_nil te2
724 assert_equal 999.9, te2.hours
650 assert_equal 999.9, te2.hours
725 assert_equal 3, te2.user_id
651 assert_equal 3, te2.user_id
726
652
727 get :index, :project_id => 1, :format => 'csv',
653 get :index, :project_id => 1, :format => 'csv',
728 :from => '2011-11-10', :to => '2011-11-10'
654 :from => '2011-11-10', :to => '2011-11-10'
729 assert_response :success
655 assert_response :success
730 assert_equal 'text/csv; header=present', @response.content_type
656 assert_equal 'text/csv; header=present', @response.content_type
731
657
732 ar = @response.body.chomp.split("\n")
658 ar = @response.body.chomp.split("\n")
733 s2 = ar[1].split(",")[7]
659 s2 = ar[1].split(",")[7]
734 assert_equal '999.9', s2
660 assert_equal '999.9', s2
735
661
736 str_tw = "Traditional Chinese (\xe7\xb9\x81\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87)"
662 str_tw = "Traditional Chinese (\xe7\xb9\x81\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87)"
737 if str_tw.respond_to?(:force_encoding)
663 if str_tw.respond_to?(:force_encoding)
738 str_tw.force_encoding('UTF-8')
664 str_tw.force_encoding('UTF-8')
739 end
665 end
740 assert_equal str_tw, l(:general_lang_name)
666 assert_equal str_tw, l(:general_lang_name)
741 assert_equal ',', l(:general_csv_separator)
667 assert_equal ',', l(:general_csv_separator)
742 assert_equal '.', l(:general_csv_decimal_separator)
668 assert_equal '.', l(:general_csv_decimal_separator)
743 end
669 end
744 end
670 end
745
671
746 def test_csv_fr
672 def test_csv_fr
747 with_settings :default_language => "fr" do
673 with_settings :default_language => "fr" do
748 str1 = "test_csv_fr"
674 str1 = "test_csv_fr"
749 user = User.find_by_id(3)
675 user = User.find_by_id(3)
750 te1 = TimeEntry.create(:spent_on => '2011-11-10',
676 te1 = TimeEntry.create(:spent_on => '2011-11-10',
751 :hours => 999.9,
677 :hours => 999.9,
752 :project => Project.find(1),
678 :project => Project.find(1),
753 :user => user,
679 :user => user,
754 :activity => TimeEntryActivity.find_by_name('Design'),
680 :activity => TimeEntryActivity.find_by_name('Design'),
755 :comments => str1)
681 :comments => str1)
756 te2 = TimeEntry.find_by_comments(str1)
682 te2 = TimeEntry.find_by_comments(str1)
757 assert_not_nil te2
683 assert_not_nil te2
758 assert_equal 999.9, te2.hours
684 assert_equal 999.9, te2.hours
759 assert_equal 3, te2.user_id
685 assert_equal 3, te2.user_id
760
686
761 get :index, :project_id => 1, :format => 'csv',
687 get :index, :project_id => 1, :format => 'csv',
762 :from => '2011-11-10', :to => '2011-11-10'
688 :from => '2011-11-10', :to => '2011-11-10'
763 assert_response :success
689 assert_response :success
764 assert_equal 'text/csv; header=present', @response.content_type
690 assert_equal 'text/csv; header=present', @response.content_type
765
691
766 ar = @response.body.chomp.split("\n")
692 ar = @response.body.chomp.split("\n")
767 s2 = ar[1].split(";")[7]
693 s2 = ar[1].split(";")[7]
768 assert_equal '999,9', s2
694 assert_equal '999,9', s2
769
695
770 str_fr = "Fran\xc3\xa7ais"
696 str_fr = "Fran\xc3\xa7ais"
771 if str_fr.respond_to?(:force_encoding)
697 if str_fr.respond_to?(:force_encoding)
772 str_fr.force_encoding('UTF-8')
698 str_fr.force_encoding('UTF-8')
773 end
699 end
774 assert_equal str_fr, l(:general_lang_name)
700 assert_equal str_fr, l(:general_lang_name)
775 assert_equal ';', l(:general_csv_separator)
701 assert_equal ';', l(:general_csv_separator)
776 assert_equal ',', l(:general_csv_decimal_separator)
702 assert_equal ',', l(:general_csv_decimal_separator)
777 end
703 end
778 end
704 end
779 end
705 end
General Comments 0
You need to be logged in to leave comments. Login now