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