##// END OF EJS Templates
Propagates time tracking to the parent project (closes #433). Time report enhancements....
Jean-Philippe Lang -
r1162:200842ba5e75
parent child
Show More
@@ -0,0 +1,41
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 class ARCondition
19 attr_reader :conditions
20
21 def initialize(condition=nil)
22 @conditions = ['1=1']
23 @conditions.add(condition) if condition
24 end
25
26 def add(condition)
27 if condition.is_a?(Array)
28 @conditions.first << " AND (#{condition.first})"
29 @conditions += condition[1..-1]
30 elsif condition.is_a?(String)
31 @conditions.first << " AND (#{condition})"
32 else
33 raise "Unsupported #{condition.class} condition: #{condition}"
34 end
35 self
36 end
37
38 def <<(condition)
39 add(condition)
40 end
41 end
1 NO CONTENT: new file 100644, binary diff hidden
@@ -1,391 +1,395
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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 ProjectsController < ApplicationController
19 19 layout 'base'
20 20 menu_item :overview
21 21 menu_item :activity, :only => :activity
22 22 menu_item :roadmap, :only => :roadmap
23 23 menu_item :files, :only => [:list_files, :add_file]
24 24 menu_item :settings, :only => :settings
25 25 menu_item :issues, :only => [:changelog]
26 26
27 27 before_filter :find_project, :except => [ :index, :list, :add ]
28 28 before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy ]
29 29 before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
30 30 accept_key_auth :activity, :calendar
31 31
32 32 helper :sort
33 33 include SortHelper
34 34 helper :custom_fields
35 35 include CustomFieldsHelper
36 36 helper :ifpdf
37 37 include IfpdfHelper
38 38 helper :issues
39 39 helper IssuesHelper
40 40 helper :queries
41 41 include QueriesHelper
42 42 helper :repositories
43 43 include RepositoriesHelper
44 44 include ProjectsHelper
45 45
46 46 def index
47 47 list
48 48 render :action => 'list' unless request.xhr?
49 49 end
50 50
51 51 # Lists visible projects
52 52 def list
53 53 projects = Project.find :all,
54 54 :conditions => Project.visible_by(User.current),
55 55 :include => :parent
56 56 @project_tree = projects.group_by {|p| p.parent || p}
57 57 @project_tree.each_key {|p| @project_tree[p] -= [p]}
58 58 end
59 59
60 60 # Add a new project
61 61 def add
62 62 @custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
63 63 @trackers = Tracker.all
64 64 @root_projects = Project.find(:all,
65 65 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
66 66 :order => 'name')
67 67 @project = Project.new(params[:project])
68 68 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
69 69 if request.get?
70 70 @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project) }
71 71 @project.trackers = Tracker.all
72 72 else
73 73 @project.custom_fields = CustomField.find(params[:custom_field_ids]) if params[:custom_field_ids]
74 74 @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) }
75 75 @project.custom_values = @custom_values
76 76 if @project.save
77 77 @project.enabled_module_names = params[:enabled_modules]
78 78 flash[:notice] = l(:notice_successful_create)
79 79 redirect_to :controller => 'admin', :action => 'projects'
80 80 end
81 81 end
82 82 end
83 83
84 84 # Show @project
85 85 def show
86 86 @custom_values = @project.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position")
87 87 @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
88 88 @subprojects = @project.active_children
89 89 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
90 90 @trackers = @project.trackers
91 91 @open_issues_by_tracker = Issue.count(:group => :tracker, :joins => "INNER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id", :conditions => ["project_id=? and #{IssueStatus.table_name}.is_closed=?", @project.id, false])
92 92 @total_issues_by_tracker = Issue.count(:group => :tracker, :conditions => ["project_id=?", @project.id])
93 @total_hours = @project.time_entries.sum(:hours)
93 TimeEntry.visible_by(User.current) do
94 @total_hours = TimeEntry.sum(:hours,
95 :include => :project,
96 :conditions => ["(#{Project.table_name}.id = ? OR #{Project.table_name}.parent_id = ?)", @project.id, @project.id]).to_f
97 end
94 98 @key = User.current.rss_key
95 99 end
96 100
97 101 def settings
98 102 @root_projects = Project.find(:all,
99 103 :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id],
100 104 :order => 'name')
101 105 @custom_fields = IssueCustomField.find(:all)
102 106 @issue_category ||= IssueCategory.new
103 107 @member ||= @project.members.new
104 108 @trackers = Tracker.all
105 109 @custom_values ||= ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @project.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) }
106 110 @repository ||= @project.repository
107 111 @wiki ||= @project.wiki
108 112 end
109 113
110 114 # Edit @project
111 115 def edit
112 116 if request.post?
113 117 if params[:custom_fields]
114 118 @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) }
115 119 @project.custom_values = @custom_values
116 120 end
117 121 @project.attributes = params[:project]
118 122 if @project.save
119 123 flash[:notice] = l(:notice_successful_update)
120 124 redirect_to :action => 'settings', :id => @project
121 125 else
122 126 settings
123 127 render :action => 'settings'
124 128 end
125 129 end
126 130 end
127 131
128 132 def modules
129 133 @project.enabled_module_names = params[:enabled_modules]
130 134 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
131 135 end
132 136
133 137 def archive
134 138 @project.archive if request.post? && @project.active?
135 139 redirect_to :controller => 'admin', :action => 'projects'
136 140 end
137 141
138 142 def unarchive
139 143 @project.unarchive if request.post? && !@project.active?
140 144 redirect_to :controller => 'admin', :action => 'projects'
141 145 end
142 146
143 147 # Delete @project
144 148 def destroy
145 149 @project_to_destroy = @project
146 150 if request.post? and params[:confirm]
147 151 @project_to_destroy.destroy
148 152 redirect_to :controller => 'admin', :action => 'projects'
149 153 end
150 154 # hide project in layout
151 155 @project = nil
152 156 end
153 157
154 158 # Add a new issue category to @project
155 159 def add_issue_category
156 160 @category = @project.issue_categories.build(params[:category])
157 161 if request.post? and @category.save
158 162 respond_to do |format|
159 163 format.html do
160 164 flash[:notice] = l(:notice_successful_create)
161 165 redirect_to :action => 'settings', :tab => 'categories', :id => @project
162 166 end
163 167 format.js do
164 168 # IE doesn't support the replace_html rjs method for select box options
165 169 render(:update) {|page| page.replace "issue_category_id",
166 170 content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
167 171 }
168 172 end
169 173 end
170 174 end
171 175 end
172 176
173 177 # Add a new version to @project
174 178 def add_version
175 179 @version = @project.versions.build(params[:version])
176 180 if request.post? and @version.save
177 181 flash[:notice] = l(:notice_successful_create)
178 182 redirect_to :action => 'settings', :tab => 'versions', :id => @project
179 183 end
180 184 end
181 185
182 186 def add_file
183 187 if request.post?
184 188 @version = @project.versions.find_by_id(params[:version_id])
185 189 attachments = attach_files(@version, params[:attachments])
186 190 Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('file_added')
187 191 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
188 192 end
189 193 @versions = @project.versions.sort
190 194 end
191 195
192 196 def list_files
193 197 @versions = @project.versions.sort
194 198 end
195 199
196 200 # Show changelog for @project
197 201 def changelog
198 202 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
199 203 retrieve_selected_tracker_ids(@trackers)
200 204 @versions = @project.versions.sort
201 205 end
202 206
203 207 def roadmap
204 208 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
205 209 retrieve_selected_tracker_ids(@trackers)
206 210 @versions = @project.versions.sort
207 211 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
208 212 end
209 213
210 214 def activity
211 215 if params[:year] and params[:year].to_i > 1900
212 216 @year = params[:year].to_i
213 217 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
214 218 @month = params[:month].to_i
215 219 end
216 220 end
217 221 @year ||= Date.today.year
218 222 @month ||= Date.today.month
219 223
220 224 case params[:format]
221 225 when 'atom'
222 226 # 30 last days
223 227 @date_from = Date.today - 30
224 228 @date_to = Date.today + 1
225 229 else
226 230 # current month
227 231 @date_from = Date.civil(@year, @month, 1)
228 232 @date_to = @date_from >> 1
229 233 end
230 234
231 235 @event_types = %w(issues news files documents changesets wiki_pages messages)
232 236 @event_types.delete('wiki_pages') unless @project.wiki
233 237 @event_types.delete('changesets') unless @project.repository
234 238 @event_types.delete('messages') unless @project.boards.any?
235 239 # only show what the user is allowed to view
236 240 @event_types = @event_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)}
237 241
238 242 @scope = @event_types.select {|t| params["show_#{t}"]}
239 243 # default events if none is specified in parameters
240 244 @scope = (@event_types - %w(wiki_pages messages))if @scope.empty?
241 245
242 246 @events = []
243 247
244 248 if @scope.include?('issues')
245 249 @events += @project.issues.find(:all, :include => [:author, :tracker], :conditions => ["#{Issue.table_name}.created_on>=? and #{Issue.table_name}.created_on<=?", @date_from, @date_to] )
246 250 @events += @project.issues_status_changes(@date_from, @date_to)
247 251 end
248 252
249 253 if @scope.include?('news')
250 254 @events += @project.news.find(:all, :conditions => ["#{News.table_name}.created_on>=? and #{News.table_name}.created_on<=?", @date_from, @date_to], :include => :author )
251 255 end
252 256
253 257 if @scope.include?('files')
254 258 @events += Attachment.find(:all, :select => "#{Attachment.table_name}.*", :joins => "LEFT JOIN #{Version.table_name} ON #{Version.table_name}.id = #{Attachment.table_name}.container_id", :conditions => ["#{Attachment.table_name}.container_type='Version' and #{Version.table_name}.project_id=? and #{Attachment.table_name}.created_on>=? and #{Attachment.table_name}.created_on<=?", @project.id, @date_from, @date_to], :include => :author )
255 259 end
256 260
257 261 if @scope.include?('documents')
258 262 @events += @project.documents.find(:all, :conditions => ["#{Document.table_name}.created_on>=? and #{Document.table_name}.created_on<=?", @date_from, @date_to] )
259 263 @events += Attachment.find(:all, :select => "attachments.*", :joins => "LEFT JOIN #{Document.table_name} ON #{Document.table_name}.id = #{Attachment.table_name}.container_id", :conditions => ["#{Attachment.table_name}.container_type='Document' and #{Document.table_name}.project_id=? and #{Attachment.table_name}.created_on>=? and #{Attachment.table_name}.created_on<=?", @project.id, @date_from, @date_to], :include => :author )
260 264 end
261 265
262 266 if @scope.include?('wiki_pages')
263 267 select = "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
264 268 "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
265 269 "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +
266 270 "#{WikiContent.versioned_table_name}.id"
267 271 joins = "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " +
268 272 "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id "
269 273 conditions = ["#{Wiki.table_name}.project_id = ? AND #{WikiContent.versioned_table_name}.updated_on BETWEEN ? AND ?",
270 274 @project.id, @date_from, @date_to]
271 275
272 276 @events += WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => conditions)
273 277 end
274 278
275 279 if @scope.include?('changesets')
276 280 @events += Changeset.find(:all, :include => :repository, :conditions => ["#{Repository.table_name}.project_id = ? AND #{Changeset.table_name}.committed_on BETWEEN ? AND ?", @project.id, @date_from, @date_to])
277 281 end
278 282
279 283 if @scope.include?('messages')
280 284 @events += Message.find(:all,
281 285 :include => [:board, :author],
282 286 :conditions => ["#{Board.table_name}.project_id=? AND #{Message.table_name}.parent_id IS NULL AND #{Message.table_name}.created_on BETWEEN ? AND ?", @project.id, @date_from, @date_to])
283 287 end
284 288
285 289 @events_by_day = @events.group_by(&:event_date)
286 290
287 291 respond_to do |format|
288 292 format.html { render :layout => false if request.xhr? }
289 293 format.atom { render_feed(@events, :title => "#{@project.name}: #{l(:label_activity)}") }
290 294 end
291 295 end
292 296
293 297 def calendar
294 298 @trackers = @project.rolled_up_trackers
295 299 retrieve_selected_tracker_ids(@trackers)
296 300
297 301 if params[:year] and params[:year].to_i > 1900
298 302 @year = params[:year].to_i
299 303 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
300 304 @month = params[:month].to_i
301 305 end
302 306 end
303 307 @year ||= Date.today.year
304 308 @month ||= Date.today.month
305 309 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
306 310
307 311 events = []
308 312 @project.issues_with_subprojects(params[:with_subprojects]) do
309 313 events += Issue.find(:all,
310 314 :include => [:tracker, :status, :assigned_to, :priority, :project],
311 315 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?)) AND #{Issue.table_name}.tracker_id IN (#{@selected_tracker_ids.join(',')})", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
312 316 ) unless @selected_tracker_ids.empty?
313 317 end
314 318 events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
315 319 @calendar.events = events
316 320
317 321 render :layout => false if request.xhr?
318 322 end
319 323
320 324 def gantt
321 325 @trackers = @project.rolled_up_trackers
322 326 retrieve_selected_tracker_ids(@trackers)
323 327
324 328 if params[:year] and params[:year].to_i >0
325 329 @year_from = params[:year].to_i
326 330 if params[:month] and params[:month].to_i >=1 and params[:month].to_i <= 12
327 331 @month_from = params[:month].to_i
328 332 else
329 333 @month_from = 1
330 334 end
331 335 else
332 336 @month_from ||= Date.today.month
333 337 @year_from ||= Date.today.year
334 338 end
335 339
336 340 zoom = (params[:zoom] || User.current.pref[:gantt_zoom]).to_i
337 341 @zoom = (zoom > 0 && zoom < 5) ? zoom : 2
338 342 months = (params[:months] || User.current.pref[:gantt_months]).to_i
339 343 @months = (months > 0 && months < 25) ? months : 6
340 344
341 345 # Save gantt paramters as user preference (zoom and months count)
342 346 if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || @months != User.current.pref[:gantt_months]))
343 347 User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
344 348 User.current.preference.save
345 349 end
346 350
347 351 @date_from = Date.civil(@year_from, @month_from, 1)
348 352 @date_to = (@date_from >> @months) - 1
349 353
350 354 @events = []
351 355 @project.issues_with_subprojects(params[:with_subprojects]) do
352 356 @events += Issue.find(:all,
353 357 :order => "start_date, due_date",
354 358 :include => [:tracker, :status, :assigned_to, :priority, :project],
355 359 :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to]
356 360 ) unless @selected_tracker_ids.empty?
357 361 end
358 362 @events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
359 363 @events.sort! {|x,y| x.start_date <=> y.start_date }
360 364
361 365 if params[:format]=='pdf'
362 366 @options_for_rfpdf ||= {}
363 367 @options_for_rfpdf[:file_name] = "#{@project.identifier}-gantt.pdf"
364 368 render :template => "projects/gantt.rfpdf", :layout => false
365 369 elsif params[:format]=='png' && respond_to?('gantt_image')
366 370 image = gantt_image(@events, @date_from, @months, @zoom)
367 371 image.format = 'PNG'
368 372 send_data(image.to_blob, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png")
369 373 else
370 374 render :template => "projects/gantt.rhtml"
371 375 end
372 376 end
373 377
374 378 private
375 379 # Find project of id params[:id]
376 380 # if not found, redirect to project list
377 381 # Used as a before_filter
378 382 def find_project
379 383 @project = Project.find(params[:id])
380 384 rescue ActiveRecord::RecordNotFound
381 385 render_404
382 386 end
383 387
384 388 def retrieve_selected_tracker_ids(selectable_trackers)
385 389 if ids = params[:tracker_ids]
386 390 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
387 391 else
388 392 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
389 393 end
390 394 end
391 395 end
@@ -1,215 +1,227
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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 layout 'base'
20 20 menu_item :issues
21 21 before_filter :find_project, :authorize
22 22
23 23 helper :sort
24 24 include SortHelper
25 25 helper :issues
26 26 include TimelogHelper
27 27
28 28 def report
29 @available_criterias = { 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
30 :values => @project.versions,
29 @available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
30 :klass => Project,
31 :label => :label_project},
32 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
33 :klass => Version,
31 34 :label => :label_version},
32 35 'category' => {:sql => "#{Issue.table_name}.category_id",
33 :values => @project.issue_categories,
36 :klass => IssueCategory,
34 37 :label => :field_category},
35 38 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
36 :values => @project.users,
39 :klass => User,
37 40 :label => :label_member},
38 41 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
39 :values => Tracker.find(:all),
42 :klass => Tracker,
40 43 :label => :label_tracker},
41 44 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
42 :values => Enumeration::get_values('ACTI'),
45 :klass => Enumeration,
43 46 :label => :label_activity}
44 47 }
45 48
46 49 @criterias = params[:criterias] || []
47 50 @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
48 51 @criterias.uniq!
52 @criterias = @criterias[0,3]
49 53
50 54 @columns = (params[:period] && %w(year month week).include?(params[:period])) ? params[:period] : 'month'
51 55
52 56 if params[:date_from]
53 57 begin; @date_from = params[:date_from].to_date; rescue; end
54 58 end
55 59 if params[:date_to]
56 60 begin; @date_to = params[:date_to].to_date; rescue; end
57 61 end
58 62 @date_from ||= Date.civil(Date.today.year, 1, 1)
59 63 @date_to ||= (Date.civil(Date.today.year, Date.today.month, 1) >> 1) - 1
60 64
61 65 unless @criterias.empty?
62 66 sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
63 67 sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
64 68
65 69 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, SUM(hours) AS hours"
66 sql << " FROM #{TimeEntry.table_name} LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
67 sql << " WHERE #{TimeEntry.table_name}.project_id = %s" % @project.id
70 sql << " FROM #{TimeEntry.table_name}"
71 sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
72 sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
73 sql << " WHERE (#{Project.table_name}.id = %s OR #{Project.table_name}.parent_id = %s)" % [@project.id, @project.id]
74 sql << " AND (%s)" % Project.allowed_to_condition(User.current, :view_time_entries)
68 75 sql << " AND spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@date_from.to_time), ActiveRecord::Base.connection.quoted_date(@date_to.to_time)]
69 76 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek"
70 77
71 78 @hours = ActiveRecord::Base.connection.select_all(sql)
72 79
73 80 @hours.each do |row|
74 81 case @columns
75 82 when 'year'
76 83 row['year'] = row['tyear']
77 84 when 'month'
78 85 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
79 86 when 'week'
80 87 row['week'] = "#{row['tyear']}-#{row['tweek']}"
81 88 end
82 89 end
90
91 @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
83 92 end
84 93
85 94 @periods = []
86 95 date_from = @date_from
87 96 # 100 columns max
88 97 while date_from < @date_to && @periods.length < 100
89 98 case @columns
90 99 when 'year'
91 100 @periods << "#{date_from.year}"
92 101 date_from = date_from >> 12
93 102 when 'month'
94 103 @periods << "#{date_from.year}-#{date_from.month}"
95 104 date_from = date_from >> 1
96 105 when 'week'
97 106 @periods << "#{date_from.year}-#{date_from.cweek}"
98 107 date_from = date_from + 7
99 108 end
100 109 end
101 110
102 111 render :layout => false if request.xhr?
103 112 end
104 113
105 114 def details
106 115 sort_init 'spent_on', 'desc'
107 116 sort_update
108 117
109 118 @free_period = false
110 119 @from, @to = nil, nil
111 120
112 121 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
113 122 case params[:period].to_s
114 123 when 'today'
115 124 @from = @to = Date.today
116 125 when 'yesterday'
117 126 @from = @to = Date.today - 1
118 127 when 'current_week'
119 128 @from = Date.today - (Date.today.cwday - 1)%7
120 129 @to = @from + 6
121 130 when 'last_week'
122 131 @from = Date.today - 7 - (Date.today.cwday - 1)%7
123 132 @to = @from + 6
124 133 when '7_days'
125 134 @from = Date.today - 7
126 135 @to = Date.today
127 136 when 'current_month'
128 137 @from = Date.civil(Date.today.year, Date.today.month, 1)
129 138 @to = (@from >> 1) - 1
130 139 when 'last_month'
131 140 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
132 141 @to = (@from >> 1) - 1
133 142 when '30_days'
134 143 @from = Date.today - 30
135 144 @to = Date.today
136 145 when 'current_year'
137 146 @from = Date.civil(Date.today.year, 1, 1)
138 147 @to = Date.civil(Date.today.year, 12, 31)
139 148 end
140 149 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
141 150 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
142 151 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
143 152 @free_period = true
144 153 else
145 154 # default
146 155 end
147 156
148 157 @from, @to = @to, @from if @from && @to && @from > @to
149 158
150 conditions = nil
159 cond = ARCondition.new
160 cond << (@issue.nil? ? ["(#{Project.table_name}.id = ? OR #{Project.table_name}.parent_id = ?)", @project.id, @project.id] :
161 ["#{TimeEntry.table_name}.issue_id = ?", @issue.id])
162
151 163 if @from
152 164 if @to
153 conditions = ['spent_on BETWEEN ? AND ?', @from, @to]
165 cond << ['spent_on BETWEEN ? AND ?', @from, @to]
154 166 else
155 conditions = ['spent_on >= ?', @from]
167 cond << ['spent_on >= ?', @from]
156 168 end
157 169 elsif @to
158 conditions = ['spent_on <= ?', @to]
170 cond << ['spent_on <= ?', @to]
159 171 end
160 172
161 @owner_id = User.current.id
162
173 TimeEntry.visible_by(User.current) do
163 174 respond_to do |format|
164 175 format.html {
165 176 # Paginate results
166 @entry_count = (@issue ? @issue : @project).time_entries.count(:conditions => conditions)
177 @entry_count = TimeEntry.count(:include => :project, :conditions => cond.conditions)
167 178 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
168 @entries = (@issue ? @issue : @project).time_entries.find(:all,
169 :include => [:activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
170 :conditions => conditions,
179 @entries = TimeEntry.find(:all,
180 :include => [:project, :activity, :user, {:issue => :tracker}],
181 :conditions => cond.conditions,
171 182 :order => sort_clause,
172 183 :limit => @entry_pages.items_per_page,
173 184 :offset => @entry_pages.current.offset)
174 @total_hours = (@issue ? @issue : @project).time_entries.sum(:hours, :conditions => conditions).to_f
185 @total_hours = TimeEntry.sum(:hours, :include => :project, :conditions => cond.conditions).to_f
175 186 render :layout => !request.xhr?
176 187 }
177 188 format.csv {
178 189 # Export all entries
179 @entries = (@issue ? @issue : @project).time_entries.find(:all,
180 :include => [:activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
181 :conditions => conditions,
190 @entries = TimeEntry.find(:all,
191 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
192 :conditions => cond.conditions,
182 193 :order => sort_clause)
183 194 send_data(entries_to_csv(@entries).read, :type => 'text/csv; header=present', :filename => 'timelog.csv')
184 195 }
185 196 end
186 197 end
198 end
187 199
188 200 def edit
189 201 render_404 and return if @time_entry && @time_entry.user != User.current
190 202 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
191 203 @time_entry.attributes = params[:time_entry]
192 204 if request.post? and @time_entry.save
193 205 flash[:notice] = l(:notice_successful_update)
194 206 redirect_to :action => 'details', :project_id => @time_entry.project, :issue_id => @time_entry.issue
195 207 return
196 208 end
197 209 @activities = Enumeration::get_values('ACTI')
198 210 end
199 211
200 212 private
201 213 def find_project
202 214 if params[:id]
203 215 @time_entry = TimeEntry.find(params[:id])
204 216 @project = @time_entry.project
205 217 elsif params[:issue_id]
206 218 @issue = Issue.find(params[:issue_id])
207 219 @project = @issue.project
208 220 elsif params[:project_id]
209 221 @project = Project.find(params[:project_id])
210 222 else
211 223 render_404
212 224 return false
213 225 end
214 226 end
215 227 end
@@ -1,77 +1,79
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 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 module TimelogHelper
19 19 def select_hours(data, criteria, value)
20 data.select {|row| row[criteria] == value.to_s}
20 data.select {|row| row[criteria] == value}
21 21 end
22 22
23 23 def sum_hours(data)
24 24 sum = 0
25 25 data.each do |row|
26 26 sum += row['hours'].to_f
27 27 end
28 28 sum
29 29 end
30 30
31 31 def options_for_period_select(value)
32 32 options_for_select([[l(:label_all_time), 'all'],
33 33 [l(:label_today), 'today'],
34 34 [l(:label_yesterday), 'yesterday'],
35 35 [l(:label_this_week), 'current_week'],
36 36 [l(:label_last_week), 'last_week'],
37 37 [l(:label_last_n_days, 7), '7_days'],
38 38 [l(:label_this_month), 'current_month'],
39 39 [l(:label_last_month), 'last_month'],
40 40 [l(:label_last_n_days, 30), '30_days'],
41 41 [l(:label_this_year), 'current_year']],
42 42 value)
43 43 end
44 44
45 45 def entries_to_csv(entries)
46 46 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
47 47 export = StringIO.new
48 48 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
49 49 # csv header fields
50 50 headers = [l(:field_spent_on),
51 51 l(:field_user),
52 52 l(:field_activity),
53 l(:field_project),
53 54 l(:field_issue),
54 55 l(:field_tracker),
55 56 l(:field_subject),
56 57 l(:field_hours),
57 58 l(:field_comments)
58 59 ]
59 60 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
60 61 # csv lines
61 62 entries.each do |entry|
62 63 fields = [l_date(entry.spent_on),
63 64 entry.user,
64 65 entry.activity,
66 entry.project,
65 67 (entry.issue ? entry.issue.id : nil),
66 68 (entry.issue ? entry.issue.tracker : nil),
67 69 (entry.issue ? entry.issue.subject : nil),
68 70 entry.hours,
69 71 entry.comments
70 72 ]
71 73 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
72 74 end
73 75 end
74 76 export.rewind
75 77 export
76 78 end
77 79 end
@@ -1,234 +1,249
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 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 Project < ActiveRecord::Base
19 19 # Project statuses
20 20 STATUS_ACTIVE = 1
21 21 STATUS_ARCHIVED = 9
22 22
23 23 has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
24 24 has_many :users, :through => :members
25 25 has_many :custom_values, :dependent => :delete_all, :as => :customized
26 26 has_many :enabled_modules, :dependent => :delete_all
27 27 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
28 28 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
29 29 has_many :issue_changes, :through => :issues, :source => :journals
30 30 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
31 31 has_many :time_entries, :dependent => :delete_all
32 32 has_many :queries, :dependent => :delete_all
33 33 has_many :documents, :dependent => :destroy
34 34 has_many :news, :dependent => :delete_all, :include => :author
35 35 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
36 36 has_many :boards, :order => "position ASC"
37 37 has_one :repository, :dependent => :destroy
38 38 has_many :changesets, :through => :repository
39 39 has_one :wiki, :dependent => :destroy
40 40 # Custom field for the project issues
41 41 has_and_belongs_to_many :custom_fields,
42 42 :class_name => 'IssueCustomField',
43 43 :order => "#{CustomField.table_name}.position",
44 44 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
45 45 :association_foreign_key => 'custom_field_id'
46 46
47 47 acts_as_tree :order => "name", :counter_cache => true
48 48
49 49 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id'
50 50 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
51 51 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}}
52 52
53 53 attr_protected :status, :enabled_module_names
54 54
55 55 validates_presence_of :name, :identifier
56 56 validates_uniqueness_of :name, :identifier
57 57 validates_associated :custom_values, :on => :update
58 58 validates_associated :repository, :wiki
59 59 validates_length_of :name, :maximum => 30
60 60 validates_length_of :homepage, :maximum => 60
61 61 validates_length_of :identifier, :in => 3..20
62 62 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
63 63
64 64 before_destroy :delete_all_members
65 65
66 66 def identifier=(identifier)
67 67 super unless identifier_frozen?
68 68 end
69 69
70 70 def identifier_frozen?
71 71 errors[:identifier].nil? && !(new_record? || identifier.blank?)
72 72 end
73 73
74 74 def issues_with_subprojects(include_subprojects=false)
75 75 conditions = nil
76 76 if include_subprojects && !active_children.empty?
77 77 ids = [id] + active_children.collect {|c| c.id}
78 78 conditions = ["#{Issue.table_name}.project_id IN (#{ids.join(',')})"]
79 79 end
80 80 conditions ||= ["#{Issue.table_name}.project_id = ?", id]
81 81 # Quick and dirty fix for Rails 2 compatibility
82 82 Issue.send(:with_scope, :find => { :conditions => conditions }) do
83 83 yield
84 84 end
85 85 end
86 86
87 87 # Return all issues status changes for the project between the 2 given dates
88 88 def issues_status_changes(from, to)
89 89 Journal.find(:all, :include => [:issue, :details, :user],
90 90 :conditions => ["#{Journal.table_name}.journalized_type = 'Issue'" +
91 91 " AND #{Issue.table_name}.project_id = ?" +
92 92 " AND #{JournalDetail.table_name}.prop_key = 'status_id'" +
93 93 " AND #{Journal.table_name}.created_on BETWEEN ? AND ?",
94 94 id, from, to+1])
95 95 end
96 96
97 97 # returns latest created projects
98 98 # non public projects will be returned only if user is a member of those
99 99 def self.latest(user=nil, count=5)
100 100 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
101 101 end
102 102
103 103 def self.visible_by(user=nil)
104 104 if user && user.admin?
105 105 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
106 106 elsif user && user.memberships.any?
107 107 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
108 108 else
109 109 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
110 110 end
111 111 end
112 112
113 def self.allowed_to_condition(user, permission)
114 statements = []
115 active_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
116 if user.admin?
117 # no restriction
118 elsif user.logged?
119 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
120 allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id}
121 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})"
122 else
123 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.anonymous.allowed_to?(permission)
124 end
125 statements.empty? ? active_statement : "(#{active_statement} AND (#{statements.join(' OR ')}))"
126 end
127
113 128 def self.find(*args)
114 129 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
115 130 project = find_by_identifier(*args)
116 131 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
117 132 project
118 133 else
119 134 super
120 135 end
121 136 end
122 137
123 138 def to_param
124 139 identifier
125 140 end
126 141
127 142 def active?
128 143 self.status == STATUS_ACTIVE
129 144 end
130 145
131 146 def archive
132 147 # Archive subprojects if any
133 148 children.each do |subproject|
134 149 subproject.archive
135 150 end
136 151 update_attribute :status, STATUS_ARCHIVED
137 152 end
138 153
139 154 def unarchive
140 155 return false if parent && !parent.active?
141 156 update_attribute :status, STATUS_ACTIVE
142 157 end
143 158
144 159 def active_children
145 160 children.select {|child| child.active?}
146 161 end
147 162
148 163 # Returns an array of the trackers used by the project and its sub projects
149 164 def rolled_up_trackers
150 165 @rolled_up_trackers ||=
151 166 Tracker.find(:all, :include => :projects,
152 167 :select => "DISTINCT #{Tracker.table_name}.*",
153 168 :conditions => ["#{Project.table_name}.id = ? OR #{Project.table_name}.parent_id = ?", id, id],
154 169 :order => "#{Tracker.table_name}.position")
155 170 end
156 171
157 172 # Deletes all project's members
158 173 def delete_all_members
159 174 Member.delete_all(['project_id = ?', id])
160 175 end
161 176
162 177 # Users issues can be assigned to
163 178 def assignable_users
164 179 members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort
165 180 end
166 181
167 182 # Returns the mail adresses of users that should be always notified on project events
168 183 def recipients
169 184 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
170 185 end
171 186
172 187 # Returns an array of all custom fields enabled for project issues
173 188 # (explictly associated custom fields and custom fields enabled for all projects)
174 189 def custom_fields_for_issues(tracker)
175 190 all_custom_fields.select {|c| tracker.custom_fields.include? c }
176 191 end
177 192
178 193 def all_custom_fields
179 194 @all_custom_fields ||= (IssueCustomField.for_all + custom_fields).uniq
180 195 end
181 196
182 197 def <=>(project)
183 198 name.downcase <=> project.name.downcase
184 199 end
185 200
186 201 def to_s
187 202 name
188 203 end
189 204
190 205 # Returns a short description of the projects (first lines)
191 206 def short_description(length = 255)
192 207 description.gsub(/^(.{#{length}}[^\n]*).*$/m, '\1').strip if description
193 208 end
194 209
195 210 def allows_to?(action)
196 211 if action.is_a? Hash
197 212 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
198 213 else
199 214 allowed_permissions.include? action
200 215 end
201 216 end
202 217
203 218 def module_enabled?(module_name)
204 219 module_name = module_name.to_s
205 220 enabled_modules.detect {|m| m.name == module_name}
206 221 end
207 222
208 223 def enabled_module_names=(module_names)
209 224 enabled_modules.clear
210 225 module_names = [] unless module_names && module_names.is_a?(Array)
211 226 module_names.each do |name|
212 227 enabled_modules << EnabledModule.new(:name => name.to_s)
213 228 end
214 229 end
215 230
216 231 protected
217 232 def validate
218 233 errors.add(parent_id, " must be a root project") if parent and parent.parent
219 234 errors.add_to_base("A project with subprojects can't be a subproject") if parent and children.size > 0
220 235 errors.add(:identifier, :activerecord_error_invalid) if !identifier.blank? && identifier.match(/^\d*$/)
221 236 end
222 237
223 238 private
224 239 def allowed_permissions
225 240 @allowed_permissions ||= begin
226 241 module_names = enabled_modules.collect {|m| m.name}
227 242 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
228 243 end
229 244 end
230 245
231 246 def allowed_actions
232 247 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
233 248 end
234 249 end
@@ -1,55 +1,61
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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 TimeEntry < ActiveRecord::Base
19 19 # could have used polymorphic association
20 20 # project association here allows easy loading of time entries at project level with one database trip
21 21 belongs_to :project
22 22 belongs_to :issue
23 23 belongs_to :user
24 24 belongs_to :activity, :class_name => 'Enumeration', :foreign_key => :activity_id
25 25
26 26 attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek
27 27
28 28 validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
29 29 validates_numericality_of :hours, :allow_nil => true
30 30 validates_length_of :comments, :maximum => 255
31 31
32 32 def before_validation
33 33 self.project = issue.project if issue && project.nil?
34 34 end
35 35
36 36 def validate
37 37 errors.add :hours, :activerecord_error_invalid if hours && (hours < 0 || hours >= 1000)
38 38 errors.add :project_id, :activerecord_error_invalid if project.nil?
39 39 errors.add :issue_id, :activerecord_error_invalid if (issue_id && !issue) || (issue && project!=issue.project)
40 40 end
41 41
42 42 # tyear, tmonth, tweek assigned where setting spent_on attributes
43 43 # these attributes make time aggregations easier
44 44 def spent_on=(date)
45 45 super
46 46 self.tyear = spent_on ? spent_on.year : nil
47 47 self.tmonth = spent_on ? spent_on.month : nil
48 48 self.tweek = spent_on ? Date.civil(spent_on.year, spent_on.month, spent_on.day).cweek : nil
49 49 end
50 50
51 51 # Returns true if the time entry can be edited by usr, otherwise false
52 52 def editable_by?(usr)
53 53 usr == self.user
54 54 end
55
56 def self.visible_by(usr)
57 with_scope(:find => { :conditions => Project.allowed_to_condition(usr, :view_time_entries) }) do
58 yield
59 end
60 end
55 61 end
@@ -1,32 +1,32
1 1 <table class="list time-entries">
2 2 <thead>
3 3 <%= sort_header_tag('spent_on', :caption => l(:label_date), :default_order => 'desc') %>
4 4 <%= sort_header_tag('user_id', :caption => l(:label_member)) %>
5 5 <%= sort_header_tag('activity_id', :caption => l(:label_activity)) %>
6 <%= sort_header_tag("#{Project.table_name}.name", :caption => l(:label_project)) %>
6 7 <%= sort_header_tag('issue_id', :caption => l(:label_issue), :default_order => 'desc') %>
7 8 <th><%= l(:field_comments) %></th>
8 9 <%= sort_header_tag('hours', :caption => l(:field_hours)) %>
9 10 <th></th>
10 11 </thead>
11 12 <tbody>
12 13 <% entries.each do |entry| -%>
13 14 <tr class="time-entry <%= cycle("odd", "even") %>">
14 15 <td class="spent_on"><%= format_date(entry.spent_on) %></td>
15 <td class="user"><%= entry.user.name %></td>
16 <td class="activity"><%= entry.activity.name %></td>
16 <td class="user"><%=h entry.user %></td>
17 <td class="activity"><%=h entry.activity %></td>
18 <td class="project"><%=h entry.project %></td>
17 19 <td class="subject">
18 20 <% if entry.issue -%>
19 <div class="tooltip"><%= link_to_issue entry.issue %>: <%= h(truncate(entry.issue.subject, 50)) -%>
20 <span class="tip"><%= render_issue_tooltip entry.issue %></span>
21 </div>
21 <%= link_to_issue entry.issue %>: <%= h(truncate(entry.issue.subject, 50)) -%>
22 22 <% end -%>
23 23 </td>
24 24 <td class="comments"><%=h entry.comments %></td>
25 <td class="hours"><%= entry.hours %></td>
25 <td class="hours"><%= html_hours("%.2f" % entry.hours) %></td>
26 26 <td align="center"><%= link_to_if_authorized(l(:button_edit),
27 27 {:controller => 'timelog', :action => 'edit', :id => entry},
28 28 :class => 'icon icon-edit') if entry.editable_by?(User.current) %></td>
29 29 </tr>
30 30 <% end -%>
31 31 </tbdoy>
32 32 </table>
@@ -1,17 +1,17
1 <% @available_criterias[criterias[level]][:values].each do |value| %>
2 <tr class="<%= cycle('odd', 'even') if criterias.length < level + 2 %>">
1 <% @hours.collect {|h| h[criterias[level]]}.uniq.each do |value| %>
2 <% hours_for_value = select_hours(hours, criterias[level], value) -%>
3 <% next if hours_for_value.empty? -%>
4 <tr class="<%= cycle('odd', 'even') %> <%= 'last-level' unless criterias.length > level+1 %>">
3 5 <%= '<td></td>' * level %>
4 <td><%= value.name %></td>
5 <%= '<td></td>' * (criterias.length - level - 1) %>
6 <% hours_for_value = select_hours(hours, criterias[level], value.id) %>
7 <% @periods.each do |period| %>
6 <td><%= value.nil? ? l(:label_none) : @available_criterias[criterias[level]][:klass].find_by_id(value) %></td>
7 <%= '<td></td>' * (criterias.length - level - 1) -%>
8 <% @periods.each do |period| -%>
8 9 <% sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s)) %>
9 <td align="center"><%= sum > 0 ? "%.2f" % sum : "-" %></td>
10 <% end %>
10 <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
11 <% end -%>
11 12 </tr>
12 <% if criterias.length > level+1 %>
13 <% if criterias.length > level+1 -%>
13 14 <%= render(:partial => 'report_criteria', :locals => {:criterias => criterias, :hours => hours_for_value, :level => (level + 1)}) %>
14 <% end %>
15 <% end -%>
15 16
16 17 <% end %>
17 <% reset_cycle %>
@@ -1,51 +1,52
1 1 <div class="contextual">
2 <%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}, :class => 'icon icon-report') %>
2 3 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time' %>
3 4 </div>
4 5
5 6 <h2><%= l(:label_spent_time) %></h2>
6 7
7 8 <% if @issue %>
8 9 <h3><%= link_to(@project.name, {:action => 'details', :project_id => @project}) %> / <%= link_to_issue(@issue) %></h3>
9 10 <% end %>
10 11
11 12 <% form_remote_tag( :url => {}, :method => :get, :update => 'content' ) do %>
12 13 <%= hidden_field_tag 'project_id', params[:project_id] %>
13 14 <%= hidden_field_tag 'issue_id', params[:issue_id] if @issue %>
14 15
15 16 <fieldset><legend><%= l(:label_date_range) %></legend>
16 17 <p>
17 18 <%= radio_button_tag 'period_type', '1', !@free_period %>
18 19 <%= select_tag 'period', options_for_period_select(params[:period]),
19 20 :onchange => 'this.form.onsubmit();',
20 21 :onfocus => '$("period_type_1").checked = true;' %>
21 22 </p>
22 23 <p>
23 24 <%= radio_button_tag 'period_type', '2', @free_period %>
24 25 <%= l(:label_date_from) %>
25 26 <%= text_field_tag 'from', @from, :size => 10, :onfocus => '$("period_type_2").checked = true;' %> <%= calendar_for('from') %>
26 27 <%= l(:label_date_to) %>
27 28 <%= text_field_tag 'to', @to, :size => 10, :onfocus => '$("period_type_2").checked = true;' %> <%= calendar_for('to') %>
28 <%= submit_tag l(:button_submit), :name => nil, :onclick => '$("period_type_2").checked = true;' %>
29 <%= submit_tag l(:button_apply), :name => nil, :onclick => '$("period_type_2").checked = true;' %>
29 30 </p>
30 31 </fieldset>
31 32 <% end %>
32 33
33 34 <div class="total-hours">
34 35 <p><%= l(:label_total) %>: <%= html_hours(lwr(:label_f_hour, @total_hours)) %></p>
35 36 </div>
36 37
37 38 <% unless @entries.empty? %>
38 39 <%= render :partial => 'list', :locals => { :entries => @entries }%>
39 40 <div class="contextual">
40 41 <%= l(:label_export_to) %>
41 42 <%= link_to 'CSV', params.merge(:format => 'csv'), :class => 'icon icon-csv' %>
42 43 </div>
43 44 <p class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></p>
44 45 <% end %>
45 46
46 47 <% content_for :header_tags do %>
47 48 <%= javascript_include_tag 'calendar/calendar' %>
48 49 <%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
49 50 <%= javascript_include_tag 'calendar/calendar-setup' %>
50 51 <%= stylesheet_link_tag 'calendar' %>
51 52 <% end %>
@@ -1,52 +1,72
1 <div class="contextual">
2 <%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}, :class => 'icon icon-details') %>
3 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time' %>
4 </div>
5
1 6 <h2><%= l(:label_spent_time) %></h2>
2 7
3 8 <% form_remote_tag(:url => {:project_id => @project}, :update => 'content') do %>
4 9 <% @criterias.each do |criteria| %>
5 10 <%= hidden_field_tag 'criterias[]', criteria %>
6 11 <% end %>
12 <fieldset><legend><%= l(:label_date_range) %></legend>
7 13 <p>
8 <%= l(:label_date_from) %>: <%= text_field_tag 'date_from', @date_from, :size => 10 %><%= calendar_for('date_from') %>
9 &nbsp;
10 <%= l(:label_date_to) %>: <%= text_field_tag 'date_to', @date_to, :size => 10 %><%= calendar_for('date_to') %>
11 &nbsp;
12 <%= l(:label_details) %>:
14 <%= l(:label_date_from) %>
15 <%= text_field_tag 'date_from', @date_from, :size => 10 %><%= calendar_for('date_from') %>
16 <%= l(:label_date_to) %>
17 <%= text_field_tag 'date_to', @date_to, :size => 10 %><%= calendar_for('date_to') %>
18 <%= l(:label_details) %>
13 19 <%= select_tag 'period', options_for_select([[l(:label_year), 'year'],
14 20 [l(:label_month), 'month'],
15 21 [l(:label_week), 'week']], @columns) %>
16 22 &nbsp;
17 23 <%= submit_tag l(:button_apply) %>
18 <%= link_to_remote l(:button_clear), {:url => {:project_id => @project}, :update => 'content'}, :class => 'icon icon-reload' %>
19 24 </p>
25 </fieldset>
20 26
21 <% if @criterias.length < 3 %>
22 <p><%= l(:button_add) %>: <%= select_tag('criterias[]', options_for_select([[]] + (@available_criterias.keys - @criterias).collect{|k| [l(@available_criterias[k][:label]), k]}), :onchange => "this.form.onsubmit();") %></p>
23 <% end %>
24
25 <br />
27 <p><%= l(:button_add) %>: <%= select_tag('criterias[]', options_for_select([[]] + (@available_criterias.keys - @criterias).collect{|k| [l(@available_criterias[k][:label]), k]}),
28 :onchange => "this.form.onsubmit();",
29 :style => 'width: 200px',
30 :disabled => (@criterias.length >= 3)) %>
31 <%= link_to_remote l(:button_clear), {:url => {:project_id => @project, :date_from => @date_from, :date_to => @date_to, :period => @columns}, :update => 'content'},
32 :class => 'icon icon-reload' %></p>
26 33
27 34 <% unless @criterias.empty? %>
28 <table class="list">
35 <div class="total-hours">
36 <p><%= l(:label_total) %>: <%= html_hours(lwr(:label_f_hour, @total_hours)) %></p>
37 </div>
38
39 <% unless @hours.empty? %>
40 <table class="list" id="time-report">
29 41 <thead>
30 42 <tr>
31 43 <% @criterias.each do |criteria| %>
32 44 <th width="15%"><%= l(@available_criterias[criteria][:label]) %></th>
33 45 <% end %>
34 46 <% @periods.each do |period| %>
35 47 <th width="<%= ((100 - @criterias.length * 15 - 15 ) / @periods.length).to_i %>%"><%= period %></th>
36 48 <% end %>
37 49 </tr>
38 50 </thead>
39
40 51 <tbody>
41 52 <%= render :partial => 'report_criteria', :locals => {:criterias => @criterias, :hours => @hours, :level => 0} %>
53 <tr class="total">
54 <td><%= l(:label_total) %></td>
55 <%= '<td></td>' * (@criterias.size - 1) %>
56 <% @periods.each do |period| -%>
57 <% sum = sum_hours(select_hours(@hours, @columns, period.to_s)) %>
58 <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
59 <% end -%>
60 </tr>
42 61 </tbody>
43 62 </table>
44 63 <% end %>
45 64 <% end %>
65 <% end %>
46 66
47 67 <% content_for :header_tags do %>
48 68 <%= javascript_include_tag 'calendar/calendar' %>
49 69 <%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
50 70 <%= javascript_include_tag 'calendar/calendar-setup' %>
51 71 <%= stylesheet_link_tag 'calendar' %>
52 72 <% end %>
@@ -1,559 +1,568
1 1 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
2 2
3 3 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
4 4 h1 {margin:0; padding:0; font-size: 24px;}
5 5 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
6 6 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
7 7 h4, .wiki h3 {font-size: 12px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
8 8
9 9 /***** Layout *****/
10 10 #wrapper {background: white;}
11 11
12 12 #top-menu {background: #2C4056;color: #fff;height:1.5em; padding: 2px 6px 0px 6px;}
13 13 #top-menu ul {margin: 0; padding: 0;}
14 14 #top-menu li {
15 15 float:left;
16 16 list-style-type:none;
17 17 margin: 0px 0px 0px 0px;
18 18 padding: 0px 0px 0px 0px;
19 19 white-space:nowrap;
20 20 }
21 21 #top-menu a {color: #fff; padding-right: 4px;}
22 22 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
23 23
24 24 #account {float:right;}
25 25
26 26 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
27 27 #header a {color:#f8f8f8;}
28 28 #quick-search {float:right;}
29 29
30 30 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
31 31 #main-menu ul {margin: 0; padding: 0;}
32 32 #main-menu li {
33 33 float:left;
34 34 list-style-type:none;
35 35 margin: 0px 10px 0px 0px;
36 36 padding: 0px 0px 0px 0px;
37 37 white-space:nowrap;
38 38 }
39 39 #main-menu li a {
40 40 display: block;
41 41 color: #fff;
42 42 text-decoration: none;
43 43 margin: 0;
44 44 padding: 4px 4px 4px 4px;
45 45 background: #2C4056;
46 46 }
47 47 #main-menu li a:hover, #main-menu li a.selected {background:#759FCF;}
48 48
49 49 #main {background: url(../images/mainbg.png) repeat-x; background-color:#EEEEEE;}
50 50
51 51 #sidebar{ float: right; width: 17%; position: relative; z-index: 9; min-height: 600px; padding: 0; margin: 0;}
52 52 * html #sidebar{ width: 17%; }
53 53 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
54 54 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
55 55 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
56 56
57 57 #content { width: 80%; background: url(../images/contentbg.png) repeat-x; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; height:600px; min-height: 600px;}
58 58 * html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
59 59 html>body #content {
60 60 height: auto;
61 61 min-height: 600px;
62 62 }
63 63
64 64 #main.nosidebar #sidebar{ display: none; }
65 65 #main.nosidebar #content{ width: auto; border-right: 0; }
66 66
67 67 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
68 68
69 69 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
70 70 #login-form table td {padding: 6px;}
71 71 #login-form label {font-weight: bold;}
72 72
73 73 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
74 74
75 75 /***** Links *****/
76 76 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
77 77 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
78 78 a img{ border: 0; }
79 79
80 80 /***** Tables *****/
81 81 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
82 82 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
83 83 table.list td { overflow: hidden; text-overflow: ellipsis; vertical-align: top;}
84 84 table.list td.id { width: 2%; text-align: center;}
85 85 table.list td.checkbox { width: 15px; padding: 0px;}
86 86
87 87 tr.issue { text-align: center; white-space: nowrap; }
88 88 tr.issue td.subject, tr.issue td.category { white-space: normal; }
89 89 tr.issue td.subject { text-align: left; }
90 90 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
91 91
92 92 tr.entry { border: 1px solid #f8f8f8; }
93 93 tr.entry td { white-space: nowrap; }
94 94 tr.entry td.filename { width: 30%; }
95 95 tr.entry td.size { text-align: right; font-size: 90%; }
96 96 tr.entry td.revision, tr.entry td.author { text-align: center; }
97 97 tr.entry td.age { text-align: right; }
98 98
99 99 tr.changeset td.author { text-align: center; width: 15%; }
100 100 tr.changeset td.committed_on { text-align: center; width: 15%; }
101 101
102 102 tr.message { height: 2.6em; }
103 103 tr.message td.last_message { font-size: 80%; }
104 104 tr.message.locked td.subject a { background-image: url(../images/locked.png); }
105 105 tr.message.sticky td.subject a { background-image: url(../images/sticky.png); font-weight: bold; }
106 106
107 107 tr.user td { width:13%; }
108 108 tr.user td.email { width:18%; }
109 109 tr.user td { white-space: nowrap; }
110 110 tr.user.locked, tr.user.registered { color: #aaa; }
111 111 tr.user.locked a, tr.user.registered a { color: #aaa; }
112 112
113 113 tr.time-entry { text-align: center; white-space: nowrap; }
114 114 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; }
115 tr.time-entry td.hours { text-align: right; font-weight: bold; padding-right: 0.6em; }
115 tr.time-entry td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
116 tr.time-entry .hours-dec { font-size: 0.9em; }
116 117
117 118 table.list tbody tr:hover { background-color:#ffffdd; }
118 119 table td {padding:2px;}
119 120 table p {margin:0;}
120 121 .odd {background-color:#f6f7f8;}
121 122 .even {background-color: #fff;}
122 123
123 124 .highlight { background-color: #FCFD8D;}
124 125 .highlight.token-1 { background-color: #faa;}
125 126 .highlight.token-2 { background-color: #afa;}
126 127 .highlight.token-3 { background-color: #aaf;}
127 128
128 129 .box{
129 130 padding:6px;
130 131 margin-bottom: 10px;
131 132 background-color:#f6f6f6;
132 133 color:#505050;
133 134 line-height:1.5em;
134 135 border: 1px solid #e4e4e4;
135 136 }
136 137
137 138 div.square {
138 139 border: 1px solid #999;
139 140 float: left;
140 141 margin: .3em .4em 0 .4em;
141 142 overflow: hidden;
142 143 width: .6em; height: .6em;
143 144 }
144 145
145 146 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
146 147 .contextual input {font-size:0.9em;}
147 148
148 149 .splitcontentleft{float:left; width:49%;}
149 150 .splitcontentright{float:right; width:49%;}
150 151 form {display: inline;}
151 152 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
152 153 fieldset {border: 1px solid #e4e4e4; margin:0;}
153 154 legend {color: #484848;}
154 155 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
155 156 textarea.wiki-edit { width: 99%; }
156 157 li p {margin-top: 0;}
157 158 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
158 159
159 160 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
160 161 div#issue-changesets .changeset { padding: 4px;}
161 162 div#issue-changesets .changeset { border-bottom: 1px solid #ddd; }
162 163 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
163 164
164 165 div#activity dl { margin-left: 2em; }
165 166 div#activity dd { margin-bottom: 1em; }
166 167 div#activity dt { margin-bottom: 1px; }
167 168 div#activity dt .time { color: #777; font-size: 80%; }
168 169 div#activity dd .description { font-style: italic; }
169 170
170 171 div#roadmap fieldset.related-issues { margin-bottom: 1em; }
171 172 div#roadmap fieldset.related-issues ul { margin-top: 0.3em; margin-bottom: 0.3em; }
172 173 div#roadmap .wiki h1:first-child { display: none; }
173 174 div#roadmap .wiki h1 { font-size: 120%; }
174 175 div#roadmap .wiki h2 { font-size: 110%; }
175 176
177 table#time-report td.hours { text-align: right; padding-right: 0.5em; }
178 table#time-report tbody tr { font-style: italic; color: #777; }
179 table#time-report tbody tr.last-level { font-style: normal; color: #555; }
180 table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
181 table#time-report .hours-dec { font-size: 0.9em; }
182
176 183 div.total-hours { text-align: left; font-size: 110%; font-weight: bold; }
177 184 div.total-hours span.hours-int { font-size: 120%; }
178 185
179 186 .autoscroll {overflow-x: auto; padding:1px; width:100%; margin-bottom: 1.2em;}
180 187 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
181 188
182 189 .pagination {font-size: 90%}
183 190 p.pagination {margin-top:8px;}
184 191
185 192 /***** Tabular forms ******/
186 193 .tabular p{
187 194 margin: 0;
188 195 padding: 5px 0 8px 0;
189 196 padding-left: 180px; /*width of left column containing the label elements*/
190 197 height: 1%;
191 198 clear:left;
192 199 }
193 200
194 201 .tabular label{
195 202 font-weight: bold;
196 203 float: left;
197 204 text-align: right;
198 205 margin-left: -180px; /*width of left column*/
199 206 width: 175px; /*width of labels. Should be smaller than left column to create some right
200 207 margin*/
201 208 }
202 209
203 210 .tabular label.floating{
204 211 font-weight: normal;
205 212 margin-left: 0px;
206 213 text-align: left;
207 214 width: 200px;
208 215 }
209 216
210 217 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
211 218
212 219 .tabular.settings p{ padding-left: 300px; }
213 220 .tabular.settings label{ margin-left: -300px; width: 295px; }
214 221
215 222 .required {color: #bb0000;}
216 223 .summary {font-style: italic;}
217 224
218 225 div.attachments p { margin:4px 0 2px 0; }
219 226
220 227 /***** Flash & error messages ****/
221 228 #errorExplanation, div.flash, .nodata {
222 229 padding: 4px 4px 4px 30px;
223 230 margin-bottom: 12px;
224 231 font-size: 1.1em;
225 232 border: 2px solid;
226 233 }
227 234
228 235 div.flash {margin-top: 8px;}
229 236
230 237 div.flash.error, #errorExplanation {
231 238 background: url(../images/false.png) 8px 5px no-repeat;
232 239 background-color: #ffe3e3;
233 240 border-color: #dd0000;
234 241 color: #550000;
235 242 }
236 243
237 244 div.flash.notice {
238 245 background: url(../images/true.png) 8px 5px no-repeat;
239 246 background-color: #dfffdf;
240 247 border-color: #9fcf9f;
241 248 color: #005f00;
242 249 }
243 250
244 251 .nodata {
245 252 text-align: center;
246 253 background-color: #FFEBC1;
247 254 border-color: #FDBF3B;
248 255 color: #A6750C;
249 256 }
250 257
251 258 #errorExplanation ul { font-size: 0.9em;}
252 259
253 260 /***** Ajax indicator ******/
254 261 #ajax-indicator {
255 262 position: absolute; /* fixed not supported by IE */
256 263 background-color:#eee;
257 264 border: 1px solid #bbb;
258 265 top:35%;
259 266 left:40%;
260 267 width:20%;
261 268 font-weight:bold;
262 269 text-align:center;
263 270 padding:0.6em;
264 271 z-index:100;
265 272 filter:alpha(opacity=50);
266 273 opacity: 0.5;
267 274 -khtml-opacity: 0.5;
268 275 }
269 276
270 277 html>body #ajax-indicator { position: fixed; }
271 278
272 279 #ajax-indicator span {
273 280 background-position: 0% 40%;
274 281 background-repeat: no-repeat;
275 282 background-image: url(../images/loading.gif);
276 283 padding-left: 26px;
277 284 vertical-align: bottom;
278 285 }
279 286
280 287 /***** Calendar *****/
281 288 table.cal {border-collapse: collapse; width: 100%; margin: 8px 0 6px 0;border: 1px solid #d7d7d7;}
282 289 table.cal thead th {width: 14%;}
283 290 table.cal tbody tr {height: 100px;}
284 291 table.cal th { background-color:#EEEEEE; padding: 4px; }
285 292 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
286 293 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
287 294 table.cal td.odd p.day-num {color: #bbb;}
288 295 table.cal td.today {background:#ffffdd;}
289 296 table.cal td.today p.day-num {font-weight: bold;}
290 297
291 298 /***** Tooltips ******/
292 299 .tooltip{position:relative;z-index:24;}
293 300 .tooltip:hover{z-index:25;color:#000;}
294 301 .tooltip span.tip{display: none; text-align:left;}
295 302
296 303 div.tooltip:hover span.tip{
297 304 display:block;
298 305 position:absolute;
299 306 top:12px; left:24px; width:270px;
300 307 border:1px solid #555;
301 308 background-color:#fff;
302 309 padding: 4px;
303 310 font-size: 0.8em;
304 311 color:#505050;
305 312 }
306 313
307 314 /***** Progress bar *****/
308 315 table.progress {
309 316 border: 1px solid #D7D7D7;
310 317 border-collapse: collapse;
311 318 border-spacing: 0pt;
312 319 empty-cells: show;
313 320 text-align: center;
314 321 float:left;
315 322 margin: 1px 6px 1px 0px;
316 323 }
317 324
318 325 table.progress td { height: 0.9em; }
319 326 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
320 327 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
321 328 table.progress td.open { background: #FFF none repeat scroll 0%; }
322 329 p.pourcent {font-size: 80%;}
323 330 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
324 331
325 332 div#status_by { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; }
326 333
327 334 /***** Tabs *****/
328 335 #content .tabs {height: 2.6em; border-bottom: 1px solid #bbbbbb; margin-bottom:1.2em; position:relative;}
329 336 #content .tabs ul {margin:0; position:absolute; bottom:-2px; padding-left:1em;}
330 337 #content .tabs>ul { bottom:-1px; } /* others */
331 338 #content .tabs ul li {
332 339 float:left;
333 340 list-style-type:none;
334 341 white-space:nowrap;
335 342 margin-right:8px;
336 343 background:#fff;
337 344 }
338 345 #content .tabs ul li a{
339 346 display:block;
340 347 font-size: 0.9em;
341 348 text-decoration:none;
342 349 line-height:1.3em;
343 350 padding:4px 6px 4px 6px;
344 351 border: 1px solid #ccc;
345 352 border-bottom: 1px solid #bbbbbb;
346 353 background-color: #eeeeee;
347 354 color:#777;
348 355 font-weight:bold;
349 356 }
350 357
351 358 #content .tabs ul li a:hover {
352 359 background-color: #ffffdd;
353 360 text-decoration:none;
354 361 }
355 362
356 363 #content .tabs ul li a.selected {
357 364 background-color: #fff;
358 365 border: 1px solid #bbbbbb;
359 366 border-bottom: 1px solid #fff;
360 367 }
361 368
362 369 #content .tabs ul li a.selected:hover {
363 370 background-color: #fff;
364 371 }
365 372
366 373 /***** Diff *****/
367 374 .diff_out { background: #fcc; }
368 375 .diff_in { background: #cfc; }
369 376
370 377 /***** Wiki *****/
371 378 div.wiki table {
372 379 border: 1px solid #505050;
373 380 border-collapse: collapse;
374 381 }
375 382
376 383 div.wiki table, div.wiki td, div.wiki th {
377 384 border: 1px solid #bbb;
378 385 padding: 4px;
379 386 }
380 387
381 388 div.wiki .external {
382 389 background-position: 0% 60%;
383 390 background-repeat: no-repeat;
384 391 padding-left: 12px;
385 392 background-image: url(../images/external.png);
386 393 }
387 394
388 395 div.wiki a.new {
389 396 color: #b73535;
390 397 }
391 398
392 399 div.wiki pre {
393 400 margin: 1em 1em 1em 1.6em;
394 401 padding: 2px;
395 402 background-color: #fafafa;
396 403 border: 1px solid #dadada;
397 404 width:95%;
398 405 overflow-x: auto;
399 406 }
400 407
401 408 div.wiki div.toc {
402 409 background-color: #ffffdd;
403 410 border: 1px solid #e4e4e4;
404 411 padding: 4px;
405 412 line-height: 1.2em;
406 413 margin-bottom: 12px;
407 414 margin-right: 12px;
408 415 display: table
409 416 }
410 417 * html div.wiki div.toc { width: 50%; } /* IE6 doesn't autosize div */
411 418
412 419 div.wiki div.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
413 420 div.wiki div.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
414 421
415 422 div.wiki div.toc a {
416 423 display: block;
417 424 font-size: 0.9em;
418 425 font-weight: normal;
419 426 text-decoration: none;
420 427 color: #606060;
421 428 }
422 429 div.wiki div.toc a:hover { color: #c61a1a; text-decoration: underline;}
423 430
424 431 div.wiki div.toc a.heading2 { margin-left: 6px; }
425 432 div.wiki div.toc a.heading3 { margin-left: 12px; font-size: 0.8em; }
426 433
427 434 /***** My page layout *****/
428 435 .block-receiver {
429 436 border:1px dashed #c0c0c0;
430 437 margin-bottom: 20px;
431 438 padding: 15px 0 15px 0;
432 439 }
433 440
434 441 .mypage-box {
435 442 margin:0 0 20px 0;
436 443 color:#505050;
437 444 line-height:1.5em;
438 445 }
439 446
440 447 .handle {
441 448 cursor: move;
442 449 }
443 450
444 451 a.close-icon {
445 452 display:block;
446 453 margin-top:3px;
447 454 overflow:hidden;
448 455 width:12px;
449 456 height:12px;
450 457 background-repeat: no-repeat;
451 458 cursor:pointer;
452 459 background-image:url('../images/close.png');
453 460 }
454 461
455 462 a.close-icon:hover {
456 463 background-image:url('../images/close_hl.png');
457 464 }
458 465
459 466 /***** Gantt chart *****/
460 467 .gantt_hdr {
461 468 position:absolute;
462 469 top:0;
463 470 height:16px;
464 471 border-top: 1px solid #c0c0c0;
465 472 border-bottom: 1px solid #c0c0c0;
466 473 border-right: 1px solid #c0c0c0;
467 474 text-align: center;
468 475 overflow: hidden;
469 476 }
470 477
471 478 .task {
472 479 position: absolute;
473 480 height:8px;
474 481 font-size:0.8em;
475 482 color:#888;
476 483 padding:0;
477 484 margin:0;
478 485 line-height:0.8em;
479 486 }
480 487
481 488 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
482 489 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
483 490 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
484 491 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
485 492
486 493 /***** Icons *****/
487 494 .icon {
488 495 background-position: 0% 40%;
489 496 background-repeat: no-repeat;
490 497 padding-left: 20px;
491 498 padding-top: 2px;
492 499 padding-bottom: 3px;
493 500 }
494 501
495 502 .icon22 {
496 503 background-position: 0% 40%;
497 504 background-repeat: no-repeat;
498 505 padding-left: 26px;
499 506 line-height: 22px;
500 507 vertical-align: middle;
501 508 }
502 509
503 510 .icon-add { background-image: url(../images/add.png); }
504 511 .icon-edit { background-image: url(../images/edit.png); }
505 512 .icon-copy { background-image: url(../images/copy.png); }
506 513 .icon-del { background-image: url(../images/delete.png); }
507 514 .icon-move { background-image: url(../images/move.png); }
508 515 .icon-save { background-image: url(../images/save.png); }
509 516 .icon-cancel { background-image: url(../images/cancel.png); }
510 517 .icon-pdf { background-image: url(../images/pdf.png); }
511 518 .icon-csv { background-image: url(../images/csv.png); }
512 519 .icon-html { background-image: url(../images/html.png); }
513 520 .icon-image { background-image: url(../images/image.png); }
514 521 .icon-txt { background-image: url(../images/txt.png); }
515 522 .icon-file { background-image: url(../images/file.png); }
516 523 .icon-folder { background-image: url(../images/folder.png); }
517 524 .open .icon-folder { background-image: url(../images/folder_open.png); }
518 525 .icon-package { background-image: url(../images/package.png); }
519 526 .icon-home { background-image: url(../images/home.png); }
520 527 .icon-user { background-image: url(../images/user.png); }
521 528 .icon-mypage { background-image: url(../images/user_page.png); }
522 529 .icon-admin { background-image: url(../images/admin.png); }
523 530 .icon-projects { background-image: url(../images/projects.png); }
524 531 .icon-logout { background-image: url(../images/logout.png); }
525 532 .icon-help { background-image: url(../images/help.png); }
526 533 .icon-attachment { background-image: url(../images/attachment.png); }
527 534 .icon-index { background-image: url(../images/index.png); }
528 535 .icon-history { background-image: url(../images/history.png); }
529 536 .icon-feed { background-image: url(../images/feed.png); }
530 537 .icon-time { background-image: url(../images/time.png); }
531 538 .icon-stats { background-image: url(../images/stats.png); }
532 539 .icon-warning { background-image: url(../images/warning.png); }
533 540 .icon-fav { background-image: url(../images/fav.png); }
534 541 .icon-fav-off { background-image: url(../images/fav_off.png); }
535 542 .icon-reload { background-image: url(../images/reload.png); }
536 543 .icon-lock { background-image: url(../images/locked.png); }
537 544 .icon-unlock { background-image: url(../images/unlock.png); }
538 545 .icon-checked { background-image: url(../images/true.png); }
546 .icon-details { background-image: url(../images/zoom_in.png); }
547 .icon-report { background-image: url(../images/report.png); }
539 548
540 549 .icon22-projects { background-image: url(../images/22x22/projects.png); }
541 550 .icon22-users { background-image: url(../images/22x22/users.png); }
542 551 .icon22-tracker { background-image: url(../images/22x22/tracker.png); }
543 552 .icon22-role { background-image: url(../images/22x22/role.png); }
544 553 .icon22-workflow { background-image: url(../images/22x22/workflow.png); }
545 554 .icon22-options { background-image: url(../images/22x22/options.png); }
546 555 .icon22-notifications { background-image: url(../images/22x22/notifications.png); }
547 556 .icon22-authent { background-image: url(../images/22x22/authent.png); }
548 557 .icon22-info { background-image: url(../images/22x22/info.png); }
549 558 .icon22-comment { background-image: url(../images/22x22/comment.png); }
550 559 .icon22-package { background-image: url(../images/22x22/package.png); }
551 560 .icon22-settings { background-image: url(../images/22x22/settings.png); }
552 561 .icon22-plugin { background-image: url(../images/22x22/plugin.png); }
553 562
554 563 /***** Media print specific styles *****/
555 564 @media print {
556 565 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual { display:none; }
557 566 #main { background: #fff; }
558 567 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; }
559 568 }
@@ -1,43 +1,58
1 1 ---
2 2 time_entries_001:
3 3 created_on: 2007-03-23 12:54:18 +01:00
4 4 tweek: 12
5 5 tmonth: 3
6 6 project_id: 1
7 7 comments: My hours
8 8 updated_on: 2007-03-23 12:54:18 +01:00
9 activity_id: 8
9 activity_id: 9
10 10 spent_on: 2007-03-23
11 11 issue_id: 1
12 12 id: 1
13 13 hours: 4.25
14 14 user_id: 2
15 15 tyear: 2007
16 16 time_entries_002:
17 17 created_on: 2007-03-23 14:11:04 +01:00
18 18 tweek: 12
19 19 tmonth: 3
20 20 project_id: 1
21 21 comments: ""
22 22 updated_on: 2007-03-23 14:11:04 +01:00
23 activity_id: 8
23 activity_id: 9
24 24 spent_on: 2007-03-12
25 25 issue_id: 1
26 26 id: 2
27 27 hours: 150.0
28 28 user_id: 1
29 29 tyear: 2007
30 30 time_entries_003:
31 31 created_on: 2007-04-21 12:20:48 +02:00
32 32 tweek: 16
33 33 tmonth: 4
34 34 project_id: 1
35 35 comments: ""
36 36 updated_on: 2007-04-21 12:20:48 +02:00
37 activity_id: 8
37 activity_id: 9
38 38 spent_on: 2007-04-21
39 39 issue_id: 2
40 40 id: 3
41 41 hours: 1.0
42 42 user_id: 1
43 43 tyear: 2007
44 time_entries_004:
45 created_on: 2007-04-22 12:20:48 +02:00
46 tweek: 16
47 tmonth: 4
48 project_id: 3
49 comments: Time spent on a subproject
50 updated_on: 2007-04-22 12:20:48 +02:00
51 activity_id: 10
52 spent_on: 2007-04-22
53 issue_id:
54 id: 4
55 hours: 7.65
56 user_id: 1
57 tyear: 2007
58 No newline at end of file
@@ -1,100 +1,112
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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.dirname(__FILE__) + '/../test_helper'
19 19 require 'timelog_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class TimelogController; def rescue_action(e) raise e end; end
23 23
24 24 class TimelogControllerTest < Test::Unit::TestCase
25 25 fixtures :projects, :issues, :time_entries, :users, :trackers, :enumerations, :issue_statuses
26 26
27 27 def setup
28 28 @controller = TimelogController.new
29 29 @request = ActionController::TestRequest.new
30 30 @response = ActionController::TestResponse.new
31 31 end
32 32
33 33 def test_report_no_criteria
34 34 get :report, :project_id => 1
35 35 assert_response :success
36 36 assert_template 'report'
37 37 end
38 38
39 39 def test_report_one_criteria
40 get :report, :project_id => 1, :period => "month", :date_from => "2007-01-01", :date_to => "2007-12-31", :criterias => ["member"]
40 get :report, :project_id => 1, :period => 'week', :date_from => "2007-04-01", :date_to => "2007-04-30", :criterias => ['project']
41 41 assert_response :success
42 42 assert_template 'report'
43 assert_not_nil assigns(:hours)
43 assert_not_nil assigns(:total_hours)
44 assert_equal "8.65", "%.2f" % assigns(:total_hours)
44 45 end
45 46
46 47 def test_report_two_criterias
47 get :report, :project_id => 1, :period => "week", :date_from => "2007-01-01", :date_to => "2007-12-31", :criterias => ["member", "activity"]
48 get :report, :project_id => 1, :period => 'month', :date_from => "2007-01-01", :date_to => "2007-12-31", :criterias => ["member", "activity"]
49 assert_response :success
50 assert_template 'report'
51 assert_not_nil assigns(:total_hours)
52 assert_equal "162.90", "%.2f" % assigns(:total_hours)
53 end
54
55 def test_report_one_criteria_no_result
56 get :report, :project_id => 1, :period => 'week', :date_from => "1998-04-01", :date_to => "1998-04-30", :criterias => ['project']
48 57 assert_response :success
49 58 assert_template 'report'
50 assert_not_nil assigns(:hours)
59 assert_not_nil assigns(:total_hours)
60 assert_equal "0.00", "%.2f" % assigns(:total_hours)
51 61 end
52 62
53 63 def test_details_at_project_level
54 64 get :details, :project_id => 1
55 65 assert_response :success
56 66 assert_template 'details'
57 67 assert_not_nil assigns(:entries)
58 assert_equal 3, assigns(:entries).size
68 assert_equal 4, assigns(:entries).size
69 # project and subproject
70 assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
59 71 assert_not_nil assigns(:total_hours)
60 assert_equal 155.25, assigns(:total_hours)
72 assert_equal "162.90", "%.2f" % assigns(:total_hours)
61 73 # display all time by default
62 74 assert_nil assigns(:from)
63 75 assert_nil assigns(:to)
64 76 end
65 77
66 78 def test_details_at_project_level_with_date_range
67 79 get :details, :project_id => 1, :from => '2007-03-20', :to => '2007-04-30'
68 80 assert_response :success
69 81 assert_template 'details'
70 82 assert_not_nil assigns(:entries)
71 assert_equal 2, assigns(:entries).size
83 assert_equal 3, assigns(:entries).size
72 84 assert_not_nil assigns(:total_hours)
73 assert_equal 5.25, assigns(:total_hours)
85 assert_equal "12.90", "%.2f" % assigns(:total_hours)
74 86 assert_equal '2007-03-20'.to_date, assigns(:from)
75 87 assert_equal '2007-04-30'.to_date, assigns(:to)
76 88 end
77 89
78 90 def test_details_at_project_level_with_period
79 91 get :details, :project_id => 1, :period => '7_days'
80 92 assert_response :success
81 93 assert_template 'details'
82 94 assert_not_nil assigns(:entries)
83 95 assert_not_nil assigns(:total_hours)
84 96 assert_equal Date.today - 7, assigns(:from)
85 97 assert_equal Date.today, assigns(:to)
86 98 end
87 99
88 100 def test_details_at_issue_level
89 101 get :details, :issue_id => 1
90 102 assert_response :success
91 103 assert_template 'details'
92 104 assert_not_nil assigns(:entries)
93 105 assert_equal 2, assigns(:entries).size
94 106 assert_not_nil assigns(:total_hours)
95 107 assert_equal 154.25, assigns(:total_hours)
96 108 # display all time by default
97 109 assert_nil assigns(:from)
98 110 assert_nil assigns(:to)
99 111 end
100 112 end
General Comments 0
You need to be logged in to leave comments. Login now