@@ -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), {: |
|
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), |
|
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), |
|
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 |
|
21 | attr_reader :criteria, :columns, :hours, :total_hours, :periods | |
22 |
|
22 | |||
23 |
def initialize(project, issue, criteria, columns, |
|
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"> </span></span>' |
|
146 | ' <span class="toggle-multiselect"> </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- |
|
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,"","", |
|
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- |
|
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,"","", |
|
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', |
|
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_ |
|
452 | def test_index_at_project_level_with_date_range_using_from_and_to_params | |
455 |
get :index, :project_id => 'ecookbook', : |
|
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 |
|
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_ |
|
464 | def test_index_at_project_level_with_period | |
467 |
get :index, :project_id => 'ecookbook', |
|
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, |
|
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