##// END OF EJS Templates
Makes spent time queries savable (#14790)....
Jean-Philippe Lang -
r15257:beb5e6039166
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,137 +1,146
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class QueriesController < ApplicationController
19 19 menu_item :issues
20 20 before_filter :find_query, :except => [:new, :create, :index]
21 21 before_filter :find_optional_project, :only => [:new, :create]
22 22
23 23 accept_api_auth :index
24 24
25 25 include QueriesHelper
26 26
27 27 def index
28 28 case params[:format]
29 29 when 'xml', 'json'
30 30 @offset, @limit = api_offset_and_limit
31 31 else
32 32 @limit = per_page_option
33 33 end
34 34 scope = query_class.visible
35 35 @query_count = scope.count
36 36 @query_pages = Paginator.new @query_count, @limit, params['page']
37 37 @queries = scope.
38 38 order("#{Query.table_name}.name").
39 39 limit(@limit).
40 40 offset(@offset).
41 41 to_a
42 42 respond_to do |format|
43 43 format.html {render_error :status => 406}
44 44 format.api
45 45 end
46 46 end
47 47
48 48 def new
49 49 @query = query_class.new
50 50 @query.user = User.current
51 51 @query.project = @project
52 52 @query.build_from_params(params)
53 53 end
54 54
55 55 def create
56 56 @query = query_class.new
57 57 @query.user = User.current
58 58 @query.project = @project
59 59 update_query_from_params
60 60
61 61 if @query.save
62 62 flash[:notice] = l(:notice_successful_create)
63 redirect_to_issues(:query_id => @query)
63 redirect_to_items(:query_id => @query)
64 64 else
65 65 render :action => 'new', :layout => !request.xhr?
66 66 end
67 67 end
68 68
69 69 def edit
70 70 end
71 71
72 72 def update
73 73 update_query_from_params
74 74
75 75 if @query.save
76 76 flash[:notice] = l(:notice_successful_update)
77 redirect_to_issues(:query_id => @query)
77 redirect_to_items(:query_id => @query)
78 78 else
79 79 render :action => 'edit'
80 80 end
81 81 end
82 82
83 83 def destroy
84 84 @query.destroy
85 redirect_to_issues(:set_filter => 1)
85 redirect_to_items(:set_filter => 1)
86 86 end
87 87
88 88 private
89 89
90 90 def find_query
91 91 @query = Query.find(params[:id])
92 92 @project = @query.project
93 93 render_403 unless @query.editable_by?(User.current)
94 94 rescue ActiveRecord::RecordNotFound
95 95 render_404
96 96 end
97 97
98 98 def find_optional_project
99 99 @project = Project.find(params[:project_id]) if params[:project_id]
100 100 render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true)
101 101 rescue ActiveRecord::RecordNotFound
102 102 render_404
103 103 end
104 104
105 105 def update_query_from_params
106 106 @query.project = params[:query_is_for_all] ? nil : @project
107 107 @query.build_from_params(params)
108 108 @query.column_names = nil if params[:default_columns]
109 109 @query.sort_criteria = params[:query] && params[:query][:sort_criteria]
110 110 @query.name = params[:query] && params[:query][:name]
111 111 if User.current.allowed_to?(:manage_public_queries, @query.project) || User.current.admin?
112 112 @query.visibility = (params[:query] && params[:query][:visibility]) || Query::VISIBILITY_PRIVATE
113 113 @query.role_ids = params[:query] && params[:query][:role_ids]
114 114 else
115 115 @query.visibility = Query::VISIBILITY_PRIVATE
116 116 end
117 117 @query
118 118 end
119 119
120 def redirect_to_issues(options)
120 def redirect_to_items(options)
121 method = "redirect_to_#{@query.class.name.underscore}"
122 send method, options
123 end
124
125 def redirect_to_issue_query(options)
121 126 if params[:gantt]
122 127 if @project
123 128 redirect_to project_gantt_path(@project, options)
124 129 else
125 130 redirect_to issues_gantt_path(options)
126 131 end
127 132 else
128 133 redirect_to _project_issues_path(@project, options)
129 134 end
130 135 end
131 136
137 def redirect_to_time_entry_query(options)
138 redirect_to _time_entries_path(@project, nil, options)
139 end
140
132 141 # Returns the Query subclass, IssueQuery by default
133 142 # for compatibility with previous behaviour
134 143 def query_class
135 144 Query.get_subclass(params[:type] || 'IssueQuery')
136 145 end
137 146 end
@@ -1,274 +1,277
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class TimelogController < ApplicationController
19 19 menu_item :issues
20 20
21 21 before_filter :find_time_entry, :only => [:show, :edit, :update]
22 22 before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
23 23 before_filter :authorize, :only => [:show, :edit, :update, :bulk_edit, :bulk_update, :destroy]
24 24
25 25 before_filter :find_optional_project, :only => [:new, :create, :index, :report]
26 26 before_filter :authorize_global, :only => [:new, :create, :index, :report]
27 27
28 28 accept_rss_auth :index
29 29 accept_api_auth :index, :show, :create, :update, :destroy
30 30
31 31 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
32 32
33 33 helper :sort
34 34 include SortHelper
35 35 helper :issues
36 36 include TimelogHelper
37 37 helper :custom_fields
38 38 include CustomFieldsHelper
39 39 helper :queries
40 40 include QueriesHelper
41 41
42 42 def index
43 @query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
44
43 retrieve_time_entry_query
45 44 sort_init(@query.sort_criteria.empty? ? [['spent_on', 'desc']] : @query.sort_criteria)
46 45 sort_update(@query.sortable_columns)
47 46 scope = time_entry_scope(:order => sort_clause).
48 47 includes(:project, :user, :issue).
49 48 preload(:issue => [:project, :tracker, :status, :assigned_to, :priority])
50 49
51 50 respond_to do |format|
52 51 format.html {
53 52 @entry_count = scope.count
54 53 @entry_pages = Paginator.new @entry_count, per_page_option, params['page']
55 54 @entries = scope.offset(@entry_pages.offset).limit(@entry_pages.per_page).to_a
56 55 @total_hours = scope.sum(:hours).to_f
57 56
58 57 render :layout => !request.xhr?
59 58 }
60 59 format.api {
61 60 @entry_count = scope.count
62 61 @offset, @limit = api_offset_and_limit
63 62 @entries = scope.offset(@offset).limit(@limit).preload(:custom_values => :custom_field).to_a
64 63 }
65 64 format.atom {
66 65 entries = scope.limit(Setting.feeds_limit.to_i).reorder("#{TimeEntry.table_name}.created_on DESC").to_a
67 66 render_feed(entries, :title => l(:label_spent_time))
68 67 }
69 68 format.csv {
70 69 # Export all entries
71 70 @entries = scope.to_a
72 71 send_data(query_to_csv(@entries, @query, params), :type => 'text/csv; header=present', :filename => 'timelog.csv')
73 72 }
74 73 end
75 74 end
76 75
77 76 def report
78 @query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
77 retrieve_time_entry_query
79 78 scope = time_entry_scope
80 79
81 80 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], scope)
82 81
83 82 respond_to do |format|
84 83 format.html { render :layout => !request.xhr? }
85 84 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
86 85 end
87 86 end
88 87
89 88 def show
90 89 respond_to do |format|
91 90 # TODO: Implement html response
92 91 format.html { render :nothing => true, :status => 406 }
93 92 format.api
94 93 end
95 94 end
96 95
97 96 def new
98 97 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
99 98 @time_entry.safe_attributes = params[:time_entry]
100 99 end
101 100
102 101 def create
103 102 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
104 103 @time_entry.safe_attributes = params[:time_entry]
105 104 if @time_entry.project && !User.current.allowed_to?(:log_time, @time_entry.project)
106 105 render_403
107 106 return
108 107 end
109 108
110 109 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
111 110
112 111 if @time_entry.save
113 112 respond_to do |format|
114 113 format.html {
115 114 flash[:notice] = l(:notice_successful_create)
116 115 if params[:continue]
117 116 options = {
118 117 :time_entry => {
119 118 :project_id => params[:time_entry][:project_id],
120 119 :issue_id => @time_entry.issue_id,
121 120 :activity_id => @time_entry.activity_id
122 121 },
123 122 :back_url => params[:back_url]
124 123 }
125 124 if params[:project_id] && @time_entry.project
126 125 redirect_to new_project_time_entry_path(@time_entry.project, options)
127 126 elsif params[:issue_id] && @time_entry.issue
128 127 redirect_to new_issue_time_entry_path(@time_entry.issue, options)
129 128 else
130 129 redirect_to new_time_entry_path(options)
131 130 end
132 131 else
133 132 redirect_back_or_default project_time_entries_path(@time_entry.project)
134 133 end
135 134 }
136 135 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
137 136 end
138 137 else
139 138 respond_to do |format|
140 139 format.html { render :action => 'new' }
141 140 format.api { render_validation_errors(@time_entry) }
142 141 end
143 142 end
144 143 end
145 144
146 145 def edit
147 146 @time_entry.safe_attributes = params[:time_entry]
148 147 end
149 148
150 149 def update
151 150 @time_entry.safe_attributes = params[:time_entry]
152 151
153 152 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
154 153
155 154 if @time_entry.save
156 155 respond_to do |format|
157 156 format.html {
158 157 flash[:notice] = l(:notice_successful_update)
159 158 redirect_back_or_default project_time_entries_path(@time_entry.project)
160 159 }
161 160 format.api { render_api_ok }
162 161 end
163 162 else
164 163 respond_to do |format|
165 164 format.html { render :action => 'edit' }
166 165 format.api { render_validation_errors(@time_entry) }
167 166 end
168 167 end
169 168 end
170 169
171 170 def bulk_edit
172 171 @available_activities = TimeEntryActivity.shared.active
173 172 @custom_fields = TimeEntry.first.available_custom_fields
174 173 end
175 174
176 175 def bulk_update
177 176 attributes = parse_params_for_bulk_update(params[:time_entry])
178 177
179 178 unsaved_time_entry_ids = []
180 179 @time_entries.each do |time_entry|
181 180 time_entry.reload
182 181 time_entry.safe_attributes = attributes
183 182 call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
184 183 unless time_entry.save
185 184 logger.info "time entry could not be updated: #{time_entry.errors.full_messages}" if logger && logger.info?
186 185 # Keep unsaved time_entry ids to display them in flash error
187 186 unsaved_time_entry_ids << time_entry.id
188 187 end
189 188 end
190 189 set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
191 190 redirect_back_or_default project_time_entries_path(@projects.first)
192 191 end
193 192
194 193 def destroy
195 194 destroyed = TimeEntry.transaction do
196 195 @time_entries.each do |t|
197 196 unless t.destroy && t.destroyed?
198 197 raise ActiveRecord::Rollback
199 198 end
200 199 end
201 200 end
202 201
203 202 respond_to do |format|
204 203 format.html {
205 204 if destroyed
206 205 flash[:notice] = l(:notice_successful_delete)
207 206 else
208 207 flash[:error] = l(:notice_unable_delete_time_entry)
209 208 end
210 209 redirect_back_or_default project_time_entries_path(@projects.first)
211 210 }
212 211 format.api {
213 212 if destroyed
214 213 render_api_ok
215 214 else
216 215 render_validation_errors(@time_entries)
217 216 end
218 217 }
219 218 end
220 219 end
221 220
222 221 private
223 222 def find_time_entry
224 223 @time_entry = TimeEntry.find(params[:id])
225 224 unless @time_entry.editable_by?(User.current)
226 225 render_403
227 226 return false
228 227 end
229 228 @project = @time_entry.project
230 229 rescue ActiveRecord::RecordNotFound
231 230 render_404
232 231 end
233 232
234 233 def find_time_entries
235 234 @time_entries = TimeEntry.where(:id => params[:id] || params[:ids]).to_a
236 235 raise ActiveRecord::RecordNotFound if @time_entries.empty?
237 236 raise Unauthorized unless @time_entries.all? {|t| t.editable_by?(User.current)}
238 237 @projects = @time_entries.collect(&:project).compact.uniq
239 238 @project = @projects.first if @projects.size == 1
240 239 rescue ActiveRecord::RecordNotFound
241 240 render_404
242 241 end
243 242
244 243 def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
245 244 if unsaved_time_entry_ids.empty?
246 245 flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
247 246 else
248 247 flash[:error] = l(:notice_failed_to_save_time_entries,
249 248 :count => unsaved_time_entry_ids.size,
250 249 :total => time_entries.size,
251 250 :ids => '#' + unsaved_time_entry_ids.join(', #'))
252 251 end
253 252 end
254 253
255 254 def find_optional_project
256 255 if params[:issue_id].present?
257 256 @issue = Issue.find(params[:issue_id])
258 257 @project = @issue.project
259 258 elsif params[:project_id].present?
260 259 @project = Project.find(params[:project_id])
261 260 end
262 261 rescue ActiveRecord::RecordNotFound
263 262 render_404
264 263 end
265 264
266 265 # Returns the TimeEntry scope for index and report actions
267 266 def time_entry_scope(options={})
268 267 scope = @query.results_scope(options)
269 268 if @issue
270 269 scope = scope.on_issue(@issue)
271 270 end
272 271 scope
273 272 end
273
274 def retrieve_time_entry_query
275 retrieve_query(TimeEntryQuery, false)
276 end
274 277 end
@@ -1,279 +1,280
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module QueriesHelper
21 21 include ApplicationHelper
22 22
23 23 def filters_options_for_select(query)
24 24 ungrouped = []
25 25 grouped = {}
26 26 query.available_filters.map do |field, field_options|
27 27 if [:tree, :relation].include?(field_options[:type])
28 28 group = :label_relations
29 29 elsif field =~ /^(.+)\./
30 30 # association filters
31 31 group = "field_#{$1}"
32 32 elsif %w(member_of_group assigned_to_role).include?(field)
33 33 group = :field_assigned_to
34 34 elsif field_options[:type] == :date_past || field_options[:type] == :date
35 35 group = :label_date
36 36 end
37 37 if group
38 38 (grouped[group] ||= []) << [field_options[:name], field]
39 39 else
40 40 ungrouped << [field_options[:name], field]
41 41 end
42 42 end
43 43 # Don't group dates if there's only one (eg. time entries filters)
44 44 if grouped[:label_date].try(:size) == 1
45 45 ungrouped << grouped.delete(:label_date).first
46 46 end
47 47 s = options_for_select([[]] + ungrouped)
48 48 if grouped.present?
49 49 localized_grouped = grouped.map {|k,v| [l(k), v]}
50 50 s << grouped_options_for_select(localized_grouped)
51 51 end
52 52 s
53 53 end
54 54
55 55 def query_filters_hidden_tags(query)
56 56 tags = ''.html_safe
57 57 query.filters.each do |field, options|
58 58 tags << hidden_field_tag("f[]", field, :id => nil)
59 59 tags << hidden_field_tag("op[#{field}]", options[:operator], :id => nil)
60 60 options[:values].each do |value|
61 61 tags << hidden_field_tag("v[#{field}][]", value, :id => nil)
62 62 end
63 63 end
64 64 tags
65 65 end
66 66
67 67 def query_columns_hidden_tags(query)
68 68 tags = ''.html_safe
69 69 query.columns.each do |column|
70 70 tags << hidden_field_tag("c[]", column.name, :id => nil)
71 71 end
72 72 tags
73 73 end
74 74
75 75 def query_hidden_tags(query)
76 76 query_filters_hidden_tags(query) + query_columns_hidden_tags(query)
77 77 end
78 78
79 79 def available_block_columns_tags(query)
80 80 tags = ''.html_safe
81 81 query.available_block_columns.each do |column|
82 82 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column), :id => nil) + " #{column.caption}", :class => 'inline')
83 83 end
84 84 tags
85 85 end
86 86
87 87 def available_totalable_columns_tags(query)
88 88 tags = ''.html_safe
89 89 query.available_totalable_columns.each do |column|
90 90 tags << content_tag('label', check_box_tag('t[]', column.name.to_s, query.totalable_columns.include?(column), :id => nil) + " #{column.caption}", :class => 'inline')
91 91 end
92 92 tags << hidden_field_tag('t[]', '')
93 93 tags
94 94 end
95 95
96 96 def query_available_inline_columns_options(query)
97 97 (query.available_inline_columns - query.columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
98 98 end
99 99
100 100 def query_selected_inline_columns_options(query)
101 101 (query.inline_columns & query.available_inline_columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
102 102 end
103 103
104 104 def render_query_columns_selection(query, options={})
105 105 tag_name = (options[:name] || 'c') + '[]'
106 106 render :partial => 'queries/columns', :locals => {:query => query, :tag_name => tag_name}
107 107 end
108 108
109 109 def render_query_totals(query)
110 110 return unless query.totalable_columns.present?
111 111 totals = query.totalable_columns.map do |column|
112 112 total_tag(column, query.total_for(column))
113 113 end
114 114 content_tag('p', totals.join(" ").html_safe, :class => "query-totals")
115 115 end
116 116
117 117 def total_tag(column, value)
118 118 label = content_tag('span', "#{column.caption}:")
119 119 value = content_tag('span', format_object(value), :class => 'value')
120 120 content_tag('span', label + " " + value, :class => "total-for-#{column.name.to_s.dasherize}")
121 121 end
122 122
123 123 def column_header(column)
124 124 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
125 125 :default_order => column.default_order) :
126 126 content_tag('th', h(column.caption))
127 127 end
128 128
129 129 def column_content(column, issue)
130 130 value = column.value_object(issue)
131 131 if value.is_a?(Array)
132 132 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
133 133 else
134 134 column_value(column, issue, value)
135 135 end
136 136 end
137 137
138 138 def column_value(column, issue, value)
139 139 case column.name
140 140 when :id
141 141 link_to value, issue_path(issue)
142 142 when :subject
143 143 link_to value, issue_path(issue)
144 144 when :parent
145 145 value ? (value.visible? ? link_to_issue(value, :subject => false) : "##{value.id}") : ''
146 146 when :description
147 147 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
148 148 when :done_ratio
149 149 progress_bar(value)
150 150 when :relations
151 151 content_tag('span',
152 152 value.to_s(issue) {|other| link_to_issue(other, :subject => false, :tracker => false)}.html_safe,
153 153 :class => value.css_classes_for(issue))
154 154 else
155 155 format_object(value)
156 156 end
157 157 end
158 158
159 159 def csv_content(column, issue)
160 160 value = column.value_object(issue)
161 161 if value.is_a?(Array)
162 162 value.collect {|v| csv_value(column, issue, v)}.compact.join(', ')
163 163 else
164 164 csv_value(column, issue, value)
165 165 end
166 166 end
167 167
168 168 def csv_value(column, object, value)
169 169 format_object(value, false) do |value|
170 170 case value.class.name
171 171 when 'Float'
172 172 sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator))
173 173 when 'IssueRelation'
174 174 value.to_s(object)
175 175 when 'Issue'
176 176 if object.is_a?(TimeEntry)
177 177 "#{value.tracker} ##{value.id}: #{value.subject}"
178 178 else
179 179 value.id
180 180 end
181 181 else
182 182 value
183 183 end
184 184 end
185 185 end
186 186
187 187 def query_to_csv(items, query, options={})
188 188 options ||= {}
189 189 columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns)
190 190 query.available_block_columns.each do |column|
191 191 if options[column.name].present?
192 192 columns << column
193 193 end
194 194 end
195 195
196 196 Redmine::Export::CSV.generate do |csv|
197 197 # csv header fields
198 198 csv << columns.map {|c| c.caption.to_s}
199 199 # csv lines
200 200 items.each do |item|
201 201 csv << columns.map {|c| csv_content(c, item)}
202 202 end
203 203 end
204 204 end
205 205
206 206 # Retrieve query from session or build a new query
207 def retrieve_query
208 if !params[:query_id].blank?
207 def retrieve_query(klass=IssueQuery, use_session=true)
208 session_key = klass.name.underscore.to_sym
209
210 if params[:query_id].present?
209 211 cond = "project_id IS NULL"
210 212 cond << " OR project_id = #{@project.id}" if @project
211 @query = IssueQuery.where(cond).find(params[:query_id])
213 @query = klass.where(cond).find(params[:query_id])
212 214 raise ::Unauthorized unless @query.visible?
213 215 @query.project = @project
214 session[:query] = {:id => @query.id, :project_id => @query.project_id}
216 session[session_key] = {:id => @query.id, :project_id => @query.project_id} if use_session
215 217 sort_clear
216 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
218 elsif api_request? || params[:set_filter] || !use_session || session[session_key].nil? || session[session_key][:project_id] != (@project ? @project.id : nil)
217 219 # Give it a name, required to be valid
218 @query = IssueQuery.new(:name => "_")
219 @query.project = @project
220 @query = klass.new(:name => "_", :project => @project)
220 221 @query.build_from_params(params)
221 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names, :totalable_names => @query.totalable_names}
222 session[session_key] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names, :totalable_names => @query.totalable_names} if use_session
222 223 else
223 224 # retrieve from session
224 225 @query = nil
225 @query = IssueQuery.find_by_id(session[:query][:id]) if session[:query][:id]
226 @query ||= IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names], :totalable_names => session[:query][:totalable_names])
226 @query = klass.find_by_id(session[session_key][:id]) if session[session_key][:id]
227 @query ||= klass.new(:name => "_", :filters => session[session_key][:filters], :group_by => session[session_key][:group_by], :column_names => session[session_key][:column_names], :totalable_names => session[session_key][:totalable_names])
227 228 @query.project = @project
228 229 end
229 230 end
230 231
231 232 def retrieve_query_from_session
232 233 if session[:query]
233 234 if session[:query][:id]
234 235 @query = IssueQuery.find_by_id(session[:query][:id])
235 236 return unless @query
236 237 else
237 238 @query = IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names], :totalable_names => session[:query][:totalable_names])
238 239 end
239 240 if session[:query].has_key?(:project_id)
240 241 @query.project_id = session[:query][:project_id]
241 242 else
242 243 @query.project = @project
243 244 end
244 245 @query
245 246 end
246 247 end
247 248
248 249 # Returns the query definition as hidden field tags
249 250 def query_as_hidden_field_tags(query)
250 251 tags = hidden_field_tag("set_filter", "1", :id => nil)
251 252
252 253 if query.filters.present?
253 254 query.filters.each do |field, filter|
254 255 tags << hidden_field_tag("f[]", field, :id => nil)
255 256 tags << hidden_field_tag("op[#{field}]", filter[:operator], :id => nil)
256 257 filter[:values].each do |value|
257 258 tags << hidden_field_tag("v[#{field}][]", value, :id => nil)
258 259 end
259 260 end
260 261 else
261 262 tags << hidden_field_tag("f[]", "", :id => nil)
262 263 end
263 264 if query.column_names.present?
264 265 query.column_names.each do |name|
265 266 tags << hidden_field_tag("c[]", name, :id => nil)
266 267 end
267 268 end
268 269 if query.totalable_names.present?
269 270 query.totalable_names.each do |name|
270 271 tags << hidden_field_tag("t[]", name, :id => nil)
271 272 end
272 273 end
273 274 if query.group_by.present?
274 275 tags << hidden_field_tag("group_by", query.group_by, :id => nil)
275 276 end
276 277
277 278 tags
278 279 end
279 280 end
@@ -1,6 +1,7
1 1 <h2><%= l(:label_query_new) %></h2>
2 2
3 3 <%= form_tag(@project ? project_queries_path(@project) : queries_path, :id => "query-form") do %>
4 <%= hidden_field_tag 'type', @query.class.name %>
4 5 <%= render :partial => 'form', :locals => {:query => @query} %>
5 6 <%= submit_tag l(:button_save) %>
6 7 <% end %>
@@ -1,36 +1,51
1 1 <div id="query_form_with_buttons" class="hide-when-print">
2 <%= hidden_field_tag 'set_filter', '1' %>
2 3 <div id="query_form_content">
3 4 <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
4 5 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
5 6 <div style="<%= @query.new_record? ? "" : "display: none;" %>">
6 7 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
7 8 </div>
8 9 </fieldset>
9 10 <fieldset id="options" class="collapsible collapsed">
10 11 <legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend>
11 12 <div style="display: none;">
12 13 <table>
13 14 <tr>
14 15 <td class="field"><%= l(:field_column_names) %></td>
15 16 <td><%= render_query_columns_selection(@query) %></td>
16 17 </tr>
17 18 </table>
18 19 </div>
19 20 </fieldset>
20 21 </div>
21 22
22 23 <p class="buttons">
23 24 <%= link_to_function l(:button_apply), '$("#query_form").submit()', :class => 'icon icon-checked' %>
24 25 <%= link_to l(:button_clear), {:project_id => @project, :issue_id => @issue}, :class => 'icon icon-reload' %>
26 <% if @query.new_record? %>
27 <% if User.current.allowed_to?(:save_queries, @project, :global => true) %>
28 <%= link_to_function l(:button_save),
29 "$('#query_form').attr('action', '#{ @project ? new_project_query_path(@project) : new_query_path }').submit()",
30 :class => 'icon icon-save' %>
31 <% end %>
32 <% else %>
33 <% if @query.editable_by?(User.current) %>
34 <%= link_to l(:button_edit), edit_query_path(@query), :class => 'icon icon-edit' %>
35 <%= delete_link query_path(@query) %>
36 <% end %>
37 <% end %>
25 38 </p>
39
40 <%= hidden_field_tag 'type', 'TimeEntryQuery' %>
26 41 </div>
27 42
28 43 <div class="tabs hide-when-print">
29 <% query_params = params.slice(:f, :op, :v, :sort) %>
44 <% query_params = params.slice(:f, :op, :v, :sort, :query_id) %>
30 45 <ul>
31 46 <li><%= link_to(l(:label_details), _time_entries_path(@project, @issue, query_params),
32 47 :class => (action_name == 'index' ? 'selected' : nil)) %></li>
33 48 <li><%= link_to(l(:label_report), _report_time_entries_path(@project, @issue, query_params),
34 49 :class => (action_name == 'report' ? 'selected' : nil)) %></li>
35 50 </ul>
36 51 </div>
@@ -1,48 +1,48
1 1 <div class="contextual">
2 2 <%= link_to l(:button_log_time),
3 3 _new_time_entry_path(@project, @issue),
4 4 :class => 'icon icon-time-add' if User.current.allowed_to?(:log_time, @project, :global => true) %>
5 5 </div>
6 6
7 7 <%= render_timelog_breadcrumb %>
8 8
9 <h2><%= l(:label_spent_time) %></h2>
9 <h2><%= @query.new_record? ? l(:label_spent_time) : @query.name %></h2>
10 10
11 11 <%= form_tag(params.slice(:project_id, :issue_id), :method => :get, :id => 'query_form') do %>
12 12 <%= render :partial => 'date_range' %>
13 13 <% end %>
14 14
15 15 <div class="total-hours">
16 16 <p><%= l(:label_total_time) %>: <%= html_hours(l_hours(@total_hours)) %></p>
17 17 </div>
18 18
19 19 <% unless @entries.empty? %>
20 20 <%= render :partial => 'list', :locals => { :entries => @entries }%>
21 21 <span class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></span>
22 22
23 23 <% other_formats_links do |f| %>
24 24 <%= f.link_to_with_query_parameters 'Atom', :key => User.current.rss_key %>
25 25 <%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '330px'); return false;" %>
26 26 <% end %>
27 27
28 28 <div id="csv-export-options" style="display:none;">
29 29 <h3 class="title"><%= l(:label_export_options, :export_format => 'CSV') %></h3>
30 30 <%= form_tag(params.slice(:project_id, :issue_id).merge(:format => 'csv', :page=>nil), :method => :get, :id => 'csv-export-form') do %>
31 31 <%= query_hidden_tags @query %>
32 32 <p>
33 33 <label><%= radio_button_tag 'columns', '', true %> <%= l(:description_selected_columns) %></label><br />
34 34 <label><%= radio_button_tag 'columns', 'all' %> <%= l(:description_all_columns) %></label>
35 35 </p>
36 36 <p class="buttons">
37 37 <%= submit_tag l(:button_export), :name => nil, :onclick => "hideModal(this);" %>
38 38 <%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => 'button' %>
39 39 </p>
40 40 <% end %>
41 41 </div>
42 42 <% end %>
43 43
44 <% html_title l(:label_spent_time), l(:label_details) %>
44 <% html_title(@query.new_record? ? l(:label_spent_time) : @query.name, l(:label_details)) %>
45 45
46 46 <% content_for :header_tags do %>
47 47 <%= auto_discovery_link_tag(:atom, {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :title => l(:label_spent_time)) %>
48 48 <% end %>
@@ -1,74 +1,74
1 1 <div class="contextual">
2 2 <%= link_to l(:button_log_time),
3 3 _new_time_entry_path(@project, @issue),
4 4 :class => 'icon icon-time-add' if User.current.allowed_to?(:log_time, @project, :global => true) %>
5 5 </div>
6 6
7 7 <%= render_timelog_breadcrumb %>
8 8
9 <h2><%= l(:label_spent_time) %></h2>
9 <h2><%= @query.new_record? ? l(:label_spent_time) : @query.name %></h2>
10 10
11 11 <%= form_tag(params.slice(:project_id, :issue_id), :method => :get, :id => 'query_form') do %>
12 12 <% @report.criteria.each do |criterion| %>
13 13 <%= hidden_field_tag 'criteria[]', criterion, :id => nil %>
14 14 <% end %>
15 15 <%= render :partial => 'timelog/date_range' %>
16 16
17 17 <p><label for='columns'><%= l(:label_details) %></label>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'],
18 18 [l(:label_month), 'month'],
19 19 [l(:label_week), 'week'],
20 20 [l(:label_day_plural).titleize, 'day']], @report.columns),
21 21 :onchange => "this.form.submit();" %>
22 22
23 23 <label for='criterias'><%= l(:button_add) %></label>: <%= select_tag('criteria[]', options_for_select([[]] + (@report.available_criteria.keys - @report.criteria).collect{|k| [l_or_humanize(@report.available_criteria[k][:label]), k]}),
24 24 :onchange => "this.form.submit();",
25 25 :style => 'width: 200px',
26 26 :disabled => (@report.criteria.length >= 3),
27 27 :id => "criterias") %>
28 28 <%= link_to l(:button_clear), {:project_id => @project, :issue_id => @issue, :period_type => params[:period_type], :period => params[:period], :from => @from, :to => @to, :columns => @report.columns}, :class => 'icon icon-reload' %></p>
29 29 <% end %>
30 30
31 31 <% unless @report.criteria.empty? %>
32 32 <div class="total-hours">
33 33 <p><%= l(:label_total_time) %>: <%= html_hours(l_hours(@report.total_hours)) %></p>
34 34 </div>
35 35
36 36 <% unless @report.hours.empty? %>
37 37 <div class="autoscroll">
38 38 <table class="list" id="time-report">
39 39 <thead>
40 40 <tr>
41 41 <% @report.criteria.each do |criteria| %>
42 42 <th><%= l_or_humanize(@report.available_criteria[criteria][:label]) %></th>
43 43 <% end %>
44 44 <% columns_width = (40 / (@report.periods.length+1)).to_i %>
45 45 <% @report.periods.each do |period| %>
46 46 <th class="period" style="width:<%= columns_width %>%;"><%= period %></th>
47 47 <% end %>
48 48 <th class="total" style="width:<%= columns_width %>%;"><%= l(:label_total_time) %></th>
49 49 </tr>
50 50 </thead>
51 51 <tbody>
52 52 <%= render :partial => 'report_criteria', :locals => {:criterias => @report.criteria, :hours => @report.hours, :level => 0} %>
53 53 <tr class="total">
54 54 <td><%= l(:label_total_time) %></td>
55 55 <%= ('<td></td>' * (@report.criteria.size - 1)).html_safe %>
56 56 <% total = 0 -%>
57 57 <% @report.periods.each do |period| -%>
58 58 <% sum = sum_hours(select_hours(@report.hours, @report.columns, period.to_s)); total += sum -%>
59 59 <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
60 60 <% end -%>
61 61 <td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td>
62 62 </tr>
63 63 </tbody>
64 64 </table>
65 65 </div>
66 66
67 67 <% other_formats_links do |f| %>
68 68 <%= f.link_to 'CSV', :url => params %>
69 69 <% end %>
70 70 <% end %>
71 71 <% end %>
72 72
73 <% html_title l(:label_spent_time), l(:label_report) %>
73 <% html_title(@query.new_record? ? l(:label_spent_time) : @query.name, l(:label_report)) %>
74 74
@@ -1,827 +1,827
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'uri'
19 19
20 20 module Redmine
21 21 module FieldFormat
22 22 def self.add(name, klass)
23 23 all[name.to_s] = klass.instance
24 24 end
25 25
26 26 def self.delete(name)
27 27 all.delete(name.to_s)
28 28 end
29 29
30 30 def self.all
31 31 @formats ||= Hash.new(Base.instance)
32 32 end
33 33
34 34 def self.available_formats
35 35 all.keys
36 36 end
37 37
38 38 def self.find(name)
39 39 all[name.to_s]
40 40 end
41 41
42 42 # Return an array of custom field formats which can be used in select_tag
43 43 def self.as_select(class_name=nil)
44 44 formats = all.values.select do |format|
45 45 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(class_name)
46 46 end
47 47 formats.map {|format| [::I18n.t(format.label), format.name] }.sort_by(&:first)
48 48 end
49 49
50 50 class Base
51 51 include Singleton
52 52 include Redmine::I18n
53 53 include Redmine::Helpers::URL
54 54 include ERB::Util
55 55
56 56 class_attribute :format_name
57 57 self.format_name = nil
58 58
59 59 # Set this to true if the format supports multiple values
60 60 class_attribute :multiple_supported
61 61 self.multiple_supported = false
62 62
63 63 # Set this to true if the format supports textual search on custom values
64 64 class_attribute :searchable_supported
65 65 self.searchable_supported = false
66 66
67 67 # Set this to true if field values can be summed up
68 68 class_attribute :totalable_supported
69 69 self.totalable_supported = false
70 70
71 71 # Restricts the classes that the custom field can be added to
72 72 # Set to nil for no restrictions
73 73 class_attribute :customized_class_names
74 74 self.customized_class_names = nil
75 75
76 76 # Name of the partial for editing the custom field
77 77 class_attribute :form_partial
78 78 self.form_partial = nil
79 79
80 80 class_attribute :change_as_diff
81 81 self.change_as_diff = false
82 82
83 83 def self.add(name)
84 84 self.format_name = name
85 85 Redmine::FieldFormat.add(name, self)
86 86 end
87 87 private_class_method :add
88 88
89 89 def self.field_attributes(*args)
90 90 CustomField.store_accessor :format_store, *args
91 91 end
92 92
93 93 field_attributes :url_pattern
94 94
95 95 def name
96 96 self.class.format_name
97 97 end
98 98
99 99 def label
100 100 "label_#{name}"
101 101 end
102 102
103 103 def cast_custom_value(custom_value)
104 104 cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
105 105 end
106 106
107 107 def cast_value(custom_field, value, customized=nil)
108 108 if value.blank?
109 109 nil
110 110 elsif value.is_a?(Array)
111 111 casted = value.map do |v|
112 112 cast_single_value(custom_field, v, customized)
113 113 end
114 114 casted.compact.sort
115 115 else
116 116 cast_single_value(custom_field, value, customized)
117 117 end
118 118 end
119 119
120 120 def cast_single_value(custom_field, value, customized=nil)
121 121 value.to_s
122 122 end
123 123
124 124 def target_class
125 125 nil
126 126 end
127 127
128 128 def possible_custom_value_options(custom_value)
129 129 possible_values_options(custom_value.custom_field, custom_value.customized)
130 130 end
131 131
132 132 def possible_values_options(custom_field, object=nil)
133 133 []
134 134 end
135 135
136 136 def value_from_keyword(custom_field, keyword, object)
137 137 possible_values_options = possible_values_options(custom_field, object)
138 138 if possible_values_options.present?
139 139 keyword = keyword.to_s
140 140 if v = possible_values_options.detect {|text, id| keyword.casecmp(text) == 0}
141 141 if v.is_a?(Array)
142 142 v.last
143 143 else
144 144 v
145 145 end
146 146 end
147 147 else
148 148 keyword
149 149 end
150 150 end
151 151
152 152 # Returns the validation errors for custom_field
153 153 # Should return an empty array if custom_field is valid
154 154 def validate_custom_field(custom_field)
155 155 errors = []
156 156 pattern = custom_field.url_pattern
157 157 if pattern.present? && !uri_with_safe_scheme?(url_pattern_without_tokens(pattern))
158 158 errors << [:url_pattern, :invalid]
159 159 end
160 160 errors
161 161 end
162 162
163 163 # Returns the validation error messages for custom_value
164 164 # Should return an empty array if custom_value is valid
165 165 def validate_custom_value(custom_value)
166 166 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
167 167 errors = values.map do |value|
168 168 validate_single_value(custom_value.custom_field, value, custom_value.customized)
169 169 end
170 170 errors.flatten.uniq
171 171 end
172 172
173 173 def validate_single_value(custom_field, value, customized=nil)
174 174 []
175 175 end
176 176
177 177 def formatted_custom_value(view, custom_value, html=false)
178 178 formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
179 179 end
180 180
181 181 def formatted_value(view, custom_field, value, customized=nil, html=false)
182 182 casted = cast_value(custom_field, value, customized)
183 183 if html && custom_field.url_pattern.present?
184 184 texts_and_urls = Array.wrap(casted).map do |single_value|
185 185 text = view.format_object(single_value, false).to_s
186 186 url = url_from_pattern(custom_field, single_value, customized)
187 187 [text, url]
188 188 end
189 189 links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to_if uri_with_safe_scheme?(url), text, url}
190 190 links.join(', ').html_safe
191 191 else
192 192 casted
193 193 end
194 194 end
195 195
196 196 # Returns an URL generated with the custom field URL pattern
197 197 # and variables substitution:
198 198 # %value% => the custom field value
199 199 # %id% => id of the customized object
200 200 # %project_id% => id of the project of the customized object if defined
201 201 # %project_identifier% => identifier of the project of the customized object if defined
202 202 # %m1%, %m2%... => capture groups matches of the custom field regexp if defined
203 203 def url_from_pattern(custom_field, value, customized)
204 204 url = custom_field.url_pattern.to_s.dup
205 205 url.gsub!('%value%') {value.to_s}
206 206 url.gsub!('%id%') {customized.id.to_s}
207 207 url.gsub!('%project_id%') {(customized.respond_to?(:project) ? customized.project.try(:id) : nil).to_s}
208 208 url.gsub!('%project_identifier%') {(customized.respond_to?(:project) ? customized.project.try(:identifier) : nil).to_s}
209 209 if custom_field.regexp.present?
210 210 url.gsub!(%r{%m(\d+)%}) do
211 211 m = $1.to_i
212 212 if matches ||= value.to_s.match(Regexp.new(custom_field.regexp))
213 213 matches[m].to_s
214 214 end
215 215 end
216 216 end
217 217 URI.encode(url)
218 218 end
219 219 protected :url_from_pattern
220 220
221 221 # Returns the URL pattern with substitution tokens removed,
222 222 # for validation purpose
223 223 def url_pattern_without_tokens(url_pattern)
224 224 url_pattern.to_s.gsub(/%(value|id|project_id|project_identifier|m\d+)%/, '')
225 225 end
226 226 protected :url_pattern_without_tokens
227 227
228 228 def edit_tag(view, tag_id, tag_name, custom_value, options={})
229 229 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id))
230 230 end
231 231
232 232 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
233 233 view.text_field_tag(tag_name, value, options.merge(:id => tag_id)) +
234 234 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
235 235 end
236 236
237 237 def bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
238 238 if custom_field.is_required?
239 239 ''.html_safe
240 240 else
241 241 view.content_tag('label',
242 242 view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
243 243 :class => 'inline'
244 244 )
245 245 end
246 246 end
247 247 protected :bulk_clear_tag
248 248
249 249 def query_filter_options(custom_field, query)
250 250 {:type => :string}
251 251 end
252 252
253 253 def before_custom_field_save(custom_field)
254 254 end
255 255
256 256 # Returns a ORDER BY clause that can used to sort customized
257 257 # objects by their value of the custom field.
258 258 # Returns nil if the custom field can not be used for sorting.
259 259 def order_statement(custom_field)
260 260 # COALESCE is here to make sure that blank and NULL values are sorted equally
261 261 "COALESCE(#{join_alias custom_field}.value, '')"
262 262 end
263 263
264 264 # Returns a GROUP BY clause that can used to group by custom value
265 265 # Returns nil if the custom field can not be used for grouping.
266 266 def group_statement(custom_field)
267 267 nil
268 268 end
269 269
270 270 # Returns a JOIN clause that is added to the query when sorting by custom values
271 271 def join_for_order_statement(custom_field)
272 272 alias_name = join_alias(custom_field)
273 273
274 274 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
275 275 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
276 276 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
277 277 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
278 278 " AND (#{custom_field.visibility_by_project_condition})" +
279 279 " AND #{alias_name}.value <> ''" +
280 280 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
281 281 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
282 282 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
283 283 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)"
284 284 end
285 285
286 286 def join_alias(custom_field)
287 287 "cf_#{custom_field.id}"
288 288 end
289 289 protected :join_alias
290 290 end
291 291
292 292 class Unbounded < Base
293 293 def validate_single_value(custom_field, value, customized=nil)
294 294 errs = super
295 295 value = value.to_s
296 296 unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
297 297 errs << ::I18n.t('activerecord.errors.messages.invalid')
298 298 end
299 299 if custom_field.min_length && value.length < custom_field.min_length
300 300 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => custom_field.min_length)
301 301 end
302 302 if custom_field.max_length && custom_field.max_length > 0 && value.length > custom_field.max_length
303 303 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => custom_field.max_length)
304 304 end
305 305 errs
306 306 end
307 307 end
308 308
309 309 class StringFormat < Unbounded
310 310 add 'string'
311 311 self.searchable_supported = true
312 312 self.form_partial = 'custom_fields/formats/string'
313 313 field_attributes :text_formatting
314 314
315 315 def formatted_value(view, custom_field, value, customized=nil, html=false)
316 316 if html
317 317 if custom_field.url_pattern.present?
318 318 super
319 319 elsif custom_field.text_formatting == 'full'
320 320 view.textilizable(value, :object => customized)
321 321 else
322 322 value.to_s
323 323 end
324 324 else
325 325 value.to_s
326 326 end
327 327 end
328 328 end
329 329
330 330 class TextFormat < Unbounded
331 331 add 'text'
332 332 self.searchable_supported = true
333 333 self.form_partial = 'custom_fields/formats/text'
334 334 self.change_as_diff = true
335 335
336 336 def formatted_value(view, custom_field, value, customized=nil, html=false)
337 337 if html
338 338 if value.present?
339 339 if custom_field.text_formatting == 'full'
340 340 view.textilizable(value, :object => customized)
341 341 else
342 342 view.simple_format(html_escape(value))
343 343 end
344 344 else
345 345 ''
346 346 end
347 347 else
348 348 value.to_s
349 349 end
350 350 end
351 351
352 352 def edit_tag(view, tag_id, tag_name, custom_value, options={})
353 353 view.text_area_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :rows => 3))
354 354 end
355 355
356 356 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
357 357 view.text_area_tag(tag_name, value, options.merge(:id => tag_id, :rows => 3)) +
358 358 '<br />'.html_safe +
359 359 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
360 360 end
361 361
362 362 def query_filter_options(custom_field, query)
363 363 {:type => :text}
364 364 end
365 365 end
366 366
367 367 class LinkFormat < StringFormat
368 368 add 'link'
369 369 self.searchable_supported = false
370 370 self.form_partial = 'custom_fields/formats/link'
371 371
372 372 def formatted_value(view, custom_field, value, customized=nil, html=false)
373 373 if html && value.present?
374 374 if custom_field.url_pattern.present?
375 375 url = url_from_pattern(custom_field, value, customized)
376 376 else
377 377 url = value.to_s
378 378 unless url =~ %r{\A[a-z]+://}i
379 379 # no protocol found, use http by default
380 380 url = "http://" + url
381 381 end
382 382 end
383 383 view.link_to value.to_s.truncate(40), url
384 384 else
385 385 value.to_s
386 386 end
387 387 end
388 388 end
389 389
390 390 class Numeric < Unbounded
391 391 self.form_partial = 'custom_fields/formats/numeric'
392 392 self.totalable_supported = true
393 393
394 394 def order_statement(custom_field)
395 395 # Make the database cast values into numeric
396 396 # Postgresql will raise an error if a value can not be casted!
397 397 # CustomValue validations should ensure that it doesn't occur
398 398 "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
399 399 end
400 400
401 401 # Returns totals for the given scope
402 402 def total_for_scope(custom_field, scope)
403 403 scope.joins(:custom_values).
404 404 where(:custom_values => {:custom_field_id => custom_field.id}).
405 405 where.not(:custom_values => {:value => ''}).
406 406 sum("CAST(#{CustomValue.table_name}.value AS decimal(30,3))")
407 407 end
408 408
409 409 def cast_total_value(custom_field, value)
410 410 cast_single_value(custom_field, value)
411 411 end
412 412 end
413 413
414 414 class IntFormat < Numeric
415 415 add 'int'
416 416
417 417 def label
418 418 "label_integer"
419 419 end
420 420
421 421 def cast_single_value(custom_field, value, customized=nil)
422 422 value.to_i
423 423 end
424 424
425 425 def validate_single_value(custom_field, value, customized=nil)
426 426 errs = super
427 427 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value.to_s =~ /^[+-]?\d+$/
428 428 errs
429 429 end
430 430
431 431 def query_filter_options(custom_field, query)
432 432 {:type => :integer}
433 433 end
434 434
435 435 def group_statement(custom_field)
436 436 order_statement(custom_field)
437 437 end
438 438 end
439 439
440 440 class FloatFormat < Numeric
441 441 add 'float'
442 442
443 443 def cast_single_value(custom_field, value, customized=nil)
444 444 value.to_f
445 445 end
446 446
447 447 def cast_total_value(custom_field, value)
448 448 value.to_f.round(2)
449 449 end
450 450
451 451 def validate_single_value(custom_field, value, customized=nil)
452 452 errs = super
453 453 errs << ::I18n.t('activerecord.errors.messages.invalid') unless (Kernel.Float(value) rescue nil)
454 454 errs
455 455 end
456 456
457 457 def query_filter_options(custom_field, query)
458 458 {:type => :float}
459 459 end
460 460 end
461 461
462 462 class DateFormat < Unbounded
463 463 add 'date'
464 464 self.form_partial = 'custom_fields/formats/date'
465 465
466 466 def cast_single_value(custom_field, value, customized=nil)
467 467 value.to_date rescue nil
468 468 end
469 469
470 470 def validate_single_value(custom_field, value, customized=nil)
471 471 if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
472 472 []
473 473 else
474 474 [::I18n.t('activerecord.errors.messages.not_a_date')]
475 475 end
476 476 end
477 477
478 478 def edit_tag(view, tag_id, tag_name, custom_value, options={})
479 479 view.date_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :size => 10)) +
480 480 view.calendar_for(tag_id)
481 481 end
482 482
483 483 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
484 484 view.date_field_tag(tag_name, value, options.merge(:id => tag_id, :size => 10)) +
485 485 view.calendar_for(tag_id) +
486 486 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
487 487 end
488 488
489 489 def query_filter_options(custom_field, query)
490 490 {:type => :date}
491 491 end
492 492
493 493 def group_statement(custom_field)
494 494 order_statement(custom_field)
495 495 end
496 496 end
497 497
498 498 class List < Base
499 499 self.multiple_supported = true
500 500 field_attributes :edit_tag_style
501 501
502 502 def edit_tag(view, tag_id, tag_name, custom_value, options={})
503 503 if custom_value.custom_field.edit_tag_style == 'check_box'
504 504 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
505 505 else
506 506 select_edit_tag(view, tag_id, tag_name, custom_value, options)
507 507 end
508 508 end
509 509
510 510 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
511 511 opts = []
512 512 opts << [l(:label_no_change_option), ''] unless custom_field.multiple?
513 513 opts << [l(:label_none), '__none__'] unless custom_field.is_required?
514 514 opts += possible_values_options(custom_field, objects)
515 515 view.select_tag(tag_name, view.options_for_select(opts, value), options.merge(:multiple => custom_field.multiple?))
516 516 end
517 517
518 518 def query_filter_options(custom_field, query)
519 519 {:type => :list_optional, :values => query_filter_values(custom_field, query)}
520 520 end
521 521
522 522 protected
523 523
524 524 # Returns the values that are available in the field filter
525 525 def query_filter_values(custom_field, query)
526 526 possible_values_options(custom_field, query.project)
527 527 end
528 528
529 529 # Renders the edit tag as a select tag
530 530 def select_edit_tag(view, tag_id, tag_name, custom_value, options={})
531 531 blank_option = ''.html_safe
532 532 unless custom_value.custom_field.multiple?
533 533 if custom_value.custom_field.is_required?
534 534 unless custom_value.custom_field.default_value.present?
535 535 blank_option = view.content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
536 536 end
537 537 else
538 538 blank_option = view.content_tag('option', '&nbsp;'.html_safe, :value => '')
539 539 end
540 540 end
541 541 options_tags = blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
542 542 s = view.select_tag(tag_name, options_tags, options.merge(:id => tag_id, :multiple => custom_value.custom_field.multiple?))
543 543 if custom_value.custom_field.multiple?
544 544 s << view.hidden_field_tag(tag_name, '')
545 545 end
546 546 s
547 547 end
548 548
549 549 # Renders the edit tag as check box or radio tags
550 550 def check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
551 551 opts = []
552 552 unless custom_value.custom_field.multiple? || custom_value.custom_field.is_required?
553 553 opts << ["(#{l(:label_none)})", '']
554 554 end
555 555 opts += possible_custom_value_options(custom_value)
556 556 s = ''.html_safe
557 557 tag_method = custom_value.custom_field.multiple? ? :check_box_tag : :radio_button_tag
558 558 opts.each do |label, value|
559 559 value ||= label
560 560 checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
561 561 tag = view.send(tag_method, tag_name, value, checked, :id => tag_id)
562 562 # set the id on the first tag only
563 563 tag_id = nil
564 564 s << view.content_tag('label', tag + ' ' + label)
565 565 end
566 566 if custom_value.custom_field.multiple?
567 567 s << view.hidden_field_tag(tag_name, '')
568 568 end
569 569 css = "#{options[:class]} check_box_group"
570 570 view.content_tag('span', s, options.merge(:class => css))
571 571 end
572 572 end
573 573
574 574 class ListFormat < List
575 575 add 'list'
576 576 self.searchable_supported = true
577 577 self.form_partial = 'custom_fields/formats/list'
578 578
579 579 def possible_custom_value_options(custom_value)
580 580 options = possible_values_options(custom_value.custom_field)
581 581 missing = [custom_value.value].flatten.reject(&:blank?) - options
582 582 if missing.any?
583 583 options += missing
584 584 end
585 585 options
586 586 end
587 587
588 588 def possible_values_options(custom_field, object=nil)
589 589 custom_field.possible_values
590 590 end
591 591
592 592 def validate_custom_field(custom_field)
593 593 errors = []
594 594 errors << [:possible_values, :blank] if custom_field.possible_values.blank?
595 595 errors << [:possible_values, :invalid] unless custom_field.possible_values.is_a? Array
596 596 errors
597 597 end
598 598
599 599 def validate_custom_value(custom_value)
600 600 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
601 601 invalid_values = values - Array.wrap(custom_value.value_was) - custom_value.custom_field.possible_values
602 602 if invalid_values.any?
603 603 [::I18n.t('activerecord.errors.messages.inclusion')]
604 604 else
605 605 []
606 606 end
607 607 end
608 608
609 609 def group_statement(custom_field)
610 610 order_statement(custom_field)
611 611 end
612 612 end
613 613
614 614 class BoolFormat < List
615 615 add 'bool'
616 616 self.multiple_supported = false
617 617 self.form_partial = 'custom_fields/formats/bool'
618 618
619 619 def label
620 620 "label_boolean"
621 621 end
622 622
623 623 def cast_single_value(custom_field, value, customized=nil)
624 624 value == '1' ? true : false
625 625 end
626 626
627 627 def possible_values_options(custom_field, object=nil)
628 628 [[::I18n.t(:general_text_Yes), '1'], [::I18n.t(:general_text_No), '0']]
629 629 end
630 630
631 631 def group_statement(custom_field)
632 632 order_statement(custom_field)
633 633 end
634 634
635 635 def edit_tag(view, tag_id, tag_name, custom_value, options={})
636 636 case custom_value.custom_field.edit_tag_style
637 637 when 'check_box'
638 638 single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
639 639 when 'radio'
640 640 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
641 641 else
642 642 select_edit_tag(view, tag_id, tag_name, custom_value, options)
643 643 end
644 644 end
645 645
646 646 # Renders the edit tag as a simple check box
647 647 def single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
648 648 s = ''.html_safe
649 649 s << view.hidden_field_tag(tag_name, '0', :id => nil)
650 650 s << view.check_box_tag(tag_name, '1', custom_value.value.to_s == '1', :id => tag_id)
651 651 view.content_tag('span', s, options)
652 652 end
653 653 end
654 654
655 655 class RecordList < List
656 656 self.customized_class_names = %w(Issue TimeEntry Version Document Project)
657 657
658 658 def cast_single_value(custom_field, value, customized=nil)
659 659 target_class.find_by_id(value.to_i) if value.present?
660 660 end
661 661
662 662 def target_class
663 663 @target_class ||= self.class.name[/^(.*::)?(.+)Format$/, 2].constantize rescue nil
664 664 end
665 665
666 666 def reset_target_class
667 667 @target_class = nil
668 668 end
669 669
670 670 def possible_custom_value_options(custom_value)
671 671 options = possible_values_options(custom_value.custom_field, custom_value.customized)
672 672 missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
673 673 if missing.any?
674 674 options += target_class.where(:id => missing.map(&:to_i)).map {|o| [o.to_s, o.id.to_s]}
675 675 end
676 676 options
677 677 end
678 678
679 679 def order_statement(custom_field)
680 680 if target_class.respond_to?(:fields_for_order_statement)
681 681 target_class.fields_for_order_statement(value_join_alias(custom_field))
682 682 end
683 683 end
684 684
685 685 def group_statement(custom_field)
686 686 "COALESCE(#{join_alias custom_field}.value, '')"
687 687 end
688 688
689 689 def join_for_order_statement(custom_field)
690 690 alias_name = join_alias(custom_field)
691 691
692 692 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
693 693 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
694 694 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
695 695 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
696 696 " AND (#{custom_field.visibility_by_project_condition})" +
697 697 " AND #{alias_name}.value <> ''" +
698 698 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
699 699 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
700 700 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
701 701 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)" +
702 702 " LEFT OUTER JOIN #{target_class.table_name} #{value_join_alias custom_field}" +
703 703 " ON CAST(CASE #{alias_name}.value WHEN '' THEN '0' ELSE #{alias_name}.value END AS decimal(30,0)) = #{value_join_alias custom_field}.id"
704 704 end
705 705
706 706 def value_join_alias(custom_field)
707 707 join_alias(custom_field) + "_" + custom_field.field_format
708 708 end
709 709 protected :value_join_alias
710 710 end
711 711
712 712 class EnumerationFormat < RecordList
713 713 add 'enumeration'
714 714 self.form_partial = 'custom_fields/formats/enumeration'
715 715
716 716 def label
717 717 "label_field_format_enumeration"
718 718 end
719 719
720 720 def target_class
721 721 @target_class ||= CustomFieldEnumeration
722 722 end
723 723
724 724 def possible_values_options(custom_field, object=nil)
725 725 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
726 726 end
727 727
728 728 def possible_values_records(custom_field, object=nil)
729 729 custom_field.enumerations.active
730 730 end
731 731
732 732 def value_from_keyword(custom_field, keyword, object)
733 733 value = custom_field.enumerations.where("LOWER(name) LIKE LOWER(?)", keyword).first
734 734 value ? value.id : nil
735 735 end
736 736 end
737 737
738 738 class UserFormat < RecordList
739 739 add 'user'
740 740 self.form_partial = 'custom_fields/formats/user'
741 741 field_attributes :user_role
742 742
743 743 def possible_values_options(custom_field, object=nil)
744 744 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
745 745 end
746 746
747 747 def possible_values_records(custom_field, object=nil)
748 748 if object.is_a?(Array)
749 749 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
750 750 projects.map {|project| possible_values_records(custom_field, project)}.reduce(:&) || []
751 751 elsif object.respond_to?(:project) && object.project
752 752 scope = object.project.users
753 753 if custom_field.user_role.is_a?(Array)
754 754 role_ids = custom_field.user_role.map(&:to_s).reject(&:blank?).map(&:to_i)
755 755 if role_ids.any?
756 756 scope = scope.where("#{Member.table_name}.id IN (SELECT DISTINCT member_id FROM #{MemberRole.table_name} WHERE role_id IN (?))", role_ids)
757 757 end
758 758 end
759 759 scope.sorted
760 760 else
761 761 []
762 762 end
763 763 end
764 764
765 765 def value_from_keyword(custom_field, keyword, object)
766 766 users = possible_values_records(custom_field, object).to_a
767 767 user = Principal.detect_by_keyword(users, keyword)
768 768 user ? user.id : nil
769 769 end
770 770
771 771 def before_custom_field_save(custom_field)
772 772 super
773 773 if custom_field.user_role.is_a?(Array)
774 774 custom_field.user_role.map!(&:to_s).reject!(&:blank?)
775 775 end
776 776 end
777 777 end
778 778
779 779 class VersionFormat < RecordList
780 780 add 'version'
781 781 self.form_partial = 'custom_fields/formats/version'
782 782 field_attributes :version_status
783 783
784 784 def possible_values_options(custom_field, object=nil)
785 785 versions_options(custom_field, object)
786 786 end
787 787
788 788 def before_custom_field_save(custom_field)
789 789 super
790 790 if custom_field.version_status.is_a?(Array)
791 791 custom_field.version_status.map!(&:to_s).reject!(&:blank?)
792 792 end
793 793 end
794 794
795 795 protected
796 796
797 797 def query_filter_values(custom_field, query)
798 798 versions_options(custom_field, query.project, true)
799 799 end
800 800
801 801 def versions_options(custom_field, object, all_statuses=false)
802 802 if object.is_a?(Array)
803 803 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
804 804 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
805 805 elsif object.respond_to?(:project) && object.project
806 806 scope = object.project.shared_versions
807 807 filtered_versions_options(custom_field, scope, all_statuses)
808 808 elsif object.nil?
809 scope = Version.visible.where(:sharing => 'system')
809 scope = ::Version.visible.where(:sharing => 'system')
810 810 filtered_versions_options(custom_field, scope, all_statuses)
811 811 else
812 812 []
813 813 end
814 814 end
815 815
816 816 def filtered_versions_options(custom_field, scope, all_statuses=false)
817 817 if !all_statuses && custom_field.version_status.is_a?(Array)
818 818 statuses = custom_field.version_status.map(&:to_s).reject(&:blank?)
819 819 if statuses.any?
820 820 scope = scope.where(:status => statuses.map(&:to_s))
821 821 end
822 822 end
823 823 scope.sort.collect{|u| [u.to_s, u.id.to_s] }
824 824 end
825 825 end
826 826 end
827 827 end
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,374 +1,401
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class QueriesControllerTest < ActionController::TestCase
21 21 fixtures :projects, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries, :enabled_modules
22 22
23 23 def setup
24 24 User.current = nil
25 25 end
26 26
27 27 def test_index
28 28 get :index
29 29 # HTML response not implemented
30 30 assert_response 406
31 31 end
32 32
33 33 def test_new_project_query
34 34 @request.session[:user_id] = 2
35 35 get :new, :project_id => 1
36 36 assert_response :success
37 37 assert_template 'new'
38 38 assert_select 'input[name=?][value="0"][checked=checked]', 'query[visibility]'
39 39 assert_select 'input[name=query_is_for_all][type=checkbox]:not([checked]):not([disabled])'
40 40 assert_select 'select[name=?]', 'c[]' do
41 41 assert_select 'option[value=tracker]'
42 42 assert_select 'option[value=subject]'
43 43 end
44 44 end
45 45
46 46 def test_new_global_query
47 47 @request.session[:user_id] = 2
48 48 get :new
49 49 assert_response :success
50 50 assert_template 'new'
51 51 assert_select 'input[name=?]', 'query[visibility]', 0
52 52 assert_select 'input[name=query_is_for_all][type=checkbox][checked]:not([disabled])'
53 53 end
54 54
55 55 def test_new_on_invalid_project
56 56 @request.session[:user_id] = 2
57 57 get :new, :project_id => 'invalid'
58 58 assert_response 404
59 59 end
60 60
61 def test_new_time_entry_query
62 @request.session[:user_id] = 2
63 get :new, :project_id => 1, :type => 'TimeEntryQuery'
64 assert_response :success
65 assert_select 'input[name=type][value=?]', 'TimeEntryQuery'
66 end
67
61 68 def test_create_project_public_query
62 69 @request.session[:user_id] = 2
63 70 post :create,
64 71 :project_id => 'ecookbook',
65 72 :default_columns => '1',
66 73 :f => ["status_id", "assigned_to_id"],
67 74 :op => {"assigned_to_id" => "=", "status_id" => "o"},
68 75 :v => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
69 76 :query => {"name" => "test_new_project_public_query", "visibility" => "2"}
70 77
71 78 q = Query.find_by_name('test_new_project_public_query')
72 79 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
73 80 assert q.is_public?
74 81 assert q.has_default_columns?
75 82 assert q.valid?
76 83 end
77 84
78 85 def test_create_project_private_query
79 86 @request.session[:user_id] = 3
80 87 post :create,
81 88 :project_id => 'ecookbook',
82 89 :default_columns => '1',
83 90 :fields => ["status_id", "assigned_to_id"],
84 91 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
85 92 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
86 93 :query => {"name" => "test_new_project_private_query", "visibility" => "0"}
87 94
88 95 q = Query.find_by_name('test_new_project_private_query')
89 96 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
90 97 assert !q.is_public?
91 98 assert q.has_default_columns?
92 99 assert q.valid?
93 100 end
94 101
95 102 def test_create_project_roles_query
96 103 @request.session[:user_id] = 2
97 104 post :create,
98 105 :project_id => 'ecookbook',
99 106 :default_columns => '1',
100 107 :fields => ["status_id", "assigned_to_id"],
101 108 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
102 109 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
103 110 :query => {"name" => "test_create_project_roles_query", "visibility" => "1", "role_ids" => ["1", "2", ""]}
104 111
105 112 q = Query.find_by_name('test_create_project_roles_query')
106 113 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
107 114 assert_equal Query::VISIBILITY_ROLES, q.visibility
108 115 assert_equal [1, 2], q.roles.ids.sort
109 116 end
110 117
111 118 def test_create_global_private_query_with_custom_columns
112 119 @request.session[:user_id] = 3
113 120 post :create,
114 121 :fields => ["status_id", "assigned_to_id"],
115 122 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
116 123 :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
117 124 :query => {"name" => "test_new_global_private_query", "visibility" => "0"},
118 125 :c => ["", "tracker", "subject", "priority", "category"]
119 126
120 127 q = Query.find_by_name('test_new_global_private_query')
121 128 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q
122 129 assert !q.is_public?
123 130 assert !q.has_default_columns?
124 131 assert_equal [:id, :tracker, :subject, :priority, :category], q.columns.collect {|c| c.name}
125 132 assert q.valid?
126 133 end
127 134
128 135 def test_create_global_query_with_custom_filters
129 136 @request.session[:user_id] = 3
130 137 post :create,
131 138 :fields => ["assigned_to_id"],
132 139 :operators => {"assigned_to_id" => "="},
133 140 :values => { "assigned_to_id" => ["me"]},
134 141 :query => {"name" => "test_new_global_query"}
135 142
136 143 q = Query.find_by_name('test_new_global_query')
137 144 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q
138 145 assert !q.is_public?
139 146 assert !q.has_filter?(:status_id)
140 147 assert_equal ['assigned_to_id'], q.filters.keys
141 148 assert q.valid?
142 149 end
143 150
144 151 def test_create_with_sort
145 152 @request.session[:user_id] = 1
146 153 post :create,
147 154 :default_columns => '1',
148 155 :operators => {"status_id" => "o"},
149 156 :values => {"status_id" => ["1"]},
150 157 :query => {:name => "test_new_with_sort",
151 158 :visibility => "2",
152 159 :sort_criteria => {"0" => ["due_date", "desc"], "1" => ["tracker", ""]}}
153 160
154 161 query = Query.find_by_name("test_new_with_sort")
155 162 assert_not_nil query
156 163 assert_equal [['due_date', 'desc'], ['tracker', 'asc']], query.sort_criteria
157 164 end
158 165
159 166 def test_create_with_failure
160 167 @request.session[:user_id] = 2
161 168 assert_no_difference '::Query.count' do
162 169 post :create, :project_id => 'ecookbook', :query => {:name => ''}
163 170 end
164 171 assert_response :success
165 172 assert_template 'new'
166 173 assert_select 'input[name=?]', 'query[name]'
167 174 end
168 175
169 176 def test_create_global_query_from_gantt
170 177 @request.session[:user_id] = 1
171 178 assert_difference 'IssueQuery.count' do
172 179 post :create,
173 180 :gantt => 1,
174 181 :operators => {"status_id" => "o"},
175 182 :values => {"status_id" => ["1"]},
176 183 :query => {:name => "test_create_from_gantt",
177 184 :draw_relations => '1',
178 185 :draw_progress_line => '1'}
179 186 assert_response 302
180 187 end
181 188 query = IssueQuery.order('id DESC').first
182 189 assert_redirected_to "/issues/gantt?query_id=#{query.id}"
183 190 assert_equal true, query.draw_relations
184 191 assert_equal true, query.draw_progress_line
185 192 end
186 193
187 194 def test_create_project_query_from_gantt
188 195 @request.session[:user_id] = 1
189 196 assert_difference 'IssueQuery.count' do
190 197 post :create,
191 198 :project_id => 'ecookbook',
192 199 :gantt => 1,
193 200 :operators => {"status_id" => "o"},
194 201 :values => {"status_id" => ["1"]},
195 202 :query => {:name => "test_create_from_gantt",
196 203 :draw_relations => '0',
197 204 :draw_progress_line => '0'}
198 205 assert_response 302
199 206 end
200 207 query = IssueQuery.order('id DESC').first
201 208 assert_redirected_to "/projects/ecookbook/issues/gantt?query_id=#{query.id}"
202 209 assert_equal false, query.draw_relations
203 210 assert_equal false, query.draw_progress_line
204 211 end
205 212
206 213 def test_create_project_public_query_should_force_private_without_manage_public_queries_permission
207 214 @request.session[:user_id] = 3
208 215 query = new_record(Query) do
209 216 post :create,
210 217 :project_id => 'ecookbook',
211 218 :query => {"name" => "name", "visibility" => "2"}
212 219 assert_response 302
213 220 end
214 221 assert_not_nil query.project
215 222 assert_equal Query::VISIBILITY_PRIVATE, query.visibility
216 223 end
217 224
218 225 def test_create_global_public_query_should_force_private_without_manage_public_queries_permission
219 226 @request.session[:user_id] = 3
220 227 query = new_record(Query) do
221 228 post :create,
222 229 :project_id => 'ecookbook', :query_is_for_all => '1',
223 230 :query => {"name" => "name", "visibility" => "2"}
224 231 assert_response 302
225 232 end
226 233 assert_nil query.project
227 234 assert_equal Query::VISIBILITY_PRIVATE, query.visibility
228 235 end
229 236
230 237 def test_create_project_public_query_with_manage_public_queries_permission
231 238 @request.session[:user_id] = 2
232 239 query = new_record(Query) do
233 240 post :create,
234 241 :project_id => 'ecookbook',
235 242 :query => {"name" => "name", "visibility" => "2"}
236 243 assert_response 302
237 244 end
238 245 assert_not_nil query.project
239 246 assert_equal Query::VISIBILITY_PUBLIC, query.visibility
240 247 end
241 248
242 249 def test_create_global_public_query_should_force_private_with_manage_public_queries_permission
243 250 @request.session[:user_id] = 2
244 251 query = new_record(Query) do
245 252 post :create,
246 253 :project_id => 'ecookbook', :query_is_for_all => '1',
247 254 :query => {"name" => "name", "visibility" => "2"}
248 255 assert_response 302
249 256 end
250 257 assert_nil query.project
251 258 assert_equal Query::VISIBILITY_PRIVATE, query.visibility
252 259 end
253 260
254 261 def test_create_global_public_query_by_admin
255 262 @request.session[:user_id] = 1
256 263 query = new_record(Query) do
257 264 post :create,
258 265 :project_id => 'ecookbook', :query_is_for_all => '1',
259 266 :query => {"name" => "name", "visibility" => "2"}
260 267 assert_response 302
261 268 end
262 269 assert_nil query.project
263 270 assert_equal Query::VISIBILITY_PUBLIC, query.visibility
264 271 end
265 272
273 def test_create_project_public_time_entry_query
274 @request.session[:user_id] = 2
275
276 q = new_record(TimeEntryQuery) do
277 post :create,
278 :project_id => 'ecookbook',
279 :type => 'TimeEntryQuery',
280 :default_columns => '1',
281 :f => ["spent_on"],
282 :op => {"spent_on" => "="},
283 :v => { "spent_on" => ["2016-07-14"]},
284 :query => {"name" => "test_new_project_public_query", "visibility" => "2"}
285 end
286
287 assert_redirected_to :controller => 'timelog', :action => 'index', :project_id => 'ecookbook', :query_id => q.id
288 assert q.is_public?
289 assert q.has_default_columns?
290 assert q.valid?
291 end
292
266 293 def test_edit_global_public_query
267 294 @request.session[:user_id] = 1
268 295 get :edit, :id => 4
269 296 assert_response :success
270 297 assert_template 'edit'
271 298 assert_select 'input[name=?][value="2"][checked=checked]', 'query[visibility]'
272 299 assert_select 'input[name=query_is_for_all][type=checkbox][checked=checked]'
273 300 end
274 301
275 302 def test_edit_global_private_query
276 303 @request.session[:user_id] = 3
277 304 get :edit, :id => 3
278 305 assert_response :success
279 306 assert_template 'edit'
280 307 assert_select 'input[name=?]', 'query[visibility]', 0
281 308 assert_select 'input[name=query_is_for_all][type=checkbox][checked=checked]'
282 309 end
283 310
284 311 def test_edit_project_private_query
285 312 @request.session[:user_id] = 3
286 313 get :edit, :id => 2
287 314 assert_response :success
288 315 assert_template 'edit'
289 316 assert_select 'input[name=?]', 'query[visibility]', 0
290 317 assert_select 'input[name=query_is_for_all][type=checkbox]:not([checked])'
291 318 end
292 319
293 320 def test_edit_project_public_query
294 321 @request.session[:user_id] = 2
295 322 get :edit, :id => 1
296 323 assert_response :success
297 324 assert_template 'edit'
298 325 assert_select 'input[name=?][value="2"][checked=checked]', 'query[visibility]'
299 326 assert_select 'input[name=query_is_for_all][type=checkbox]:not([checked])'
300 327 end
301 328
302 329 def test_edit_sort_criteria
303 330 @request.session[:user_id] = 1
304 331 get :edit, :id => 5
305 332 assert_response :success
306 333 assert_template 'edit'
307 334 assert_select 'select[name=?]', 'query[sort_criteria][0][]' do
308 335 assert_select 'option[value=priority][selected=selected]'
309 336 assert_select 'option[value=desc][selected=selected]'
310 337 end
311 338 end
312 339
313 340 def test_edit_invalid_query
314 341 @request.session[:user_id] = 2
315 342 get :edit, :id => 99
316 343 assert_response 404
317 344 end
318 345
319 346 def test_udpate_global_private_query
320 347 @request.session[:user_id] = 3
321 348 put :update,
322 349 :id => 3,
323 350 :default_columns => '1',
324 351 :fields => ["status_id", "assigned_to_id"],
325 352 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
326 353 :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
327 354 :query => {"name" => "test_edit_global_private_query", "visibility" => "2"}
328 355
329 356 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 3
330 357 q = Query.find_by_name('test_edit_global_private_query')
331 358 assert !q.is_public?
332 359 assert q.has_default_columns?
333 360 assert q.valid?
334 361 end
335 362
336 363 def test_update_global_public_query
337 364 @request.session[:user_id] = 1
338 365 put :update,
339 366 :id => 4,
340 367 :default_columns => '1',
341 368 :fields => ["status_id", "assigned_to_id"],
342 369 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
343 370 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
344 371 :query => {"name" => "test_edit_global_public_query", "visibility" => "2"}
345 372
346 373 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 4
347 374 q = Query.find_by_name('test_edit_global_public_query')
348 375 assert q.is_public?
349 376 assert q.has_default_columns?
350 377 assert q.valid?
351 378 end
352 379
353 380 def test_update_with_failure
354 381 @request.session[:user_id] = 1
355 382 put :update, :id => 4, :query => {:name => ''}
356 383 assert_response :success
357 384 assert_template 'edit'
358 385 end
359 386
360 387 def test_destroy
361 388 @request.session[:user_id] = 2
362 389 delete :destroy, :id => 1
363 390 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :set_filter => 1, :query_id => nil
364 391 assert_nil Query.find_by_id(1)
365 392 end
366 393
367 394 def test_backslash_should_be_escaped_in_filters
368 395 @request.session[:user_id] = 2
369 396 get :new, :subject => 'foo/bar'
370 397 assert_response :success
371 398 assert_template 'new'
372 399 assert_include 'addFilter("subject", "=", ["foo\/bar"]);', response.body
373 400 end
374 401 end
@@ -1,835 +1,845
1 1 # -*- coding: utf-8 -*-
2 2 # Redmine - project management software
3 3 # Copyright (C) 2006-2016 Jean-Philippe Lang
4 4 #
5 5 # This program is free software; you can redistribute it and/or
6 6 # modify it under the terms of the GNU General Public License
7 7 # as published by the Free Software Foundation; either version 2
8 8 # of the License, or (at your option) any later version.
9 9 #
10 10 # This program is distributed in the hope that it will be useful,
11 11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 13 # GNU General Public License for more details.
14 14 #
15 15 # You should have received a copy of the GNU General Public License
16 16 # along with this program; if not, write to the Free Software
17 17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 18
19 19 require File.expand_path('../../test_helper', __FILE__)
20 20
21 21 class TimelogControllerTest < ActionController::TestCase
22 22 fixtures :projects, :enabled_modules, :roles, :members,
23 23 :member_roles, :issues, :time_entries, :users,
24 24 :trackers, :enumerations, :issue_statuses,
25 25 :custom_fields, :custom_values,
26 26 :projects_trackers, :custom_fields_trackers,
27 27 :custom_fields_projects
28 28
29 29 include Redmine::I18n
30 30
31 31 def test_new
32 32 @request.session[:user_id] = 3
33 33 get :new
34 34 assert_response :success
35 35 assert_template 'new'
36 36 assert_select 'input[name=?][type=hidden]', 'project_id', 0
37 37 assert_select 'input[name=?][type=hidden]', 'issue_id', 0
38 38 assert_select 'select[name=?]', 'time_entry[project_id]' do
39 39 # blank option for project
40 40 assert_select 'option[value=""]'
41 41 end
42 42 end
43 43
44 44 def test_new_with_project_id
45 45 @request.session[:user_id] = 3
46 46 get :new, :project_id => 1
47 47 assert_response :success
48 48 assert_template 'new'
49 49 assert_select 'input[name=?][type=hidden]', 'project_id'
50 50 assert_select 'input[name=?][type=hidden]', 'issue_id', 0
51 51 assert_select 'select[name=?]', 'time_entry[project_id]', 0
52 52 end
53 53
54 54 def test_new_with_issue_id
55 55 @request.session[:user_id] = 3
56 56 get :new, :issue_id => 2
57 57 assert_response :success
58 58 assert_template 'new'
59 59 assert_select 'input[name=?][type=hidden]', 'project_id', 0
60 60 assert_select 'input[name=?][type=hidden]', 'issue_id'
61 61 assert_select 'select[name=?]', 'time_entry[project_id]', 0
62 62 end
63 63
64 64 def test_new_without_project_should_prefill_the_form
65 65 @request.session[:user_id] = 3
66 66 get :new, :time_entry => {:project_id => '1'}
67 67 assert_response :success
68 68 assert_template 'new'
69 69 assert_select 'select[name=?]', 'time_entry[project_id]' do
70 70 assert_select 'option[value="1"][selected=selected]'
71 71 end
72 72 end
73 73
74 74 def test_new_without_project_should_deny_without_permission
75 75 Role.all.each {|role| role.remove_permission! :log_time}
76 76 @request.session[:user_id] = 3
77 77
78 78 get :new
79 79 assert_response 403
80 80 end
81 81
82 82 def test_new_should_select_default_activity
83 83 @request.session[:user_id] = 3
84 84 get :new, :project_id => 1
85 85 assert_response :success
86 86 assert_select 'select[name=?]', 'time_entry[activity_id]' do
87 87 assert_select 'option[selected=selected]', :text => 'Development'
88 88 end
89 89 end
90 90
91 91 def test_new_should_only_show_active_time_entry_activities
92 92 @request.session[:user_id] = 3
93 93 get :new, :project_id => 1
94 94 assert_response :success
95 95 assert_select 'option', :text => 'Inactive Activity', :count => 0
96 96 end
97 97
98 98 def test_post_new_as_js_should_update_activity_options
99 99 @request.session[:user_id] = 3
100 100 post :new, :time_entry => {:project_id => 1}, :format => 'js'
101 101 assert_response :success
102 102 assert_include '#time_entry_activity_id', response.body
103 103 end
104 104
105 105 def test_get_edit_existing_time
106 106 @request.session[:user_id] = 2
107 107 get :edit, :id => 2, :project_id => nil
108 108 assert_response :success
109 109 assert_template 'edit'
110 110 assert_select 'form[action=?]', '/time_entries/2'
111 111 end
112 112
113 113 def test_get_edit_with_an_existing_time_entry_with_inactive_activity
114 114 te = TimeEntry.find(1)
115 115 te.activity = TimeEntryActivity.find_by_name("Inactive Activity")
116 116 te.save!(:validate => false)
117 117
118 118 @request.session[:user_id] = 1
119 119 get :edit, :project_id => 1, :id => 1
120 120 assert_response :success
121 121 assert_template 'edit'
122 122 # Blank option since nothing is pre-selected
123 123 assert_select 'option', :text => '--- Please select ---'
124 124 end
125 125
126 126 def test_post_create
127 127 @request.session[:user_id] = 3
128 128 assert_difference 'TimeEntry.count' do
129 129 post :create, :project_id => 1,
130 130 :time_entry => {:comments => 'Some work on TimelogControllerTest',
131 131 # Not the default activity
132 132 :activity_id => '11',
133 133 :spent_on => '2008-03-14',
134 134 :issue_id => '1',
135 135 :hours => '7.3'}
136 136 assert_redirected_to '/projects/ecookbook/time_entries'
137 137 end
138 138
139 139 t = TimeEntry.order('id DESC').first
140 140 assert_not_nil t
141 141 assert_equal 'Some work on TimelogControllerTest', t.comments
142 142 assert_equal 1, t.project_id
143 143 assert_equal 1, t.issue_id
144 144 assert_equal 11, t.activity_id
145 145 assert_equal 7.3, t.hours
146 146 assert_equal 3, t.user_id
147 147 end
148 148
149 149 def test_post_create_with_blank_issue
150 150 @request.session[:user_id] = 3
151 151 assert_difference 'TimeEntry.count' do
152 152 post :create, :project_id => 1,
153 153 :time_entry => {:comments => 'Some work on TimelogControllerTest',
154 154 # Not the default activity
155 155 :activity_id => '11',
156 156 :issue_id => '',
157 157 :spent_on => '2008-03-14',
158 158 :hours => '7.3'}
159 159 assert_redirected_to '/projects/ecookbook/time_entries'
160 160 end
161 161
162 162 t = TimeEntry.order('id DESC').first
163 163 assert_not_nil t
164 164 assert_equal 'Some work on TimelogControllerTest', t.comments
165 165 assert_equal 1, t.project_id
166 166 assert_nil t.issue_id
167 167 assert_equal 11, t.activity_id
168 168 assert_equal 7.3, t.hours
169 169 assert_equal 3, t.user_id
170 170 end
171 171
172 172 def test_create_on_project_with_time_tracking_disabled_should_fail
173 173 Project.find(1).disable_module! :time_tracking
174 174
175 175 @request.session[:user_id] = 2
176 176 assert_no_difference 'TimeEntry.count' do
177 177 post :create, :time_entry => {
178 178 :project_id => '1', :issue_id => '',
179 179 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
180 180 }
181 181 end
182 182 end
183 183
184 184 def test_create_on_project_without_permission_should_fail
185 185 Role.find(1).remove_permission! :log_time
186 186
187 187 @request.session[:user_id] = 2
188 188 assert_no_difference 'TimeEntry.count' do
189 189 post :create, :time_entry => {
190 190 :project_id => '1', :issue_id => '',
191 191 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
192 192 }
193 193 end
194 194 end
195 195
196 196 def test_create_on_issue_in_project_with_time_tracking_disabled_should_fail
197 197 Project.find(1).disable_module! :time_tracking
198 198
199 199 @request.session[:user_id] = 2
200 200 assert_no_difference 'TimeEntry.count' do
201 201 post :create, :time_entry => {
202 202 :project_id => '', :issue_id => '1',
203 203 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
204 204 }
205 205 assert_select_error /Issue is invalid/
206 206 end
207 207 end
208 208
209 209 def test_create_on_issue_in_project_without_permission_should_fail
210 210 Role.find(1).remove_permission! :log_time
211 211
212 212 @request.session[:user_id] = 2
213 213 assert_no_difference 'TimeEntry.count' do
214 214 post :create, :time_entry => {
215 215 :project_id => '', :issue_id => '1',
216 216 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
217 217 }
218 218 assert_select_error /Issue is invalid/
219 219 end
220 220 end
221 221
222 222 def test_create_on_issue_that_is_not_visible_should_not_disclose_subject
223 223 issue = Issue.generate!(:subject => "issue_that_is_not_visible", :is_private => true)
224 224 assert !issue.visible?(User.find(3))
225 225
226 226 @request.session[:user_id] = 3
227 227 assert_no_difference 'TimeEntry.count' do
228 228 post :create, :time_entry => {
229 229 :project_id => '', :issue_id => issue.id.to_s,
230 230 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
231 231 }
232 232 end
233 233 assert_select_error /Issue is invalid/
234 234 assert_select "input[name=?][value=?]", "time_entry[issue_id]", issue.id.to_s
235 235 assert_select "#time_entry_issue", 0
236 236 assert !response.body.include?('issue_that_is_not_visible')
237 237 end
238 238
239 239 def test_create_and_continue_at_project_level
240 240 @request.session[:user_id] = 2
241 241 assert_difference 'TimeEntry.count' do
242 242 post :create, :time_entry => {:project_id => '1',
243 243 :activity_id => '11',
244 244 :issue_id => '',
245 245 :spent_on => '2008-03-14',
246 246 :hours => '7.3'},
247 247 :continue => '1'
248 248 assert_redirected_to '/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=&time_entry%5Bproject_id%5D=1'
249 249 end
250 250 end
251 251
252 252 def test_create_and_continue_at_issue_level
253 253 @request.session[:user_id] = 2
254 254 assert_difference 'TimeEntry.count' do
255 255 post :create, :time_entry => {:project_id => '',
256 256 :activity_id => '11',
257 257 :issue_id => '1',
258 258 :spent_on => '2008-03-14',
259 259 :hours => '7.3'},
260 260 :continue => '1'
261 261 assert_redirected_to '/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=1&time_entry%5Bproject_id%5D='
262 262 end
263 263 end
264 264
265 265 def test_create_and_continue_with_project_id
266 266 @request.session[:user_id] = 2
267 267 assert_difference 'TimeEntry.count' do
268 268 post :create, :project_id => 1,
269 269 :time_entry => {:activity_id => '11',
270 270 :issue_id => '',
271 271 :spent_on => '2008-03-14',
272 272 :hours => '7.3'},
273 273 :continue => '1'
274 274 assert_redirected_to '/projects/ecookbook/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=&time_entry%5Bproject_id%5D='
275 275 end
276 276 end
277 277
278 278 def test_create_and_continue_with_issue_id
279 279 @request.session[:user_id] = 2
280 280 assert_difference 'TimeEntry.count' do
281 281 post :create, :issue_id => 1,
282 282 :time_entry => {:activity_id => '11',
283 283 :issue_id => '1',
284 284 :spent_on => '2008-03-14',
285 285 :hours => '7.3'},
286 286 :continue => '1'
287 287 assert_redirected_to '/issues/1/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=1&time_entry%5Bproject_id%5D='
288 288 end
289 289 end
290 290
291 291 def test_create_without_log_time_permission_should_be_denied
292 292 @request.session[:user_id] = 2
293 293 Role.find_by_name('Manager').remove_permission! :log_time
294 294 post :create, :project_id => 1,
295 295 :time_entry => {:activity_id => '11',
296 296 :issue_id => '',
297 297 :spent_on => '2008-03-14',
298 298 :hours => '7.3'}
299 299
300 300 assert_response 403
301 301 end
302 302
303 303 def test_create_without_project_and_issue_should_fail
304 304 @request.session[:user_id] = 2
305 305 post :create, :time_entry => {:issue_id => ''}
306 306
307 307 assert_response :success
308 308 assert_template 'new'
309 309 end
310 310
311 311 def test_create_with_failure
312 312 @request.session[:user_id] = 2
313 313 post :create, :project_id => 1,
314 314 :time_entry => {:activity_id => '',
315 315 :issue_id => '',
316 316 :spent_on => '2008-03-14',
317 317 :hours => '7.3'}
318 318
319 319 assert_response :success
320 320 assert_template 'new'
321 321 end
322 322
323 323 def test_create_without_project
324 324 @request.session[:user_id] = 2
325 325 assert_difference 'TimeEntry.count' do
326 326 post :create, :time_entry => {:project_id => '1',
327 327 :activity_id => '11',
328 328 :issue_id => '',
329 329 :spent_on => '2008-03-14',
330 330 :hours => '7.3'}
331 331 end
332 332
333 333 assert_redirected_to '/projects/ecookbook/time_entries'
334 334 time_entry = TimeEntry.order('id DESC').first
335 335 assert_equal 1, time_entry.project_id
336 336 end
337 337
338 338 def test_create_without_project_should_fail_with_issue_not_inside_project
339 339 @request.session[:user_id] = 2
340 340 assert_no_difference 'TimeEntry.count' do
341 341 post :create, :time_entry => {:project_id => '1',
342 342 :activity_id => '11',
343 343 :issue_id => '5',
344 344 :spent_on => '2008-03-14',
345 345 :hours => '7.3'}
346 346 end
347 347
348 348 assert_response :success
349 349 assert assigns(:time_entry).errors[:issue_id].present?
350 350 end
351 351
352 352 def test_create_without_project_should_deny_without_permission
353 353 @request.session[:user_id] = 2
354 354 Project.find(3).disable_module!(:time_tracking)
355 355
356 356 assert_no_difference 'TimeEntry.count' do
357 357 post :create, :time_entry => {:project_id => '3',
358 358 :activity_id => '11',
359 359 :issue_id => '',
360 360 :spent_on => '2008-03-14',
361 361 :hours => '7.3'}
362 362 end
363 363
364 364 assert_response 403
365 365 end
366 366
367 367 def test_create_without_project_with_failure
368 368 @request.session[:user_id] = 2
369 369 assert_no_difference 'TimeEntry.count' do
370 370 post :create, :time_entry => {:project_id => '1',
371 371 :activity_id => '11',
372 372 :issue_id => '',
373 373 :spent_on => '2008-03-14',
374 374 :hours => ''}
375 375 end
376 376
377 377 assert_response :success
378 378 assert_select 'select[name=?]', 'time_entry[project_id]' do
379 379 assert_select 'option[value="1"][selected=selected]'
380 380 end
381 381 end
382 382
383 383 def test_update
384 384 entry = TimeEntry.find(1)
385 385 assert_equal 1, entry.issue_id
386 386 assert_equal 2, entry.user_id
387 387
388 388 @request.session[:user_id] = 1
389 389 put :update, :id => 1,
390 390 :time_entry => {:issue_id => '2',
391 391 :hours => '8'}
392 392 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
393 393 entry.reload
394 394
395 395 assert_equal 8, entry.hours
396 396 assert_equal 2, entry.issue_id
397 397 assert_equal 2, entry.user_id
398 398 end
399 399
400 400 def test_update_should_allow_to_change_issue_to_another_project
401 401 entry = TimeEntry.generate!(:issue_id => 1)
402 402
403 403 @request.session[:user_id] = 1
404 404 put :update, :id => entry.id, :time_entry => {:issue_id => '5'}
405 405 assert_response 302
406 406 entry.reload
407 407
408 408 assert_equal 5, entry.issue_id
409 409 assert_equal 3, entry.project_id
410 410 end
411 411
412 412 def test_update_should_not_allow_to_change_issue_to_an_invalid_project
413 413 entry = TimeEntry.generate!(:issue_id => 1)
414 414 Project.find(3).disable_module!(:time_tracking)
415 415
416 416 @request.session[:user_id] = 1
417 417 put :update, :id => entry.id, :time_entry => {:issue_id => '5'}
418 418 assert_response 200
419 419 assert_include "Issue is invalid", assigns(:time_entry).errors.full_messages
420 420 end
421 421
422 422 def test_get_bulk_edit
423 423 @request.session[:user_id] = 2
424 424 get :bulk_edit, :ids => [1, 2]
425 425 assert_response :success
426 426 assert_template 'bulk_edit'
427 427
428 428 assert_select 'ul#bulk-selection' do
429 429 assert_select 'li', 2
430 430 assert_select 'li a', :text => '03/23/2007 - eCookbook: 4.25 hours'
431 431 end
432 432
433 433 assert_select 'form#bulk_edit_form[action=?]', '/time_entries/bulk_update' do
434 434 # System wide custom field
435 435 assert_select 'select[name=?]', 'time_entry[custom_field_values][10]'
436 436
437 437 # Activities
438 438 assert_select 'select[name=?]', 'time_entry[activity_id]' do
439 439 assert_select 'option[value=""]', :text => '(No change)'
440 440 assert_select 'option[value="9"]', :text => 'Design'
441 441 end
442 442 end
443 443 end
444 444
445 445 def test_get_bulk_edit_on_different_projects
446 446 @request.session[:user_id] = 2
447 447 get :bulk_edit, :ids => [1, 2, 6]
448 448 assert_response :success
449 449 assert_template 'bulk_edit'
450 450 end
451 451
452 452 def test_bulk_edit_with_edit_own_time_entries_permission
453 453 @request.session[:user_id] = 2
454 454 Role.find_by_name('Manager').remove_permission! :edit_time_entries
455 455 Role.find_by_name('Manager').add_permission! :edit_own_time_entries
456 456 ids = (0..1).map {TimeEntry.generate!(:user => User.find(2)).id}
457 457
458 458 get :bulk_edit, :ids => ids
459 459 assert_response :success
460 460 end
461 461
462 462 def test_bulk_update
463 463 @request.session[:user_id] = 2
464 464 # update time entry activity
465 465 post :bulk_update, :ids => [1, 2], :time_entry => { :activity_id => 9}
466 466
467 467 assert_response 302
468 468 # check that the issues were updated
469 469 assert_equal [9, 9], TimeEntry.where(:id => [1, 2]).collect {|i| i.activity_id}
470 470 end
471 471
472 472 def test_bulk_update_with_failure
473 473 @request.session[:user_id] = 2
474 474 post :bulk_update, :ids => [1, 2], :time_entry => { :hours => 'A'}
475 475
476 476 assert_response 302
477 477 assert_match /Failed to save 2 time entrie/, flash[:error]
478 478 end
479 479
480 480 def test_bulk_update_on_different_projects
481 481 @request.session[:user_id] = 2
482 482 # makes user a manager on the other project
483 483 Member.create!(:user_id => 2, :project_id => 3, :role_ids => [1])
484 484
485 485 # update time entry activity
486 486 post :bulk_update, :ids => [1, 2, 4], :time_entry => { :activity_id => 9 }
487 487
488 488 assert_response 302
489 489 # check that the issues were updated
490 490 assert_equal [9, 9, 9], TimeEntry.where(:id => [1, 2, 4]).collect {|i| i.activity_id}
491 491 end
492 492
493 493 def test_bulk_update_on_different_projects_without_rights
494 494 @request.session[:user_id] = 3
495 495 user = User.find(3)
496 496 action = { :controller => "timelog", :action => "bulk_update" }
497 497 assert user.allowed_to?(action, TimeEntry.find(1).project)
498 498 assert ! user.allowed_to?(action, TimeEntry.find(5).project)
499 499 post :bulk_update, :ids => [1, 5], :time_entry => { :activity_id => 9 }
500 500 assert_response 403
501 501 end
502 502
503 503 def test_bulk_update_with_edit_own_time_entries_permission
504 504 @request.session[:user_id] = 2
505 505 Role.find_by_name('Manager').remove_permission! :edit_time_entries
506 506 Role.find_by_name('Manager').add_permission! :edit_own_time_entries
507 507 ids = (0..1).map {TimeEntry.generate!(:user => User.find(2)).id}
508 508
509 509 post :bulk_update, :ids => ids, :time_entry => { :activity_id => 9 }
510 510 assert_response 302
511 511 end
512 512
513 513 def test_bulk_update_with_edit_own_time_entries_permissions_should_be_denied_for_time_entries_of_other_user
514 514 @request.session[:user_id] = 2
515 515 Role.find_by_name('Manager').remove_permission! :edit_time_entries
516 516 Role.find_by_name('Manager').add_permission! :edit_own_time_entries
517 517
518 518 post :bulk_update, :ids => [1, 2], :time_entry => { :activity_id => 9 }
519 519 assert_response 403
520 520 end
521 521
522 522 def test_bulk_update_custom_field
523 523 @request.session[:user_id] = 2
524 524 post :bulk_update, :ids => [1, 2], :time_entry => { :custom_field_values => {'10' => '0'} }
525 525
526 526 assert_response 302
527 527 assert_equal ["0", "0"], TimeEntry.where(:id => [1, 2]).collect {|i| i.custom_value_for(10).value}
528 528 end
529 529
530 530 def test_bulk_update_clear_custom_field
531 531 field = TimeEntryCustomField.generate!(:field_format => 'string')
532 532 @request.session[:user_id] = 2
533 533 post :bulk_update, :ids => [1, 2], :time_entry => { :custom_field_values => {field.id.to_s => '__none__'} }
534 534
535 535 assert_response 302
536 536 assert_equal ["", ""], TimeEntry.where(:id => [1, 2]).collect {|i| i.custom_value_for(field).value}
537 537 end
538 538
539 539 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
540 540 @request.session[:user_id] = 2
541 541 post :bulk_update, :ids => [1,2], :back_url => '/time_entries'
542 542
543 543 assert_response :redirect
544 544 assert_redirected_to '/time_entries'
545 545 end
546 546
547 547 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
548 548 @request.session[:user_id] = 2
549 549 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
550 550
551 551 assert_response :redirect
552 552 assert_redirected_to :controller => 'timelog', :action => 'index', :project_id => Project.find(1).identifier
553 553 end
554 554
555 555 def test_post_bulk_update_without_edit_permission_should_be_denied
556 556 @request.session[:user_id] = 2
557 557 Role.find_by_name('Manager').remove_permission! :edit_time_entries
558 558 post :bulk_update, :ids => [1,2]
559 559
560 560 assert_response 403
561 561 end
562 562
563 563 def test_destroy
564 564 @request.session[:user_id] = 2
565 565 delete :destroy, :id => 1
566 566 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
567 567 assert_equal I18n.t(:notice_successful_delete), flash[:notice]
568 568 assert_nil TimeEntry.find_by_id(1)
569 569 end
570 570
571 571 def test_destroy_should_fail
572 572 # simulate that this fails (e.g. due to a plugin), see #5700
573 573 TimeEntry.any_instance.expects(:destroy).returns(false)
574 574
575 575 @request.session[:user_id] = 2
576 576 delete :destroy, :id => 1
577 577 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
578 578 assert_equal I18n.t(:notice_unable_delete_time_entry), flash[:error]
579 579 assert_not_nil TimeEntry.find_by_id(1)
580 580 end
581 581
582 582 def test_index_all_projects
583 583 get :index
584 584 assert_response :success
585 585 assert_template 'index'
586 586 assert_not_nil assigns(:total_hours)
587 587 assert_equal "162.90", "%.2f" % assigns(:total_hours)
588 588 assert_select 'form#query_form[action=?]', '/time_entries'
589 589 end
590 590
591 591 def test_index_all_projects_should_show_log_time_link
592 592 @request.session[:user_id] = 2
593 593 get :index
594 594 assert_response :success
595 595 assert_template 'index'
596 596 assert_select 'a[href=?]', '/time_entries/new', :text => /Log time/
597 597 end
598 598
599 599 def test_index_my_spent_time
600 600 @request.session[:user_id] = 2
601 601 get :index, :user_id => 'me'
602 602 assert_response :success
603 603 assert_template 'index'
604 604 assert assigns(:entries).all? {|entry| entry.user_id == 2}
605 605 end
606 606
607 607 def test_index_at_project_level
608 608 get :index, :project_id => 'ecookbook'
609 609 assert_response :success
610 610 assert_template 'index'
611 611 assert_not_nil assigns(:entries)
612 612 assert_equal 4, assigns(:entries).size
613 613 # project and subproject
614 614 assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
615 615 assert_not_nil assigns(:total_hours)
616 616 assert_equal "162.90", "%.2f" % assigns(:total_hours)
617 617 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
618 618 end
619 619
620 620 def test_index_with_display_subprojects_issues_to_false_should_not_include_subproject_entries
621 621 entry = TimeEntry.generate!(:project => Project.find(3))
622 622
623 623 with_settings :display_subprojects_issues => '0' do
624 624 get :index, :project_id => 'ecookbook'
625 625 assert_response :success
626 626 assert_template 'index'
627 627 assert_not_include entry, assigns(:entries)
628 628 end
629 629 end
630 630
631 631 def test_index_with_display_subprojects_issues_to_false_and_subproject_filter_should_include_subproject_entries
632 632 entry = TimeEntry.generate!(:project => Project.find(3))
633 633
634 634 with_settings :display_subprojects_issues => '0' do
635 635 get :index, :project_id => 'ecookbook', :subproject_id => 3
636 636 assert_response :success
637 637 assert_template 'index'
638 638 assert_include entry, assigns(:entries)
639 639 end
640 640 end
641 641
642 642 def test_index_at_project_level_with_date_range
643 643 get :index, :project_id => 'ecookbook',
644 644 :f => ['spent_on'],
645 645 :op => {'spent_on' => '><'},
646 646 :v => {'spent_on' => ['2007-03-20', '2007-04-30']}
647 647 assert_response :success
648 648 assert_template 'index'
649 649 assert_not_nil assigns(:entries)
650 650 assert_equal 3, assigns(:entries).size
651 651 assert_not_nil assigns(:total_hours)
652 652 assert_equal "12.90", "%.2f" % assigns(:total_hours)
653 653 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
654 654 end
655 655
656 656 def test_index_at_project_level_with_date_range_using_from_and_to_params
657 657 get :index, :project_id => 'ecookbook', :from => '2007-03-20', :to => '2007-04-30'
658 658 assert_response :success
659 659 assert_template 'index'
660 660 assert_not_nil assigns(:entries)
661 661 assert_equal 3, assigns(:entries).size
662 662 assert_not_nil assigns(:total_hours)
663 663 assert_equal "12.90", "%.2f" % assigns(:total_hours)
664 664 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
665 665 end
666 666
667 667 def test_index_at_project_level_with_period
668 668 get :index, :project_id => 'ecookbook',
669 669 :f => ['spent_on'],
670 670 :op => {'spent_on' => '>t-'},
671 671 :v => {'spent_on' => ['7']}
672 672 assert_response :success
673 673 assert_template 'index'
674 674 assert_not_nil assigns(:entries)
675 675 assert_not_nil assigns(:total_hours)
676 676 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
677 677 end
678 678
679 679 def test_index_at_issue_level
680 680 get :index, :issue_id => 1
681 681 assert_response :success
682 682 assert_template 'index'
683 683 assert_not_nil assigns(:entries)
684 684 assert_equal 2, assigns(:entries).size
685 685 assert_not_nil assigns(:total_hours)
686 686 assert_equal 154.25, assigns(:total_hours)
687 687 # display all time
688 688 assert_nil assigns(:from)
689 689 assert_nil assigns(:to)
690 690 assert_select 'form#query_form[action=?]', '/issues/1/time_entries'
691 691 end
692 692
693 693 def test_index_should_sort_by_spent_on_and_created_on
694 694 t1 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:00:00', :activity_id => 10)
695 695 t2 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:05:00', :activity_id => 10)
696 696 t3 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-15', :created_on => '2012-06-16 20:10:00', :activity_id => 10)
697 697
698 698 get :index, :project_id => 1,
699 699 :f => ['spent_on'],
700 700 :op => {'spent_on' => '><'},
701 701 :v => {'spent_on' => ['2012-06-15', '2012-06-16']}
702 702 assert_response :success
703 703 assert_equal [t2, t1, t3], assigns(:entries)
704 704
705 705 get :index, :project_id => 1,
706 706 :f => ['spent_on'],
707 707 :op => {'spent_on' => '><'},
708 708 :v => {'spent_on' => ['2012-06-15', '2012-06-16']},
709 709 :sort => 'spent_on'
710 710 assert_response :success
711 711 assert_equal [t3, t1, t2], assigns(:entries)
712 712 end
713 713
714 714 def test_index_with_filter_on_issue_custom_field
715 715 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'})
716 716 entry = TimeEntry.generate!(:issue => issue, :hours => 2.5)
717 717
718 718 get :index, :f => ['issue.cf_2'], :op => {'issue.cf_2' => '='}, :v => {'issue.cf_2' => ['filter_on_issue_custom_field']}
719 719 assert_response :success
720 720 assert_equal [entry], assigns(:entries)
721 721 end
722 722
723 723 def test_index_with_issue_custom_field_column
724 724 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'})
725 725 entry = TimeEntry.generate!(:issue => issue, :hours => 2.5)
726 726
727 727 get :index, :c => %w(project spent_on issue comments hours issue.cf_2)
728 728 assert_response :success
729 729 assert_include :'issue.cf_2', assigns(:query).column_names
730 730 assert_select 'td.issue_cf_2', :text => 'filter_on_issue_custom_field'
731 731 end
732 732
733 733 def test_index_with_time_entry_custom_field_column
734 734 field = TimeEntryCustomField.generate!(:field_format => 'string')
735 735 entry = TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value'})
736 736 field_name = "cf_#{field.id}"
737 737
738 738 get :index, :c => ["hours", field_name]
739 739 assert_response :success
740 740 assert_include field_name.to_sym, assigns(:query).column_names
741 741 assert_select "td.#{field_name}", :text => 'CF Value'
742 742 end
743 743
744 744 def test_index_with_time_entry_custom_field_sorting
745 745 field = TimeEntryCustomField.generate!(:field_format => 'string', :name => 'String Field')
746 746 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 1'})
747 747 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 3'})
748 748 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 2'})
749 749 field_name = "cf_#{field.id}"
750 750
751 751 get :index, :c => ["hours", field_name], :sort => field_name
752 752 assert_response :success
753 753 assert_include field_name.to_sym, assigns(:query).column_names
754 754 assert_select "th a.sort", :text => 'String Field'
755 755
756 756 # Make sure that values are properly sorted
757 757 values = assigns(:entries).map {|e| e.custom_field_value(field)}.compact
758 758 assert_equal 3, values.size
759 759 assert_equal values.sort, values
760 760 end
761 761
762 def test_index_with_query
763 query = TimeEntryQuery.new(:project_id => 1, :name => 'Time Entry Query', :visibility => 2)
764 query.save!
765 @request.session[:user_id] = 2
766
767 get :index, :project_id => 'ecookbook', :query_id => query.id
768 assert_response :success
769 assert_select 'h2', :text => query.name
770 end
771
762 772 def test_index_atom_feed
763 773 get :index, :project_id => 1, :format => 'atom'
764 774 assert_response :success
765 775 assert_equal 'application/atom+xml', @response.content_type
766 776 assert_not_nil assigns(:items)
767 777 assert assigns(:items).first.is_a?(TimeEntry)
768 778 end
769 779
770 780 def test_index_at_project_level_should_include_csv_export_dialog
771 781 get :index, :project_id => 'ecookbook',
772 782 :f => ['spent_on'],
773 783 :op => {'spent_on' => '>='},
774 784 :v => {'spent_on' => ['2007-04-01']},
775 785 :c => ['spent_on', 'user']
776 786 assert_response :success
777 787
778 788 assert_select '#csv-export-options' do
779 789 assert_select 'form[action=?][method=get]', '/projects/ecookbook/time_entries.csv' do
780 790 # filter
781 791 assert_select 'input[name=?][value=?]', 'f[]', 'spent_on'
782 792 assert_select 'input[name=?][value=?]', 'op[spent_on]', '>='
783 793 assert_select 'input[name=?][value=?]', 'v[spent_on][]', '2007-04-01'
784 794 # columns
785 795 assert_select 'input[name=?][value=?]', 'c[]', 'spent_on'
786 796 assert_select 'input[name=?][value=?]', 'c[]', 'user'
787 797 assert_select 'input[name=?]', 'c[]', 2
788 798 end
789 799 end
790 800 end
791 801
792 802 def test_index_cross_project_should_include_csv_export_dialog
793 803 get :index
794 804 assert_response :success
795 805
796 806 assert_select '#csv-export-options' do
797 807 assert_select 'form[action=?][method=get]', '/time_entries.csv'
798 808 end
799 809 end
800 810
801 811 def test_index_at_issue_level_should_include_csv_export_dialog
802 812 get :index, :issue_id => 3
803 813 assert_response :success
804 814
805 815 assert_select '#csv-export-options' do
806 816 assert_select 'form[action=?][method=get]', '/issues/3/time_entries.csv'
807 817 end
808 818 end
809 819
810 820 def test_index_csv_all_projects
811 821 with_settings :date_format => '%m/%d/%Y' do
812 822 get :index, :format => 'csv'
813 823 assert_response :success
814 824 assert_equal 'text/csv; header=present', response.content_type
815 825 end
816 826 end
817 827
818 828 def test_index_csv
819 829 with_settings :date_format => '%m/%d/%Y' do
820 830 get :index, :project_id => 1, :format => 'csv'
821 831 assert_response :success
822 832 assert_equal 'text/csv; header=present', response.content_type
823 833 end
824 834 end
825 835
826 836 def test_index_csv_should_fill_issue_column_with_tracker_id_and_subject
827 837 issue = Issue.find(1)
828 838 entry = TimeEntry.generate!(:issue => issue, :comments => "Issue column content test")
829 839
830 840 get :index, :format => 'csv'
831 841 line = response.body.split("\n").detect {|l| l.include?(entry.comments)}
832 842 assert_not_nil line
833 843 assert_include "#{issue.tracker} #1: #{issue.subject}", line
834 844 end
835 845 end
General Comments 0
You need to be logged in to leave comments. Login now