##// END OF EJS Templates
Time report can be done at issue level (closes #970) + timelog views xhtml validation....
Jean-Philippe Lang -
r1304:467f74510e44
parent child
Show More
@@ -1,242 +1,245
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 verify :method => :post, :only => :destroy, :redirect_to => { :action => :details }
23 verify :method => :post, :only => :destroy, :redirect_to => { :action => :details }
24
24
25 helper :sort
25 helper :sort
26 include SortHelper
26 include SortHelper
27 helper :issues
27 helper :issues
28 include TimelogHelper
28 include TimelogHelper
29
29
30 def report
30 def report
31 @available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
31 @available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
32 :klass => Project,
32 :klass => Project,
33 :label => :label_project},
33 :label => :label_project},
34 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
34 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
35 :klass => Version,
35 :klass => Version,
36 :label => :label_version},
36 :label => :label_version},
37 'category' => {:sql => "#{Issue.table_name}.category_id",
37 'category' => {:sql => "#{Issue.table_name}.category_id",
38 :klass => IssueCategory,
38 :klass => IssueCategory,
39 :label => :field_category},
39 :label => :field_category},
40 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
40 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
41 :klass => User,
41 :klass => User,
42 :label => :label_member},
42 :label => :label_member},
43 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
43 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
44 :klass => Tracker,
44 :klass => Tracker,
45 :label => :label_tracker},
45 :label => :label_tracker},
46 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
46 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
47 :klass => Enumeration,
47 :klass => Enumeration,
48 :label => :label_activity}
48 :label => :label_activity},
49 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
50 :klass => Issue,
51 :label => :label_issue}
49 }
52 }
50
53
51 @criterias = params[:criterias] || []
54 @criterias = params[:criterias] || []
52 @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
55 @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
53 @criterias.uniq!
56 @criterias.uniq!
54 @criterias = @criterias[0,3]
57 @criterias = @criterias[0,3]
55
58
56 @columns = (params[:columns] && %w(year month week).include?(params[:columns])) ? params[:columns] : 'month'
59 @columns = (params[:columns] && %w(year month week).include?(params[:columns])) ? params[:columns] : 'month'
57
60
58 retrieve_date_range
61 retrieve_date_range
59 @from ||= TimeEntry.minimum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today
62 @from ||= TimeEntry.minimum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today
60 @to ||= TimeEntry.maximum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today
63 @to ||= TimeEntry.maximum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today
61
64
62 unless @criterias.empty?
65 unless @criterias.empty?
63 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(', ')
64 sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
67 sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
65
68
66 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, SUM(hours) AS hours"
69 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, SUM(hours) AS hours"
67 sql << " FROM #{TimeEntry.table_name}"
70 sql << " FROM #{TimeEntry.table_name}"
68 sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
71 sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
69 sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
72 sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
70 sql << " WHERE (%s)" % @project.project_condition(Setting.display_subprojects_issues?)
73 sql << " WHERE (%s)" % @project.project_condition(Setting.display_subprojects_issues?)
71 sql << " AND (%s)" % Project.allowed_to_condition(User.current, :view_time_entries)
74 sql << " AND (%s)" % Project.allowed_to_condition(User.current, :view_time_entries)
72 sql << " AND spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)]
75 sql << " AND spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)]
73 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek"
76 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek"
74
77
75 @hours = ActiveRecord::Base.connection.select_all(sql)
78 @hours = ActiveRecord::Base.connection.select_all(sql)
76
79
77 @hours.each do |row|
80 @hours.each do |row|
78 case @columns
81 case @columns
79 when 'year'
82 when 'year'
80 row['year'] = row['tyear']
83 row['year'] = row['tyear']
81 when 'month'
84 when 'month'
82 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
85 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
83 when 'week'
86 when 'week'
84 row['week'] = "#{row['tyear']}-#{row['tweek']}"
87 row['week'] = "#{row['tyear']}-#{row['tweek']}"
85 end
88 end
86 end
89 end
87
90
88 @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
91 @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
89
92
90 @periods = []
93 @periods = []
91 # Date#at_beginning_of_ not supported in Rails 1.2.x
94 # Date#at_beginning_of_ not supported in Rails 1.2.x
92 date_from = @from.to_time
95 date_from = @from.to_time
93 # 100 columns max
96 # 100 columns max
94 while date_from <= @to.to_time && @periods.length < 100
97 while date_from <= @to.to_time && @periods.length < 100
95 case @columns
98 case @columns
96 when 'year'
99 when 'year'
97 @periods << "#{date_from.year}"
100 @periods << "#{date_from.year}"
98 date_from = (date_from + 1.year).at_beginning_of_year
101 date_from = (date_from + 1.year).at_beginning_of_year
99 when 'month'
102 when 'month'
100 @periods << "#{date_from.year}-#{date_from.month}"
103 @periods << "#{date_from.year}-#{date_from.month}"
101 date_from = (date_from + 1.month).at_beginning_of_month
104 date_from = (date_from + 1.month).at_beginning_of_month
102 when 'week'
105 when 'week'
103 @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
106 @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
104 date_from = (date_from + 7.day).at_beginning_of_week
107 date_from = (date_from + 7.day).at_beginning_of_week
105 end
108 end
106 end
109 end
107 end
110 end
108
111
109 render :layout => false if request.xhr?
112 render :layout => false if request.xhr?
110 end
113 end
111
114
112 def details
115 def details
113 sort_init 'spent_on', 'desc'
116 sort_init 'spent_on', 'desc'
114 sort_update
117 sort_update
115
118
116 cond = ARCondition.new
119 cond = ARCondition.new
117 cond << (@issue.nil? ? @project.project_condition(Setting.display_subprojects_issues?) :
120 cond << (@issue.nil? ? @project.project_condition(Setting.display_subprojects_issues?) :
118 ["#{TimeEntry.table_name}.issue_id = ?", @issue.id])
121 ["#{TimeEntry.table_name}.issue_id = ?", @issue.id])
119
122
120 retrieve_date_range
123 retrieve_date_range
121
124
122 if @from
125 if @from
123 if @to
126 if @to
124 cond << ['spent_on BETWEEN ? AND ?', @from, @to]
127 cond << ['spent_on BETWEEN ? AND ?', @from, @to]
125 else
128 else
126 cond << ['spent_on >= ?', @from]
129 cond << ['spent_on >= ?', @from]
127 end
130 end
128 elsif @to
131 elsif @to
129 cond << ['spent_on <= ?', @to]
132 cond << ['spent_on <= ?', @to]
130 end
133 end
131
134
132 TimeEntry.visible_by(User.current) do
135 TimeEntry.visible_by(User.current) do
133 respond_to do |format|
136 respond_to do |format|
134 format.html {
137 format.html {
135 # Paginate results
138 # Paginate results
136 @entry_count = TimeEntry.count(:include => :project, :conditions => cond.conditions)
139 @entry_count = TimeEntry.count(:include => :project, :conditions => cond.conditions)
137 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
140 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
138 @entries = TimeEntry.find(:all,
141 @entries = TimeEntry.find(:all,
139 :include => [:project, :activity, :user, {:issue => :tracker}],
142 :include => [:project, :activity, :user, {:issue => :tracker}],
140 :conditions => cond.conditions,
143 :conditions => cond.conditions,
141 :order => sort_clause,
144 :order => sort_clause,
142 :limit => @entry_pages.items_per_page,
145 :limit => @entry_pages.items_per_page,
143 :offset => @entry_pages.current.offset)
146 :offset => @entry_pages.current.offset)
144 @total_hours = TimeEntry.sum(:hours, :include => :project, :conditions => cond.conditions).to_f
147 @total_hours = TimeEntry.sum(:hours, :include => :project, :conditions => cond.conditions).to_f
145 render :layout => !request.xhr?
148 render :layout => !request.xhr?
146 }
149 }
147 format.csv {
150 format.csv {
148 # Export all entries
151 # Export all entries
149 @entries = TimeEntry.find(:all,
152 @entries = TimeEntry.find(:all,
150 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
153 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
151 :conditions => cond.conditions,
154 :conditions => cond.conditions,
152 :order => sort_clause)
155 :order => sort_clause)
153 send_data(entries_to_csv(@entries).read, :type => 'text/csv; header=present', :filename => 'timelog.csv')
156 send_data(entries_to_csv(@entries).read, :type => 'text/csv; header=present', :filename => 'timelog.csv')
154 }
157 }
155 end
158 end
156 end
159 end
157 end
160 end
158
161
159 def edit
162 def edit
160 render_403 and return if @time_entry && !@time_entry.editable_by?(User.current)
163 render_403 and return if @time_entry && !@time_entry.editable_by?(User.current)
161 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
164 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
162 @time_entry.attributes = params[:time_entry]
165 @time_entry.attributes = params[:time_entry]
163 if request.post? and @time_entry.save
166 if request.post? and @time_entry.save
164 flash[:notice] = l(:notice_successful_update)
167 flash[:notice] = l(:notice_successful_update)
165 redirect_to :action => 'details', :project_id => @time_entry.project
168 redirect_to :action => 'details', :project_id => @time_entry.project
166 return
169 return
167 end
170 end
168 @activities = Enumeration::get_values('ACTI')
171 @activities = Enumeration::get_values('ACTI')
169 end
172 end
170
173
171 def destroy
174 def destroy
172 render_404 and return unless @time_entry
175 render_404 and return unless @time_entry
173 render_403 and return unless @time_entry.editable_by?(User.current)
176 render_403 and return unless @time_entry.editable_by?(User.current)
174 @time_entry.destroy
177 @time_entry.destroy
175 flash[:notice] = l(:notice_successful_delete)
178 flash[:notice] = l(:notice_successful_delete)
176 redirect_to :back
179 redirect_to :back
177 rescue RedirectBackError
180 rescue RedirectBackError
178 redirect_to :action => 'details', :project_id => @time_entry.project
181 redirect_to :action => 'details', :project_id => @time_entry.project
179 end
182 end
180
183
181 private
184 private
182 def find_project
185 def find_project
183 if params[:id]
186 if params[:id]
184 @time_entry = TimeEntry.find(params[:id])
187 @time_entry = TimeEntry.find(params[:id])
185 @project = @time_entry.project
188 @project = @time_entry.project
186 elsif params[:issue_id]
189 elsif params[:issue_id]
187 @issue = Issue.find(params[:issue_id])
190 @issue = Issue.find(params[:issue_id])
188 @project = @issue.project
191 @project = @issue.project
189 elsif params[:project_id]
192 elsif params[:project_id]
190 @project = Project.find(params[:project_id])
193 @project = Project.find(params[:project_id])
191 else
194 else
192 render_404
195 render_404
193 return false
196 return false
194 end
197 end
195 rescue ActiveRecord::RecordNotFound
198 rescue ActiveRecord::RecordNotFound
196 render_404
199 render_404
197 end
200 end
198
201
199 # Retreive the date range based on predefined ranges or specific from/to param dates
202 # Retrieves the date range based on predefined ranges or specific from/to param dates
200 def retrieve_date_range
203 def retrieve_date_range
201 @free_period = false
204 @free_period = false
202 @from, @to = nil, nil
205 @from, @to = nil, nil
203
206
204 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
207 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
205 case params[:period].to_s
208 case params[:period].to_s
206 when 'today'
209 when 'today'
207 @from = @to = Date.today
210 @from = @to = Date.today
208 when 'yesterday'
211 when 'yesterday'
209 @from = @to = Date.today - 1
212 @from = @to = Date.today - 1
210 when 'current_week'
213 when 'current_week'
211 @from = Date.today - (Date.today.cwday - 1)%7
214 @from = Date.today - (Date.today.cwday - 1)%7
212 @to = @from + 6
215 @to = @from + 6
213 when 'last_week'
216 when 'last_week'
214 @from = Date.today - 7 - (Date.today.cwday - 1)%7
217 @from = Date.today - 7 - (Date.today.cwday - 1)%7
215 @to = @from + 6
218 @to = @from + 6
216 when '7_days'
219 when '7_days'
217 @from = Date.today - 7
220 @from = Date.today - 7
218 @to = Date.today
221 @to = Date.today
219 when 'current_month'
222 when 'current_month'
220 @from = Date.civil(Date.today.year, Date.today.month, 1)
223 @from = Date.civil(Date.today.year, Date.today.month, 1)
221 @to = (@from >> 1) - 1
224 @to = (@from >> 1) - 1
222 when 'last_month'
225 when 'last_month'
223 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
226 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
224 @to = (@from >> 1) - 1
227 @to = (@from >> 1) - 1
225 when '30_days'
228 when '30_days'
226 @from = Date.today - 30
229 @from = Date.today - 30
227 @to = Date.today
230 @to = Date.today
228 when 'current_year'
231 when 'current_year'
229 @from = Date.civil(Date.today.year, 1, 1)
232 @from = Date.civil(Date.today.year, 1, 1)
230 @to = Date.civil(Date.today.year, 12, 31)
233 @to = Date.civil(Date.today.year, 12, 31)
231 end
234 end
232 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
235 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
233 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
236 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
234 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
237 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
235 @free_period = true
238 @free_period = true
236 else
239 else
237 # default
240 # default
238 end
241 end
239
242
240 @from, @to = @to, @from if @from && @to && @from > @to
243 @from, @to = @to, @from if @from && @to && @from > @to
241 end
244 end
242 end
245 end
@@ -1,240 +1,244
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 Issue < ActiveRecord::Base
18 class Issue < ActiveRecord::Base
19 belongs_to :project
19 belongs_to :project
20 belongs_to :tracker
20 belongs_to :tracker
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
25 belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
25 belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
27
27
28 has_many :journals, :as => :journalized, :dependent => :destroy
28 has_many :journals, :as => :journalized, :dependent => :destroy
29 has_many :attachments, :as => :container, :dependent => :destroy
29 has_many :attachments, :as => :container, :dependent => :destroy
30 has_many :time_entries, :dependent => :delete_all
30 has_many :time_entries, :dependent => :delete_all
31 has_many :custom_values, :dependent => :delete_all, :as => :customized
31 has_many :custom_values, :dependent => :delete_all, :as => :customized
32 has_many :custom_fields, :through => :custom_values
32 has_many :custom_fields, :through => :custom_values
33 has_and_belongs_to_many :changesets, :order => "revision ASC"
33 has_and_belongs_to_many :changesets, :order => "revision ASC"
34
34
35 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
35 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
36 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
37
37
38 acts_as_watchable
38 acts_as_watchable
39 acts_as_searchable :columns => ['subject', 'description'], :with => {:journal => :issue}
39 acts_as_searchable :columns => ['subject', 'description'], :with => {:journal => :issue}
40 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
40 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
41 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
41 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
42
42
43 validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status
43 validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status
44 validates_length_of :subject, :maximum => 255
44 validates_length_of :subject, :maximum => 255
45 validates_inclusion_of :done_ratio, :in => 0..100
45 validates_inclusion_of :done_ratio, :in => 0..100
46 validates_numericality_of :estimated_hours, :allow_nil => true
46 validates_numericality_of :estimated_hours, :allow_nil => true
47 validates_associated :custom_values, :on => :update
47 validates_associated :custom_values, :on => :update
48
48
49 def after_initialize
49 def after_initialize
50 if new_record?
50 if new_record?
51 # set default values for new records only
51 # set default values for new records only
52 self.status ||= IssueStatus.default
52 self.status ||= IssueStatus.default
53 self.priority ||= Enumeration.default('IPRI')
53 self.priority ||= Enumeration.default('IPRI')
54 end
54 end
55 end
55 end
56
56
57 def copy_from(arg)
57 def copy_from(arg)
58 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
58 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
59 self.attributes = issue.attributes.dup
59 self.attributes = issue.attributes.dup
60 self.custom_values = issue.custom_values.collect {|v| v.clone}
60 self.custom_values = issue.custom_values.collect {|v| v.clone}
61 self
61 self
62 end
62 end
63
63
64 # Move an issue to a new project and tracker
64 # Move an issue to a new project and tracker
65 def move_to(new_project, new_tracker = nil)
65 def move_to(new_project, new_tracker = nil)
66 transaction do
66 transaction do
67 if new_project && project_id != new_project.id
67 if new_project && project_id != new_project.id
68 # delete issue relations
68 # delete issue relations
69 unless Setting.cross_project_issue_relations?
69 unless Setting.cross_project_issue_relations?
70 self.relations_from.clear
70 self.relations_from.clear
71 self.relations_to.clear
71 self.relations_to.clear
72 end
72 end
73 # issue is moved to another project
73 # issue is moved to another project
74 self.category = nil
74 self.category = nil
75 self.fixed_version = nil
75 self.fixed_version = nil
76 self.project = new_project
76 self.project = new_project
77 end
77 end
78 if new_tracker
78 if new_tracker
79 self.tracker = new_tracker
79 self.tracker = new_tracker
80 end
80 end
81 if save
81 if save
82 # Manually update project_id on related time entries
82 # Manually update project_id on related time entries
83 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
83 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
84 else
84 else
85 rollback_db_transaction
85 rollback_db_transaction
86 return false
86 return false
87 end
87 end
88 end
88 end
89 return true
89 return true
90 end
90 end
91
91
92 def priority_id=(pid)
92 def priority_id=(pid)
93 self.priority = nil
93 self.priority = nil
94 write_attribute(:priority_id, pid)
94 write_attribute(:priority_id, pid)
95 end
95 end
96
96
97 def validate
97 def validate
98 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
98 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
99 errors.add :due_date, :activerecord_error_not_a_date
99 errors.add :due_date, :activerecord_error_not_a_date
100 end
100 end
101
101
102 if self.due_date and self.start_date and self.due_date < self.start_date
102 if self.due_date and self.start_date and self.due_date < self.start_date
103 errors.add :due_date, :activerecord_error_greater_than_start_date
103 errors.add :due_date, :activerecord_error_greater_than_start_date
104 end
104 end
105
105
106 if start_date && soonest_start && start_date < soonest_start
106 if start_date && soonest_start && start_date < soonest_start
107 errors.add :start_date, :activerecord_error_invalid
107 errors.add :start_date, :activerecord_error_invalid
108 end
108 end
109 end
109 end
110
110
111 def validate_on_create
111 def validate_on_create
112 errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
112 errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
113 end
113 end
114
114
115 def before_create
115 def before_create
116 # default assignment based on category
116 # default assignment based on category
117 if assigned_to.nil? && category && category.assigned_to
117 if assigned_to.nil? && category && category.assigned_to
118 self.assigned_to = category.assigned_to
118 self.assigned_to = category.assigned_to
119 end
119 end
120 end
120 end
121
121
122 def before_save
122 def before_save
123 if @current_journal
123 if @current_journal
124 # attributes changes
124 # attributes changes
125 (Issue.column_names - %w(id description)).each {|c|
125 (Issue.column_names - %w(id description)).each {|c|
126 @current_journal.details << JournalDetail.new(:property => 'attr',
126 @current_journal.details << JournalDetail.new(:property => 'attr',
127 :prop_key => c,
127 :prop_key => c,
128 :old_value => @issue_before_change.send(c),
128 :old_value => @issue_before_change.send(c),
129 :value => send(c)) unless send(c)==@issue_before_change.send(c)
129 :value => send(c)) unless send(c)==@issue_before_change.send(c)
130 }
130 }
131 # custom fields changes
131 # custom fields changes
132 custom_values.each {|c|
132 custom_values.each {|c|
133 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
133 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
134 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
134 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
135 @current_journal.details << JournalDetail.new(:property => 'cf',
135 @current_journal.details << JournalDetail.new(:property => 'cf',
136 :prop_key => c.custom_field_id,
136 :prop_key => c.custom_field_id,
137 :old_value => @custom_values_before_change[c.custom_field_id],
137 :old_value => @custom_values_before_change[c.custom_field_id],
138 :value => c.value)
138 :value => c.value)
139 }
139 }
140 @current_journal.save
140 @current_journal.save
141 end
141 end
142 # Save the issue even if the journal is not saved (because empty)
142 # Save the issue even if the journal is not saved (because empty)
143 true
143 true
144 end
144 end
145
145
146 def after_save
146 def after_save
147 # Reload is needed in order to get the right status
147 # Reload is needed in order to get the right status
148 reload
148 reload
149
149
150 # Update start/due dates of following issues
150 # Update start/due dates of following issues
151 relations_from.each(&:set_issue_to_dates)
151 relations_from.each(&:set_issue_to_dates)
152
152
153 # Close duplicates if the issue was closed
153 # Close duplicates if the issue was closed
154 if @issue_before_change && !@issue_before_change.closed? && self.closed?
154 if @issue_before_change && !@issue_before_change.closed? && self.closed?
155 duplicates.each do |duplicate|
155 duplicates.each do |duplicate|
156 # Don't re-close it if it's already closed
156 # Don't re-close it if it's already closed
157 next if duplicate.closed?
157 next if duplicate.closed?
158 # Same user and notes
158 # Same user and notes
159 duplicate.init_journal(@current_journal.user, @current_journal.notes)
159 duplicate.init_journal(@current_journal.user, @current_journal.notes)
160 duplicate.update_attribute :status, self.status
160 duplicate.update_attribute :status, self.status
161 end
161 end
162 end
162 end
163 end
163 end
164
164
165 def custom_value_for(custom_field)
165 def custom_value_for(custom_field)
166 self.custom_values.each {|v| return v if v.custom_field_id == custom_field.id }
166 self.custom_values.each {|v| return v if v.custom_field_id == custom_field.id }
167 return nil
167 return nil
168 end
168 end
169
169
170 def init_journal(user, notes = "")
170 def init_journal(user, notes = "")
171 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
171 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
172 @issue_before_change = self.clone
172 @issue_before_change = self.clone
173 @issue_before_change.status = self.status
173 @issue_before_change.status = self.status
174 @custom_values_before_change = {}
174 @custom_values_before_change = {}
175 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
175 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
176 @current_journal
176 @current_journal
177 end
177 end
178
178
179 # Return true if the issue is closed, otherwise false
179 # Return true if the issue is closed, otherwise false
180 def closed?
180 def closed?
181 self.status.is_closed?
181 self.status.is_closed?
182 end
182 end
183
183
184 # Users the issue can be assigned to
184 # Users the issue can be assigned to
185 def assignable_users
185 def assignable_users
186 project.assignable_users
186 project.assignable_users
187 end
187 end
188
188
189 # Returns an array of status that user is able to apply
189 # Returns an array of status that user is able to apply
190 def new_statuses_allowed_to(user)
190 def new_statuses_allowed_to(user)
191 statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
191 statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
192 statuses << status unless statuses.empty?
192 statuses << status unless statuses.empty?
193 statuses.uniq.sort
193 statuses.uniq.sort
194 end
194 end
195
195
196 # Returns the mail adresses of users that should be notified for the issue
196 # Returns the mail adresses of users that should be notified for the issue
197 def recipients
197 def recipients
198 recipients = project.recipients
198 recipients = project.recipients
199 # Author and assignee are always notified unless they have been locked
199 # Author and assignee are always notified unless they have been locked
200 recipients << author.mail if author && author.active?
200 recipients << author.mail if author && author.active?
201 recipients << assigned_to.mail if assigned_to && assigned_to.active?
201 recipients << assigned_to.mail if assigned_to && assigned_to.active?
202 recipients.compact.uniq
202 recipients.compact.uniq
203 end
203 end
204
204
205 def spent_hours
205 def spent_hours
206 @spent_hours ||= time_entries.sum(:hours) || 0
206 @spent_hours ||= time_entries.sum(:hours) || 0
207 end
207 end
208
208
209 def relations
209 def relations
210 (relations_from + relations_to).sort
210 (relations_from + relations_to).sort
211 end
211 end
212
212
213 def all_dependent_issues
213 def all_dependent_issues
214 dependencies = []
214 dependencies = []
215 relations_from.each do |relation|
215 relations_from.each do |relation|
216 dependencies << relation.issue_to
216 dependencies << relation.issue_to
217 dependencies += relation.issue_to.all_dependent_issues
217 dependencies += relation.issue_to.all_dependent_issues
218 end
218 end
219 dependencies
219 dependencies
220 end
220 end
221
221
222 # Returns an array of the duplicate issues
222 # Returns an array of the duplicate issues
223 def duplicates
223 def duplicates
224 relations.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.other_issue(self)}
224 relations.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.other_issue(self)}
225 end
225 end
226
226
227 def duration
227 def duration
228 (start_date && due_date) ? due_date - start_date : 0
228 (start_date && due_date) ? due_date - start_date : 0
229 end
229 end
230
230
231 def soonest_start
231 def soonest_start
232 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
232 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
233 end
233 end
234
234
235 def self.visible_by(usr)
235 def self.visible_by(usr)
236 with_scope(:find => { :conditions => Project.visible_by(usr) }) do
236 with_scope(:find => { :conditions => Project.visible_by(usr) }) do
237 yield
237 yield
238 end
238 end
239 end
239 end
240
241 def to_s
242 "#{tracker} ##{id}: #{subject}"
243 end
240 end
244 end
@@ -1,39 +1,41
1 <table class="list time-entries">
1 <table class="list time-entries">
2 <thead>
2 <thead>
3 <tr>
3 <%= sort_header_tag('spent_on', :caption => l(:label_date), :default_order => 'desc') %>
4 <%= sort_header_tag('spent_on', :caption => l(:label_date), :default_order => 'desc') %>
4 <%= sort_header_tag('user_id', :caption => l(:label_member)) %>
5 <%= sort_header_tag('user_id', :caption => l(:label_member)) %>
5 <%= sort_header_tag('activity_id', :caption => l(:label_activity)) %>
6 <%= sort_header_tag('activity_id', :caption => l(:label_activity)) %>
6 <%= sort_header_tag("#{Project.table_name}.name", :caption => l(:label_project)) %>
7 <%= sort_header_tag("#{Project.table_name}.name", :caption => l(:label_project)) %>
7 <%= sort_header_tag('issue_id', :caption => l(:label_issue), :default_order => 'desc') %>
8 <%= sort_header_tag('issue_id', :caption => l(:label_issue), :default_order => 'desc') %>
8 <th><%= l(:field_comments) %></th>
9 <th><%= l(:field_comments) %></th>
9 <%= sort_header_tag('hours', :caption => l(:field_hours)) %>
10 <%= sort_header_tag('hours', :caption => l(:field_hours)) %>
10 <th></th>
11 <th></th>
12 </tr>
11 </thead>
13 </thead>
12 <tbody>
14 <tbody>
13 <% entries.each do |entry| -%>
15 <% entries.each do |entry| -%>
14 <tr class="time-entry <%= cycle("odd", "even") %>">
16 <tr class="time-entry <%= cycle("odd", "even") %>">
15 <td class="spent_on"><%= format_date(entry.spent_on) %></td>
17 <td class="spent_on"><%= format_date(entry.spent_on) %></td>
16 <td class="user"><%=h entry.user %></td>
18 <td class="user"><%=h entry.user %></td>
17 <td class="activity"><%=h entry.activity %></td>
19 <td class="activity"><%=h entry.activity %></td>
18 <td class="project"><%=h entry.project %></td>
20 <td class="project"><%=h entry.project %></td>
19 <td class="subject">
21 <td class="subject">
20 <% if entry.issue -%>
22 <% if entry.issue -%>
21 <%= link_to_issue entry.issue %>: <%= h(truncate(entry.issue.subject, 50)) -%>
23 <%= link_to_issue entry.issue %>: <%= h(truncate(entry.issue.subject, 50)) -%>
22 <% end -%>
24 <% end -%>
23 </td>
25 </td>
24 <td class="comments"><%=h entry.comments %></td>
26 <td class="comments"><%=h entry.comments %></td>
25 <td class="hours"><%= html_hours("%.2f" % entry.hours) %></td>
27 <td class="hours"><%= html_hours("%.2f" % entry.hours) %></td>
26 <td align="center">
28 <td align="center">
27 <% if entry.editable_by?(User.current) -%>
29 <% if entry.editable_by?(User.current) -%>
28 <%= link_to image_tag('edit.png'), {:controller => 'timelog', :action => 'edit', :id => entry},
30 <%= link_to image_tag('edit.png'), {:controller => 'timelog', :action => 'edit', :id => entry},
29 :title => l(:button_edit) %>
31 :title => l(:button_edit) %>
30 <%= link_to image_tag('delete.png'), {:controller => 'timelog', :action => 'destroy', :id => entry},
32 <%= link_to image_tag('delete.png'), {:controller => 'timelog', :action => 'destroy', :id => entry},
31 :confirm => l(:text_are_you_sure),
33 :confirm => l(:text_are_you_sure),
32 :method => :post,
34 :method => :post,
33 :title => l(:button_delete) %>
35 :title => l(:button_delete) %>
34 <% end -%>
36 <% end -%>
35 </td>
37 </td>
36 </tr>
38 </tr>
37 <% end -%>
39 <% end -%>
38 </tbdoy>
40 </tbody>
39 </table>
41 </table>
@@ -1,30 +1,32
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(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}, :class => 'icon icon-report') %>
3 <%= 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' %>
4 </div>
4 </div>
5
5
6 <h2><%= l(:label_spent_time) %></h2>
6 <h2><%= l(:label_spent_time) %></h2>
7
7
8 <% if @issue %>
8 <% if @issue %>
9 <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>
10 <% end %>
10 <% end %>
11
11
12 <% form_remote_tag( :url => {}, :method => :get, :update => 'content' ) do %>
12 <% form_remote_tag( :url => {}, :method => :get, :update => 'content' ) do %>
13 <%= hidden_field_tag 'project_id', params[:project_id] %>
13 <%= hidden_field_tag 'project_id', params[:project_id] %>
14 <%= hidden_field_tag 'issue_id', params[:issue_id] if @issue %>
14 <%= hidden_field_tag 'issue_id', params[:issue_id] if @issue %>
15 <%= render :partial => 'date_range' %>
15 <%= render :partial => 'date_range' %>
16 <% end %>
16 <% end %>
17
17
18 <div class="total-hours">
18 <div class="total-hours">
19 <p><%= l(:label_total) %>: <%= html_hours(lwr(:label_f_hour, @total_hours)) %></p>
19 <p><%= l(:label_total) %>: <%= html_hours(lwr(:label_f_hour, @total_hours)) %></p>
20 </div>
20 </div>
21
21
22 <% unless @entries.empty? %>
22 <% unless @entries.empty? %>
23 <%= render :partial => 'list', :locals => { :entries => @entries }%>
23 <%= render :partial => 'list', :locals => { :entries => @entries }%>
24 <p class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></p>
24 <p class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></p>
25
25
26 <p class="other-formats">
26 <p class="other-formats">
27 <%= l(:label_export_to) %>
27 <%= l(:label_export_to) %>
28 <span><%= link_to 'CSV', params.merge(:format => 'csv'), :class => 'csv' %></span>
28 <span><%= link_to 'CSV', params.merge(:format => 'csv'), :class => 'csv' %></span>
29 </p>
29 </p>
30 <% end %>
30 <% end %>
31
32 <% html_title l(:label_spent_time), l(:label_details) %>
@@ -1,59 +1,62
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}, :class => 'icon icon-details') %>
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' %>
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>
4 </div>
5
5
6 <h2><%= l(:label_spent_time) %></h2>
6 <h2><%= l(:label_spent_time) %></h2>
7
7
8 <% form_remote_tag(:url => {}, :update => 'content') do %>
8 <% form_remote_tag(:url => {}, :update => 'content') do %>
9 <% @criterias.each do |criteria| %>
9 <% @criterias.each do |criteria| %>
10 <%= hidden_field_tag 'criterias[]', criteria %>
10 <%= hidden_field_tag 'criterias[]', criteria, :id => nil %>
11 <% end %>
11 <% end %>
12 <%= hidden_field_tag 'project_id', params[:project_id] %>
12 <%= hidden_field_tag 'project_id', params[:project_id] %>
13 <%= render :partial => 'date_range' %>
13 <%= render :partial => 'date_range' %>
14 </p>
14
15 </fieldset>
16 <p><%= l(:label_details) %>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'],
15 <p><%= l(:label_details) %>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'],
17 [l(:label_month), 'month'],
16 [l(:label_month), 'month'],
18 [l(:label_week), 'week']], @columns),
17 [l(:label_week), 'week']], @columns),
19 :onchange => "this.form.onsubmit();" %>
18 :onchange => "this.form.onsubmit();" %>
20
19
21 <%= l(:button_add) %>: <%= select_tag('criterias[]', options_for_select([[]] + (@available_criterias.keys - @criterias).collect{|k| [l(@available_criterias[k][:label]), k]}),
20 <%= l(:button_add) %>: <%= select_tag('criterias[]', options_for_select([[]] + (@available_criterias.keys - @criterias).collect{|k| [l(@available_criterias[k][:label]), k]}),
22 :onchange => "this.form.onsubmit();",
21 :onchange => "this.form.onsubmit();",
23 :style => 'width: 200px',
22 :style => 'width: 200px',
23 :id => nil,
24 :disabled => (@criterias.length >= 3)) %>
24 :disabled => (@criterias.length >= 3)) %>
25 <%= link_to_remote l(:button_clear), {:url => {:project_id => @project, :date_from => @date_from, :date_to => @date_to, :period => @columns}, :update => 'content'},
25 <%= link_to_remote l(:button_clear), {:url => {:project_id => @project, :date_from => @date_from, :date_to => @date_to, :period => @columns}, :update => 'content'},
26 :class => 'icon icon-reload' %></p>
26 :class => 'icon icon-reload' %></p>
27 <% end %>
27 <% end %>
28
28
29 <% unless @criterias.empty? %>
29 <% unless @criterias.empty? %>
30 <div class="total-hours">
30 <div class="total-hours">
31 <p><%= l(:label_total) %>: <%= html_hours(lwr(:label_f_hour, @total_hours)) %></p>
31 <p><%= l(:label_total) %>: <%= html_hours(lwr(:label_f_hour, @total_hours)) %></p>
32 </div>
32 </div>
33
33
34 <% unless @hours.empty? %>
34 <% unless @hours.empty? %>
35 <table class="list" id="time-report">
35 <table class="list" id="time-report">
36 <thead>
36 <thead>
37 <tr>
37 <tr>
38 <% @criterias.each do |criteria| %>
38 <% @criterias.each do |criteria| %>
39 <th width="15%"><%= l(@available_criterias[criteria][:label]) %></th>
39 <th><%= l(@available_criterias[criteria][:label]) %></th>
40 <% end %>
40 <% end %>
41 <% @periods.each do |period| %>
41 <% @periods.each do |period| %>
42 <th width="<%= ((100 - @criterias.length * 15 - 15 ) / @periods.length).to_i %>%"><%= period %></th>
42 <th class="period" width="<%= (40 / @periods.length).to_i %>%"><%= period %></th>
43 <% end %>
43 <% end %>
44 </tr>
44 </tr>
45 </thead>
45 </thead>
46 <tbody>
46 <tbody>
47 <%= render :partial => 'report_criteria', :locals => {:criterias => @criterias, :hours => @hours, :level => 0} %>
47 <%= render :partial => 'report_criteria', :locals => {:criterias => @criterias, :hours => @hours, :level => 0} %>
48 <tr class="total">
48 <tr class="total">
49 <td><%= l(:label_total) %></td>
49 <td><%= l(:label_total) %></td>
50 <%= '<td></td>' * (@criterias.size - 1) %>
50 <%= '<td></td>' * (@criterias.size - 1) %>
51 <% @periods.each do |period| -%>
51 <% @periods.each do |period| -%>
52 <% sum = sum_hours(select_hours(@hours, @columns, period.to_s)) %>
52 <% sum = sum_hours(select_hours(@hours, @columns, period.to_s)) %>
53 <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
53 <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
54 <% end -%>
54 <% end -%>
55 </tr>
55 </tr>
56 </tbody>
56 </tbody>
57 </table>
57 </table>
58 <% end %>
58 <% end %>
59 <% end %>
59 <% end %>
60
61 <% html_title l(:label_spent_time), l(:label_report) %>
62
@@ -1,578 +1,578
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: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
7 h4, .wiki h3 {font-size: 13px;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.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
12 #top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 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: 8px; font-weight: bold;}
21 #top-menu a {color: #fff; padding-right: 8px; font-weight: bold;}
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 2px 0px 0px;
35 margin: 0px 2px 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 font-weight: bold;
43 font-weight: bold;
44 margin: 0;
44 margin: 0;
45 padding: 4px 10px 4px 10px;
45 padding: 4px 10px 4px 10px;
46 }
46 }
47 #main-menu li a:hover {background:#759FCF; color:#fff;}
47 #main-menu li a:hover {background:#759FCF; color:#fff;}
48 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
48 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
49
49
50 #main {background-color:#EEEEEE;}
50 #main {background-color:#EEEEEE;}
51
51
52 #sidebar{ float: right; width: 17%; position: relative; z-index: 9; min-height: 600px; padding: 0; margin: 0;}
52 #sidebar{ float: right; width: 17%; position: relative; z-index: 9; min-height: 600px; padding: 0; margin: 0;}
53 * html #sidebar{ width: 17%; }
53 * html #sidebar{ width: 17%; }
54 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
54 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
55 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
55 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
56 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
56 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
57
57
58 #content { width: 80%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; height:600px; min-height: 600px;}
58 #content { width: 80%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; height:600px; min-height: 600px;}
59 * html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
59 * html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
60 html>body #content { height: auto; min-height: 600px; overflow: auto; }
60 html>body #content { height: auto; min-height: 600px; overflow: auto; }
61
61
62 #main.nosidebar #sidebar{ display: none; }
62 #main.nosidebar #sidebar{ display: none; }
63 #main.nosidebar #content{ width: auto; border-right: 0; }
63 #main.nosidebar #content{ width: auto; border-right: 0; }
64
64
65 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
65 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
66
66
67 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
67 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
68 #login-form table td {padding: 6px;}
68 #login-form table td {padding: 6px;}
69 #login-form label {font-weight: bold;}
69 #login-form label {font-weight: bold;}
70
70
71 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
71 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
72
72
73 /***** Links *****/
73 /***** Links *****/
74 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
74 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
75 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
75 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
76 a img{ border: 0; }
76 a img{ border: 0; }
77
77
78 a.issue.closed, .issue.closed a { text-decoration: line-through; }
78 a.issue.closed, .issue.closed a { text-decoration: line-through; }
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; vertical-align: top;}
83 table.list td { overflow: hidden; 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 table.list.issues { margin-top: 10px; }
87 table.list.issues { margin-top: 10px; }
88 tr.issue { text-align: center; white-space: nowrap; }
88 tr.issue { text-align: center; white-space: nowrap; }
89 tr.issue td.subject, tr.issue td.category { white-space: normal; }
89 tr.issue td.subject, tr.issue td.category { white-space: normal; }
90 tr.issue td.subject { text-align: left; }
90 tr.issue td.subject { text-align: left; }
91 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
91 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
92
92
93 tr.entry { border: 1px solid #f8f8f8; }
93 tr.entry { border: 1px solid #f8f8f8; }
94 tr.entry td { white-space: nowrap; }
94 tr.entry td { white-space: nowrap; }
95 tr.entry td.filename { width: 30%; }
95 tr.entry td.filename { width: 30%; }
96 tr.entry td.size { text-align: right; font-size: 90%; }
96 tr.entry td.size { text-align: right; font-size: 90%; }
97 tr.entry td.revision, tr.entry td.author { text-align: center; }
97 tr.entry td.revision, tr.entry td.author { text-align: center; }
98 tr.entry td.age { text-align: right; }
98 tr.entry td.age { text-align: right; }
99
99
100 tr.changeset td.author { text-align: center; width: 15%; }
100 tr.changeset td.author { text-align: center; width: 15%; }
101 tr.changeset td.committed_on { text-align: center; width: 15%; }
101 tr.changeset td.committed_on { text-align: center; width: 15%; }
102
102
103 tr.message { height: 2.6em; }
103 tr.message { height: 2.6em; }
104 tr.message td.last_message { font-size: 80%; }
104 tr.message td.last_message { font-size: 80%; }
105 tr.message.locked td.subject a { background-image: url(../images/locked.png); }
105 tr.message.locked td.subject a { background-image: url(../images/locked.png); }
106 tr.message.sticky td.subject a { background-image: url(../images/sticky.png); font-weight: bold; }
106 tr.message.sticky td.subject a { background-image: url(../images/sticky.png); font-weight: bold; }
107
107
108 tr.user td { width:13%; }
108 tr.user td { width:13%; }
109 tr.user td.email { width:18%; }
109 tr.user td.email { width:18%; }
110 tr.user td { white-space: nowrap; }
110 tr.user td { white-space: nowrap; }
111 tr.user.locked, tr.user.registered { color: #aaa; }
111 tr.user.locked, tr.user.registered { color: #aaa; }
112 tr.user.locked a, tr.user.registered a { color: #aaa; }
112 tr.user.locked a, tr.user.registered a { color: #aaa; }
113
113
114 tr.time-entry { text-align: center; white-space: nowrap; }
114 tr.time-entry { text-align: center; white-space: nowrap; }
115 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
115 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
116 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
116 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
117 td.hours .hours-dec { font-size: 0.9em; }
117 td.hours .hours-dec { font-size: 0.9em; }
118
118
119 table.list tbody tr:hover { background-color:#ffffdd; }
119 table.list tbody tr:hover { background-color:#ffffdd; }
120 table td {padding:2px;}
120 table td {padding:2px;}
121 table p {margin:0;}
121 table p {margin:0;}
122 .odd {background-color:#f6f7f8;}
122 .odd {background-color:#f6f7f8;}
123 .even {background-color: #fff;}
123 .even {background-color: #fff;}
124
124
125 .highlight { background-color: #FCFD8D;}
125 .highlight { background-color: #FCFD8D;}
126 .highlight.token-1 { background-color: #faa;}
126 .highlight.token-1 { background-color: #faa;}
127 .highlight.token-2 { background-color: #afa;}
127 .highlight.token-2 { background-color: #afa;}
128 .highlight.token-3 { background-color: #aaf;}
128 .highlight.token-3 { background-color: #aaf;}
129
129
130 .box{
130 .box{
131 padding:6px;
131 padding:6px;
132 margin-bottom: 10px;
132 margin-bottom: 10px;
133 background-color:#f6f6f6;
133 background-color:#f6f6f6;
134 color:#505050;
134 color:#505050;
135 line-height:1.5em;
135 line-height:1.5em;
136 border: 1px solid #e4e4e4;
136 border: 1px solid #e4e4e4;
137 }
137 }
138
138
139 div.square {
139 div.square {
140 border: 1px solid #999;
140 border: 1px solid #999;
141 float: left;
141 float: left;
142 margin: .3em .4em 0 .4em;
142 margin: .3em .4em 0 .4em;
143 overflow: hidden;
143 overflow: hidden;
144 width: .6em; height: .6em;
144 width: .6em; height: .6em;
145 }
145 }
146 .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;}
147 .contextual input {font-size:0.9em;}
147 .contextual input {font-size:0.9em;}
148
148
149 .splitcontentleft{float:left; width:49%;}
149 .splitcontentleft{float:left; width:49%;}
150 .splitcontentright{float:right; width:49%;}
150 .splitcontentright{float:right; width:49%;}
151 form {display: inline;}
151 form {display: inline;}
152 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
152 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
153 fieldset {border: 1px solid #e4e4e4; margin:0;}
153 fieldset {border: 1px solid #e4e4e4; margin:0;}
154 legend {color: #484848;}
154 legend {color: #484848;}
155 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
155 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
156 textarea.wiki-edit { width: 99%; }
156 textarea.wiki-edit { width: 99%; }
157 li p {margin-top: 0;}
157 li p {margin-top: 0;}
158 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;}
159 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
159 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
160 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
160 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
161
161
162 fieldset#filters .buttons { text-align: right; font-size: 0.9em; margin: 0 4px 0px 0; }
162 fieldset#filters .buttons { text-align: right; font-size: 0.9em; margin: 0 4px 0px 0; }
163
163
164 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
164 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
165 div#issue-changesets .changeset { padding: 4px;}
165 div#issue-changesets .changeset { padding: 4px;}
166 div#issue-changesets .changeset { border-bottom: 1px solid #ddd; }
166 div#issue-changesets .changeset { border-bottom: 1px solid #ddd; }
167 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
167 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
168
168
169 div#activity dl { margin-left: 2em; }
169 div#activity dl { margin-left: 2em; }
170 div#activity dd { margin-bottom: 1em; }
170 div#activity dd { margin-bottom: 1em; }
171 div#activity dt { margin-bottom: 1px; }
171 div#activity dt { margin-bottom: 1px; }
172 div#activity dt .time { color: #777; font-size: 80%; }
172 div#activity dt .time { color: #777; font-size: 80%; }
173 div#activity dd .description { font-style: italic; }
173 div#activity dd .description { font-style: italic; }
174 div#activity span.project:after { content: " -"; }
174 div#activity span.project:after { content: " -"; }
175
175
176 div#roadmap fieldset.related-issues { margin-bottom: 1em; }
176 div#roadmap fieldset.related-issues { margin-bottom: 1em; }
177 div#roadmap fieldset.related-issues ul { margin-top: 0.3em; margin-bottom: 0.3em; }
177 div#roadmap fieldset.related-issues ul { margin-top: 0.3em; margin-bottom: 0.3em; }
178 div#roadmap .wiki h1:first-child { display: none; }
178 div#roadmap .wiki h1:first-child { display: none; }
179 div#roadmap .wiki h1 { font-size: 120%; }
179 div#roadmap .wiki h1 { font-size: 120%; }
180 div#roadmap .wiki h2 { font-size: 110%; }
180 div#roadmap .wiki h2 { font-size: 110%; }
181
181
182 div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
182 div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
183 div#version-summary fieldset { margin-bottom: 1em; }
183 div#version-summary fieldset { margin-bottom: 1em; }
184 div#version-summary .total-hours { text-align: right; }
184 div#version-summary .total-hours { text-align: right; }
185
185
186 table#time-report td.hours { text-align: right; padding-right: 0.5em; }
186 table#time-report td.hours, table#time-report th.period { text-align: right; padding-right: 0.5em; }
187 table#time-report tbody tr { font-style: italic; color: #777; }
187 table#time-report tbody tr { font-style: italic; color: #777; }
188 table#time-report tbody tr.last-level { font-style: normal; color: #555; }
188 table#time-report tbody tr.last-level { font-style: normal; color: #555; }
189 table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
189 table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
190 table#time-report .hours-dec { font-size: 0.9em; }
190 table#time-report .hours-dec { font-size: 0.9em; }
191
191
192 .total-hours { font-size: 110%; font-weight: bold; }
192 .total-hours { font-size: 110%; font-weight: bold; }
193 .total-hours span.hours-int { font-size: 120%; }
193 .total-hours span.hours-int { font-size: 120%; }
194
194
195 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
195 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
196 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
196 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
197
197
198 .pagination {font-size: 90%}
198 .pagination {font-size: 90%}
199 p.pagination {margin-top:8px;}
199 p.pagination {margin-top:8px;}
200
200
201 /***** Tabular forms ******/
201 /***** Tabular forms ******/
202 .tabular p{
202 .tabular p{
203 margin: 0;
203 margin: 0;
204 padding: 5px 0 8px 0;
204 padding: 5px 0 8px 0;
205 padding-left: 180px; /*width of left column containing the label elements*/
205 padding-left: 180px; /*width of left column containing the label elements*/
206 height: 1%;
206 height: 1%;
207 clear:left;
207 clear:left;
208 }
208 }
209
209
210 .tabular label{
210 .tabular label{
211 font-weight: bold;
211 font-weight: bold;
212 float: left;
212 float: left;
213 text-align: right;
213 text-align: right;
214 margin-left: -180px; /*width of left column*/
214 margin-left: -180px; /*width of left column*/
215 width: 175px; /*width of labels. Should be smaller than left column to create some right
215 width: 175px; /*width of labels. Should be smaller than left column to create some right
216 margin*/
216 margin*/
217 }
217 }
218
218
219 .tabular label.floating{
219 .tabular label.floating{
220 font-weight: normal;
220 font-weight: normal;
221 margin-left: 0px;
221 margin-left: 0px;
222 text-align: left;
222 text-align: left;
223 width: 200px;
223 width: 200px;
224 }
224 }
225
225
226 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
226 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
227
227
228 .tabular.settings p{ padding-left: 300px; }
228 .tabular.settings p{ padding-left: 300px; }
229 .tabular.settings label{ margin-left: -300px; width: 295px; }
229 .tabular.settings label{ margin-left: -300px; width: 295px; }
230
230
231 .required {color: #bb0000;}
231 .required {color: #bb0000;}
232 .summary {font-style: italic;}
232 .summary {font-style: italic;}
233
233
234 #attachments_fields input[type=text] {margin-left: 8px; }
234 #attachments_fields input[type=text] {margin-left: 8px; }
235
235
236 div.attachments p { margin:4px 0 2px 0; }
236 div.attachments p { margin:4px 0 2px 0; }
237 div.attachments img { vertical-align: middle; }
237 div.attachments img { vertical-align: middle; }
238 div.attachments span.author { font-size: 0.9em; color: #888; }
238 div.attachments span.author { font-size: 0.9em; color: #888; }
239
239
240 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
240 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
241 .other-formats span + span:before { content: "| "; }
241 .other-formats span + span:before { content: "| "; }
242
242
243 a.feed { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
243 a.feed { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
244
244
245 /***** Flash & error messages ****/
245 /***** Flash & error messages ****/
246 #errorExplanation, div.flash, .nodata {
246 #errorExplanation, div.flash, .nodata {
247 padding: 4px 4px 4px 30px;
247 padding: 4px 4px 4px 30px;
248 margin-bottom: 12px;
248 margin-bottom: 12px;
249 font-size: 1.1em;
249 font-size: 1.1em;
250 border: 2px solid;
250 border: 2px solid;
251 }
251 }
252
252
253 div.flash {margin-top: 8px;}
253 div.flash {margin-top: 8px;}
254
254
255 div.flash.error, #errorExplanation {
255 div.flash.error, #errorExplanation {
256 background: url(../images/false.png) 8px 5px no-repeat;
256 background: url(../images/false.png) 8px 5px no-repeat;
257 background-color: #ffe3e3;
257 background-color: #ffe3e3;
258 border-color: #dd0000;
258 border-color: #dd0000;
259 color: #550000;
259 color: #550000;
260 }
260 }
261
261
262 div.flash.notice {
262 div.flash.notice {
263 background: url(../images/true.png) 8px 5px no-repeat;
263 background: url(../images/true.png) 8px 5px no-repeat;
264 background-color: #dfffdf;
264 background-color: #dfffdf;
265 border-color: #9fcf9f;
265 border-color: #9fcf9f;
266 color: #005f00;
266 color: #005f00;
267 }
267 }
268
268
269 .nodata {
269 .nodata {
270 text-align: center;
270 text-align: center;
271 background-color: #FFEBC1;
271 background-color: #FFEBC1;
272 border-color: #FDBF3B;
272 border-color: #FDBF3B;
273 color: #A6750C;
273 color: #A6750C;
274 }
274 }
275
275
276 #errorExplanation ul { font-size: 0.9em;}
276 #errorExplanation ul { font-size: 0.9em;}
277
277
278 /***** Ajax indicator ******/
278 /***** Ajax indicator ******/
279 #ajax-indicator {
279 #ajax-indicator {
280 position: absolute; /* fixed not supported by IE */
280 position: absolute; /* fixed not supported by IE */
281 background-color:#eee;
281 background-color:#eee;
282 border: 1px solid #bbb;
282 border: 1px solid #bbb;
283 top:35%;
283 top:35%;
284 left:40%;
284 left:40%;
285 width:20%;
285 width:20%;
286 font-weight:bold;
286 font-weight:bold;
287 text-align:center;
287 text-align:center;
288 padding:0.6em;
288 padding:0.6em;
289 z-index:100;
289 z-index:100;
290 filter:alpha(opacity=50);
290 filter:alpha(opacity=50);
291 opacity: 0.5;
291 opacity: 0.5;
292 }
292 }
293
293
294 html>body #ajax-indicator { position: fixed; }
294 html>body #ajax-indicator { position: fixed; }
295
295
296 #ajax-indicator span {
296 #ajax-indicator span {
297 background-position: 0% 40%;
297 background-position: 0% 40%;
298 background-repeat: no-repeat;
298 background-repeat: no-repeat;
299 background-image: url(../images/loading.gif);
299 background-image: url(../images/loading.gif);
300 padding-left: 26px;
300 padding-left: 26px;
301 vertical-align: bottom;
301 vertical-align: bottom;
302 }
302 }
303
303
304 /***** Calendar *****/
304 /***** Calendar *****/
305 table.cal {border-collapse: collapse; width: 100%; margin: 8px 0 6px 0;border: 1px solid #d7d7d7;}
305 table.cal {border-collapse: collapse; width: 100%; margin: 8px 0 6px 0;border: 1px solid #d7d7d7;}
306 table.cal thead th {width: 14%;}
306 table.cal thead th {width: 14%;}
307 table.cal tbody tr {height: 100px;}
307 table.cal tbody tr {height: 100px;}
308 table.cal th { background-color:#EEEEEE; padding: 4px; }
308 table.cal th { background-color:#EEEEEE; padding: 4px; }
309 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
309 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
310 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
310 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
311 table.cal td.odd p.day-num {color: #bbb;}
311 table.cal td.odd p.day-num {color: #bbb;}
312 table.cal td.today {background:#ffffdd;}
312 table.cal td.today {background:#ffffdd;}
313 table.cal td.today p.day-num {font-weight: bold;}
313 table.cal td.today p.day-num {font-weight: bold;}
314
314
315 /***** Tooltips ******/
315 /***** Tooltips ******/
316 .tooltip{position:relative;z-index:24;}
316 .tooltip{position:relative;z-index:24;}
317 .tooltip:hover{z-index:25;color:#000;}
317 .tooltip:hover{z-index:25;color:#000;}
318 .tooltip span.tip{display: none; text-align:left;}
318 .tooltip span.tip{display: none; text-align:left;}
319
319
320 div.tooltip:hover span.tip{
320 div.tooltip:hover span.tip{
321 display:block;
321 display:block;
322 position:absolute;
322 position:absolute;
323 top:12px; left:24px; width:270px;
323 top:12px; left:24px; width:270px;
324 border:1px solid #555;
324 border:1px solid #555;
325 background-color:#fff;
325 background-color:#fff;
326 padding: 4px;
326 padding: 4px;
327 font-size: 0.8em;
327 font-size: 0.8em;
328 color:#505050;
328 color:#505050;
329 }
329 }
330
330
331 /***** Progress bar *****/
331 /***** Progress bar *****/
332 table.progress {
332 table.progress {
333 border: 1px solid #D7D7D7;
333 border: 1px solid #D7D7D7;
334 border-collapse: collapse;
334 border-collapse: collapse;
335 border-spacing: 0pt;
335 border-spacing: 0pt;
336 empty-cells: show;
336 empty-cells: show;
337 text-align: center;
337 text-align: center;
338 float:left;
338 float:left;
339 margin: 1px 6px 1px 0px;
339 margin: 1px 6px 1px 0px;
340 }
340 }
341
341
342 table.progress td { height: 0.9em; }
342 table.progress td { height: 0.9em; }
343 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
343 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
344 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
344 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
345 table.progress td.open { background: #FFF none repeat scroll 0%; }
345 table.progress td.open { background: #FFF none repeat scroll 0%; }
346 p.pourcent {font-size: 80%;}
346 p.pourcent {font-size: 80%;}
347 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
347 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
348
348
349 /***** Tabs *****/
349 /***** Tabs *****/
350 #content .tabs {height: 2.6em; border-bottom: 1px solid #bbbbbb; margin-bottom:1.2em; position:relative;}
350 #content .tabs {height: 2.6em; border-bottom: 1px solid #bbbbbb; margin-bottom:1.2em; position:relative;}
351 #content .tabs ul {margin:0; position:absolute; bottom:-2px; padding-left:1em;}
351 #content .tabs ul {margin:0; position:absolute; bottom:-2px; padding-left:1em;}
352 #content .tabs>ul { bottom:-1px; } /* others */
352 #content .tabs>ul { bottom:-1px; } /* others */
353 #content .tabs ul li {
353 #content .tabs ul li {
354 float:left;
354 float:left;
355 list-style-type:none;
355 list-style-type:none;
356 white-space:nowrap;
356 white-space:nowrap;
357 margin-right:8px;
357 margin-right:8px;
358 background:#fff;
358 background:#fff;
359 }
359 }
360 #content .tabs ul li a{
360 #content .tabs ul li a{
361 display:block;
361 display:block;
362 font-size: 0.9em;
362 font-size: 0.9em;
363 text-decoration:none;
363 text-decoration:none;
364 line-height:1.3em;
364 line-height:1.3em;
365 padding:4px 6px 4px 6px;
365 padding:4px 6px 4px 6px;
366 border: 1px solid #ccc;
366 border: 1px solid #ccc;
367 border-bottom: 1px solid #bbbbbb;
367 border-bottom: 1px solid #bbbbbb;
368 background-color: #eeeeee;
368 background-color: #eeeeee;
369 color:#777;
369 color:#777;
370 font-weight:bold;
370 font-weight:bold;
371 }
371 }
372
372
373 #content .tabs ul li a:hover {
373 #content .tabs ul li a:hover {
374 background-color: #ffffdd;
374 background-color: #ffffdd;
375 text-decoration:none;
375 text-decoration:none;
376 }
376 }
377
377
378 #content .tabs ul li a.selected {
378 #content .tabs ul li a.selected {
379 background-color: #fff;
379 background-color: #fff;
380 border: 1px solid #bbbbbb;
380 border: 1px solid #bbbbbb;
381 border-bottom: 1px solid #fff;
381 border-bottom: 1px solid #fff;
382 }
382 }
383
383
384 #content .tabs ul li a.selected:hover {
384 #content .tabs ul li a.selected:hover {
385 background-color: #fff;
385 background-color: #fff;
386 }
386 }
387
387
388 /***** Diff *****/
388 /***** Diff *****/
389 .diff_out { background: #fcc; }
389 .diff_out { background: #fcc; }
390 .diff_in { background: #cfc; }
390 .diff_in { background: #cfc; }
391
391
392 /***** Wiki *****/
392 /***** Wiki *****/
393 div.wiki table {
393 div.wiki table {
394 border: 1px solid #505050;
394 border: 1px solid #505050;
395 border-collapse: collapse;
395 border-collapse: collapse;
396 margin-bottom: 1em;
396 margin-bottom: 1em;
397 }
397 }
398
398
399 div.wiki table, div.wiki td, div.wiki th {
399 div.wiki table, div.wiki td, div.wiki th {
400 border: 1px solid #bbb;
400 border: 1px solid #bbb;
401 padding: 4px;
401 padding: 4px;
402 }
402 }
403
403
404 div.wiki .external {
404 div.wiki .external {
405 background-position: 0% 60%;
405 background-position: 0% 60%;
406 background-repeat: no-repeat;
406 background-repeat: no-repeat;
407 padding-left: 12px;
407 padding-left: 12px;
408 background-image: url(../images/external.png);
408 background-image: url(../images/external.png);
409 }
409 }
410
410
411 div.wiki a.new {
411 div.wiki a.new {
412 color: #b73535;
412 color: #b73535;
413 }
413 }
414
414
415 div.wiki pre {
415 div.wiki pre {
416 margin: 1em 1em 1em 1.6em;
416 margin: 1em 1em 1em 1.6em;
417 padding: 2px;
417 padding: 2px;
418 background-color: #fafafa;
418 background-color: #fafafa;
419 border: 1px solid #dadada;
419 border: 1px solid #dadada;
420 width:95%;
420 width:95%;
421 overflow-x: auto;
421 overflow-x: auto;
422 }
422 }
423
423
424 div.wiki div.toc {
424 div.wiki div.toc {
425 background-color: #ffffdd;
425 background-color: #ffffdd;
426 border: 1px solid #e4e4e4;
426 border: 1px solid #e4e4e4;
427 padding: 4px;
427 padding: 4px;
428 line-height: 1.2em;
428 line-height: 1.2em;
429 margin-bottom: 12px;
429 margin-bottom: 12px;
430 margin-right: 12px;
430 margin-right: 12px;
431 display: table
431 display: table
432 }
432 }
433 * html div.wiki div.toc { width: 50%; } /* IE6 doesn't autosize div */
433 * html div.wiki div.toc { width: 50%; } /* IE6 doesn't autosize div */
434
434
435 div.wiki div.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
435 div.wiki div.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
436 div.wiki div.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
436 div.wiki div.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
437
437
438 div.wiki div.toc a {
438 div.wiki div.toc a {
439 display: block;
439 display: block;
440 font-size: 0.9em;
440 font-size: 0.9em;
441 font-weight: normal;
441 font-weight: normal;
442 text-decoration: none;
442 text-decoration: none;
443 color: #606060;
443 color: #606060;
444 }
444 }
445 div.wiki div.toc a:hover { color: #c61a1a; text-decoration: underline;}
445 div.wiki div.toc a:hover { color: #c61a1a; text-decoration: underline;}
446
446
447 div.wiki div.toc a.heading2 { margin-left: 6px; }
447 div.wiki div.toc a.heading2 { margin-left: 6px; }
448 div.wiki div.toc a.heading3 { margin-left: 12px; font-size: 0.8em; }
448 div.wiki div.toc a.heading3 { margin-left: 12px; font-size: 0.8em; }
449
449
450 /***** My page layout *****/
450 /***** My page layout *****/
451 .block-receiver {
451 .block-receiver {
452 border:1px dashed #c0c0c0;
452 border:1px dashed #c0c0c0;
453 margin-bottom: 20px;
453 margin-bottom: 20px;
454 padding: 15px 0 15px 0;
454 padding: 15px 0 15px 0;
455 }
455 }
456
456
457 .mypage-box {
457 .mypage-box {
458 margin:0 0 20px 0;
458 margin:0 0 20px 0;
459 color:#505050;
459 color:#505050;
460 line-height:1.5em;
460 line-height:1.5em;
461 }
461 }
462
462
463 .handle {
463 .handle {
464 cursor: move;
464 cursor: move;
465 }
465 }
466
466
467 a.close-icon {
467 a.close-icon {
468 display:block;
468 display:block;
469 margin-top:3px;
469 margin-top:3px;
470 overflow:hidden;
470 overflow:hidden;
471 width:12px;
471 width:12px;
472 height:12px;
472 height:12px;
473 background-repeat: no-repeat;
473 background-repeat: no-repeat;
474 cursor:pointer;
474 cursor:pointer;
475 background-image:url('../images/close.png');
475 background-image:url('../images/close.png');
476 }
476 }
477
477
478 a.close-icon:hover {
478 a.close-icon:hover {
479 background-image:url('../images/close_hl.png');
479 background-image:url('../images/close_hl.png');
480 }
480 }
481
481
482 /***** Gantt chart *****/
482 /***** Gantt chart *****/
483 .gantt_hdr {
483 .gantt_hdr {
484 position:absolute;
484 position:absolute;
485 top:0;
485 top:0;
486 height:16px;
486 height:16px;
487 border-top: 1px solid #c0c0c0;
487 border-top: 1px solid #c0c0c0;
488 border-bottom: 1px solid #c0c0c0;
488 border-bottom: 1px solid #c0c0c0;
489 border-right: 1px solid #c0c0c0;
489 border-right: 1px solid #c0c0c0;
490 text-align: center;
490 text-align: center;
491 overflow: hidden;
491 overflow: hidden;
492 }
492 }
493
493
494 .task {
494 .task {
495 position: absolute;
495 position: absolute;
496 height:8px;
496 height:8px;
497 font-size:0.8em;
497 font-size:0.8em;
498 color:#888;
498 color:#888;
499 padding:0;
499 padding:0;
500 margin:0;
500 margin:0;
501 line-height:0.8em;
501 line-height:0.8em;
502 }
502 }
503
503
504 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
504 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
505 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
505 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
506 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
506 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
507 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
507 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
508
508
509 /***** Icons *****/
509 /***** Icons *****/
510 .icon {
510 .icon {
511 background-position: 0% 40%;
511 background-position: 0% 40%;
512 background-repeat: no-repeat;
512 background-repeat: no-repeat;
513 padding-left: 20px;
513 padding-left: 20px;
514 padding-top: 2px;
514 padding-top: 2px;
515 padding-bottom: 3px;
515 padding-bottom: 3px;
516 }
516 }
517
517
518 .icon22 {
518 .icon22 {
519 background-position: 0% 40%;
519 background-position: 0% 40%;
520 background-repeat: no-repeat;
520 background-repeat: no-repeat;
521 padding-left: 26px;
521 padding-left: 26px;
522 line-height: 22px;
522 line-height: 22px;
523 vertical-align: middle;
523 vertical-align: middle;
524 }
524 }
525
525
526 .icon-add { background-image: url(../images/add.png); }
526 .icon-add { background-image: url(../images/add.png); }
527 .icon-edit { background-image: url(../images/edit.png); }
527 .icon-edit { background-image: url(../images/edit.png); }
528 .icon-copy { background-image: url(../images/copy.png); }
528 .icon-copy { background-image: url(../images/copy.png); }
529 .icon-del { background-image: url(../images/delete.png); }
529 .icon-del { background-image: url(../images/delete.png); }
530 .icon-move { background-image: url(../images/move.png); }
530 .icon-move { background-image: url(../images/move.png); }
531 .icon-save { background-image: url(../images/save.png); }
531 .icon-save { background-image: url(../images/save.png); }
532 .icon-cancel { background-image: url(../images/cancel.png); }
532 .icon-cancel { background-image: url(../images/cancel.png); }
533 .icon-file { background-image: url(../images/file.png); }
533 .icon-file { background-image: url(../images/file.png); }
534 .icon-folder { background-image: url(../images/folder.png); }
534 .icon-folder { background-image: url(../images/folder.png); }
535 .open .icon-folder { background-image: url(../images/folder_open.png); }
535 .open .icon-folder { background-image: url(../images/folder_open.png); }
536 .icon-package { background-image: url(../images/package.png); }
536 .icon-package { background-image: url(../images/package.png); }
537 .icon-home { background-image: url(../images/home.png); }
537 .icon-home { background-image: url(../images/home.png); }
538 .icon-user { background-image: url(../images/user.png); }
538 .icon-user { background-image: url(../images/user.png); }
539 .icon-mypage { background-image: url(../images/user_page.png); }
539 .icon-mypage { background-image: url(../images/user_page.png); }
540 .icon-admin { background-image: url(../images/admin.png); }
540 .icon-admin { background-image: url(../images/admin.png); }
541 .icon-projects { background-image: url(../images/projects.png); }
541 .icon-projects { background-image: url(../images/projects.png); }
542 .icon-logout { background-image: url(../images/logout.png); }
542 .icon-logout { background-image: url(../images/logout.png); }
543 .icon-help { background-image: url(../images/help.png); }
543 .icon-help { background-image: url(../images/help.png); }
544 .icon-attachment { background-image: url(../images/attachment.png); }
544 .icon-attachment { background-image: url(../images/attachment.png); }
545 .icon-index { background-image: url(../images/index.png); }
545 .icon-index { background-image: url(../images/index.png); }
546 .icon-history { background-image: url(../images/history.png); }
546 .icon-history { background-image: url(../images/history.png); }
547 .icon-time { background-image: url(../images/time.png); }
547 .icon-time { background-image: url(../images/time.png); }
548 .icon-stats { background-image: url(../images/stats.png); }
548 .icon-stats { background-image: url(../images/stats.png); }
549 .icon-warning { background-image: url(../images/warning.png); }
549 .icon-warning { background-image: url(../images/warning.png); }
550 .icon-fav { background-image: url(../images/fav.png); }
550 .icon-fav { background-image: url(../images/fav.png); }
551 .icon-fav-off { background-image: url(../images/fav_off.png); }
551 .icon-fav-off { background-image: url(../images/fav_off.png); }
552 .icon-reload { background-image: url(../images/reload.png); }
552 .icon-reload { background-image: url(../images/reload.png); }
553 .icon-lock { background-image: url(../images/locked.png); }
553 .icon-lock { background-image: url(../images/locked.png); }
554 .icon-unlock { background-image: url(../images/unlock.png); }
554 .icon-unlock { background-image: url(../images/unlock.png); }
555 .icon-checked { background-image: url(../images/true.png); }
555 .icon-checked { background-image: url(../images/true.png); }
556 .icon-details { background-image: url(../images/zoom_in.png); }
556 .icon-details { background-image: url(../images/zoom_in.png); }
557 .icon-report { background-image: url(../images/report.png); }
557 .icon-report { background-image: url(../images/report.png); }
558
558
559 .icon22-projects { background-image: url(../images/22x22/projects.png); }
559 .icon22-projects { background-image: url(../images/22x22/projects.png); }
560 .icon22-users { background-image: url(../images/22x22/users.png); }
560 .icon22-users { background-image: url(../images/22x22/users.png); }
561 .icon22-tracker { background-image: url(../images/22x22/tracker.png); }
561 .icon22-tracker { background-image: url(../images/22x22/tracker.png); }
562 .icon22-role { background-image: url(../images/22x22/role.png); }
562 .icon22-role { background-image: url(../images/22x22/role.png); }
563 .icon22-workflow { background-image: url(../images/22x22/workflow.png); }
563 .icon22-workflow { background-image: url(../images/22x22/workflow.png); }
564 .icon22-options { background-image: url(../images/22x22/options.png); }
564 .icon22-options { background-image: url(../images/22x22/options.png); }
565 .icon22-notifications { background-image: url(../images/22x22/notifications.png); }
565 .icon22-notifications { background-image: url(../images/22x22/notifications.png); }
566 .icon22-authent { background-image: url(../images/22x22/authent.png); }
566 .icon22-authent { background-image: url(../images/22x22/authent.png); }
567 .icon22-info { background-image: url(../images/22x22/info.png); }
567 .icon22-info { background-image: url(../images/22x22/info.png); }
568 .icon22-comment { background-image: url(../images/22x22/comment.png); }
568 .icon22-comment { background-image: url(../images/22x22/comment.png); }
569 .icon22-package { background-image: url(../images/22x22/package.png); }
569 .icon22-package { background-image: url(../images/22x22/package.png); }
570 .icon22-settings { background-image: url(../images/22x22/settings.png); }
570 .icon22-settings { background-image: url(../images/22x22/settings.png); }
571 .icon22-plugin { background-image: url(../images/22x22/plugin.png); }
571 .icon22-plugin { background-image: url(../images/22x22/plugin.png); }
572
572
573 /***** Media print specific styles *****/
573 /***** Media print specific styles *****/
574 @media print {
574 @media print {
575 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual { display:none; }
575 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual { display:none; }
576 #main { background: #fff; }
576 #main { background: #fff; }
577 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; }
577 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; }
578 }
578 }
@@ -1,171 +1,171
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, :roles, :members, :issues, :time_entries, :users, :trackers, :enumerations, :issue_statuses
25 fixtures :projects, :roles, :members, :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_create
33 def test_create
34 @request.session[:user_id] = 3
34 @request.session[:user_id] = 3
35 post :edit, :project_id => 1,
35 post :edit, :project_id => 1,
36 :time_entry => {:comments => 'Some work on TimelogControllerTest',
36 :time_entry => {:comments => 'Some work on TimelogControllerTest',
37 :activity_id => '10',
37 :activity_id => '10',
38 :spent_on => '2008-03-14',
38 :spent_on => '2008-03-14',
39 :issue_id => '1',
39 :issue_id => '1',
40 :hours => '7.3'}
40 :hours => '7.3'}
41 assert_redirected_to 'projects/ecookbook/timelog/details'
41 assert_redirected_to 'projects/ecookbook/timelog/details'
42
42
43 i = Issue.find(1)
43 i = Issue.find(1)
44 t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
44 t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
45 assert_not_nil t
45 assert_not_nil t
46 assert_equal 7.3, t.hours
46 assert_equal 7.3, t.hours
47 assert_equal 3, t.user_id
47 assert_equal 3, t.user_id
48 assert_equal i, t.issue
48 assert_equal i, t.issue
49 assert_equal i.project, t.project
49 assert_equal i.project, t.project
50 end
50 end
51
51
52 def test_update
52 def test_update
53 entry = TimeEntry.find(1)
53 entry = TimeEntry.find(1)
54 assert_equal 1, entry.issue_id
54 assert_equal 1, entry.issue_id
55 assert_equal 2, entry.user_id
55 assert_equal 2, entry.user_id
56
56
57 @request.session[:user_id] = 1
57 @request.session[:user_id] = 1
58 post :edit, :id => 1,
58 post :edit, :id => 1,
59 :time_entry => {:issue_id => '2',
59 :time_entry => {:issue_id => '2',
60 :hours => '8'}
60 :hours => '8'}
61 assert_redirected_to 'projects/ecookbook/timelog/details'
61 assert_redirected_to 'projects/ecookbook/timelog/details'
62 entry.reload
62 entry.reload
63
63
64 assert_equal 8, entry.hours
64 assert_equal 8, entry.hours
65 assert_equal 2, entry.issue_id
65 assert_equal 2, entry.issue_id
66 assert_equal 2, entry.user_id
66 assert_equal 2, entry.user_id
67 end
67 end
68
68
69 def destroy
69 def destroy
70 @request.session[:user_id] = 2
70 @request.session[:user_id] = 2
71 post :destroy, :id => 1
71 post :destroy, :id => 1
72 assert_redirected_to 'projects/ecookbook/timelog/details'
72 assert_redirected_to 'projects/ecookbook/timelog/details'
73 assert_nil TimeEntry.find_by_id(1)
73 assert_nil TimeEntry.find_by_id(1)
74 end
74 end
75
75
76 def test_report_no_criteria
76 def test_report_no_criteria
77 get :report, :project_id => 1
77 get :report, :project_id => 1
78 assert_response :success
78 assert_response :success
79 assert_template 'report'
79 assert_template 'report'
80 end
80 end
81
81
82 def test_report_all_time
82 def test_report_all_time
83 get :report, :project_id => 1, :criterias => ['project']
83 get :report, :project_id => 1, :criterias => ['project', 'issue']
84 assert_response :success
84 assert_response :success
85 assert_template 'report'
85 assert_template 'report'
86 assert_not_nil assigns(:total_hours)
86 assert_not_nil assigns(:total_hours)
87 assert_equal "162.90", "%.2f" % assigns(:total_hours)
87 assert_equal "162.90", "%.2f" % assigns(:total_hours)
88 end
88 end
89
89
90 def test_report_one_criteria
90 def test_report_one_criteria
91 get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project']
91 get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project']
92 assert_response :success
92 assert_response :success
93 assert_template 'report'
93 assert_template 'report'
94 assert_not_nil assigns(:total_hours)
94 assert_not_nil assigns(:total_hours)
95 assert_equal "8.65", "%.2f" % assigns(:total_hours)
95 assert_equal "8.65", "%.2f" % assigns(:total_hours)
96 end
96 end
97
97
98 def test_report_two_criterias
98 def test_report_two_criterias
99 get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criterias => ["member", "activity"]
99 get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criterias => ["member", "activity"]
100 assert_response :success
100 assert_response :success
101 assert_template 'report'
101 assert_template 'report'
102 assert_not_nil assigns(:total_hours)
102 assert_not_nil assigns(:total_hours)
103 assert_equal "162.90", "%.2f" % assigns(:total_hours)
103 assert_equal "162.90", "%.2f" % assigns(:total_hours)
104 end
104 end
105
105
106 def test_report_one_criteria_no_result
106 def test_report_one_criteria_no_result
107 get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criterias => ['project']
107 get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criterias => ['project']
108 assert_response :success
108 assert_response :success
109 assert_template 'report'
109 assert_template 'report'
110 assert_not_nil assigns(:total_hours)
110 assert_not_nil assigns(:total_hours)
111 assert_equal "0.00", "%.2f" % assigns(:total_hours)
111 assert_equal "0.00", "%.2f" % assigns(:total_hours)
112 end
112 end
113
113
114 def test_details_at_project_level
114 def test_details_at_project_level
115 get :details, :project_id => 1
115 get :details, :project_id => 1
116 assert_response :success
116 assert_response :success
117 assert_template 'details'
117 assert_template 'details'
118 assert_not_nil assigns(:entries)
118 assert_not_nil assigns(:entries)
119 assert_equal 4, assigns(:entries).size
119 assert_equal 4, assigns(:entries).size
120 # project and subproject
120 # project and subproject
121 assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
121 assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
122 assert_not_nil assigns(:total_hours)
122 assert_not_nil assigns(:total_hours)
123 assert_equal "162.90", "%.2f" % assigns(:total_hours)
123 assert_equal "162.90", "%.2f" % assigns(:total_hours)
124 # display all time by default
124 # display all time by default
125 assert_nil assigns(:from)
125 assert_nil assigns(:from)
126 assert_nil assigns(:to)
126 assert_nil assigns(:to)
127 end
127 end
128
128
129 def test_details_at_project_level_with_date_range
129 def test_details_at_project_level_with_date_range
130 get :details, :project_id => 1, :from => '2007-03-20', :to => '2007-04-30'
130 get :details, :project_id => 1, :from => '2007-03-20', :to => '2007-04-30'
131 assert_response :success
131 assert_response :success
132 assert_template 'details'
132 assert_template 'details'
133 assert_not_nil assigns(:entries)
133 assert_not_nil assigns(:entries)
134 assert_equal 3, assigns(:entries).size
134 assert_equal 3, assigns(:entries).size
135 assert_not_nil assigns(:total_hours)
135 assert_not_nil assigns(:total_hours)
136 assert_equal "12.90", "%.2f" % assigns(:total_hours)
136 assert_equal "12.90", "%.2f" % assigns(:total_hours)
137 assert_equal '2007-03-20'.to_date, assigns(:from)
137 assert_equal '2007-03-20'.to_date, assigns(:from)
138 assert_equal '2007-04-30'.to_date, assigns(:to)
138 assert_equal '2007-04-30'.to_date, assigns(:to)
139 end
139 end
140
140
141 def test_details_at_project_level_with_period
141 def test_details_at_project_level_with_period
142 get :details, :project_id => 1, :period => '7_days'
142 get :details, :project_id => 1, :period => '7_days'
143 assert_response :success
143 assert_response :success
144 assert_template 'details'
144 assert_template 'details'
145 assert_not_nil assigns(:entries)
145 assert_not_nil assigns(:entries)
146 assert_not_nil assigns(:total_hours)
146 assert_not_nil assigns(:total_hours)
147 assert_equal Date.today - 7, assigns(:from)
147 assert_equal Date.today - 7, assigns(:from)
148 assert_equal Date.today, assigns(:to)
148 assert_equal Date.today, assigns(:to)
149 end
149 end
150
150
151 def test_details_at_issue_level
151 def test_details_at_issue_level
152 get :details, :issue_id => 1
152 get :details, :issue_id => 1
153 assert_response :success
153 assert_response :success
154 assert_template 'details'
154 assert_template 'details'
155 assert_not_nil assigns(:entries)
155 assert_not_nil assigns(:entries)
156 assert_equal 2, assigns(:entries).size
156 assert_equal 2, assigns(:entries).size
157 assert_not_nil assigns(:total_hours)
157 assert_not_nil assigns(:total_hours)
158 assert_equal 154.25, assigns(:total_hours)
158 assert_equal 154.25, assigns(:total_hours)
159 # display all time by default
159 # display all time by default
160 assert_nil assigns(:from)
160 assert_nil assigns(:from)
161 assert_nil assigns(:to)
161 assert_nil assigns(:to)
162 end
162 end
163
163
164 def test_details_csv_export
164 def test_details_csv_export
165 get :details, :project_id => 1, :format => 'csv'
165 get :details, :project_id => 1, :format => 'csv'
166 assert_response :success
166 assert_response :success
167 assert_equal 'text/csv', @response.content_type
167 assert_equal 'text/csv', @response.content_type
168 assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment\n")
168 assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment\n")
169 assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,2,Feature request,Add ingredients categories,1.0,\"\"\n")
169 assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,2,Feature request,Add ingredients categories,1.0,\"\"\n")
170 end
170 end
171 end
171 end
General Comments 0
You need to be logged in to leave comments. Login now