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