##// END OF EJS Templates
Various timelog report enhancements:...
Jean-Philippe Lang -
r1311:4e961f44eff2
parent child
Show More
@@ -1,245 +1,242
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 48 :label => :label_activity},
49 49 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
50 50 :klass => Issue,
51 51 :label => :label_issue}
52 52 }
53 53
54 54 @criterias = params[:criterias] || []
55 55 @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
56 56 @criterias.uniq!
57 57 @criterias = @criterias[0,3]
58 58
59 @columns = (params[:columns] && %w(year month week).include?(params[:columns])) ? params[:columns] : 'month'
59 @columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
60 60
61 61 retrieve_date_range
62 @from ||= TimeEntry.minimum(: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
64 62
65 63 unless @criterias.empty?
66 64 sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
67 65 sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
68 66
69 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, SUM(hours) AS hours"
67 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
70 68 sql << " FROM #{TimeEntry.table_name}"
71 69 sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
72 70 sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
73 71 sql << " WHERE (%s)" % @project.project_condition(Setting.display_subprojects_issues?)
74 72 sql << " AND (%s)" % Project.allowed_to_condition(User.current, :view_time_entries)
75 73 sql << " AND spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)]
76 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek"
74 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
77 75
78 76 @hours = ActiveRecord::Base.connection.select_all(sql)
79 77
80 78 @hours.each do |row|
81 79 case @columns
82 80 when 'year'
83 81 row['year'] = row['tyear']
84 82 when 'month'
85 83 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
86 84 when 'week'
87 85 row['week'] = "#{row['tyear']}-#{row['tweek']}"
86 when 'day'
87 row['day'] = "#{row['spent_on']}"
88 88 end
89 89 end
90 90
91 91 @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
92 92
93 93 @periods = []
94 94 # Date#at_beginning_of_ not supported in Rails 1.2.x
95 95 date_from = @from.to_time
96 96 # 100 columns max
97 97 while date_from <= @to.to_time && @periods.length < 100
98 98 case @columns
99 99 when 'year'
100 100 @periods << "#{date_from.year}"
101 101 date_from = (date_from + 1.year).at_beginning_of_year
102 102 when 'month'
103 103 @periods << "#{date_from.year}-#{date_from.month}"
104 104 date_from = (date_from + 1.month).at_beginning_of_month
105 105 when 'week'
106 106 @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
107 107 date_from = (date_from + 7.day).at_beginning_of_week
108 when 'day'
109 @periods << "#{date_from.to_date}"
110 date_from = date_from + 1.day
108 111 end
109 112 end
110 113 end
111 114
112 115 render :layout => false if request.xhr?
113 116 end
114 117
115 118 def details
116 119 sort_init 'spent_on', 'desc'
117 120 sort_update
118 121
119 122 cond = ARCondition.new
120 123 cond << (@issue.nil? ? @project.project_condition(Setting.display_subprojects_issues?) :
121 124 ["#{TimeEntry.table_name}.issue_id = ?", @issue.id])
122 125
123 126 retrieve_date_range
124
125 if @from
126 if @to
127 127 cond << ['spent_on BETWEEN ? AND ?', @from, @to]
128 else
129 cond << ['spent_on >= ?', @from]
130 end
131 elsif @to
132 cond << ['spent_on <= ?', @to]
133 end
134 128
135 129 TimeEntry.visible_by(User.current) do
136 130 respond_to do |format|
137 131 format.html {
138 132 # Paginate results
139 133 @entry_count = TimeEntry.count(:include => :project, :conditions => cond.conditions)
140 134 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
141 135 @entries = TimeEntry.find(:all,
142 136 :include => [:project, :activity, :user, {:issue => :tracker}],
143 137 :conditions => cond.conditions,
144 138 :order => sort_clause,
145 139 :limit => @entry_pages.items_per_page,
146 140 :offset => @entry_pages.current.offset)
147 141 @total_hours = TimeEntry.sum(:hours, :include => :project, :conditions => cond.conditions).to_f
142
148 143 render :layout => !request.xhr?
149 144 }
150 145 format.csv {
151 146 # Export all entries
152 147 @entries = TimeEntry.find(:all,
153 148 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
154 149 :conditions => cond.conditions,
155 150 :order => sort_clause)
156 151 send_data(entries_to_csv(@entries).read, :type => 'text/csv; header=present', :filename => 'timelog.csv')
157 152 }
158 153 end
159 154 end
160 155 end
161 156
162 157 def edit
163 158 render_403 and return if @time_entry && !@time_entry.editable_by?(User.current)
164 159 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
165 160 @time_entry.attributes = params[:time_entry]
166 161 if request.post? and @time_entry.save
167 162 flash[:notice] = l(:notice_successful_update)
168 163 redirect_to :action => 'details', :project_id => @time_entry.project
169 164 return
170 165 end
171 166 @activities = Enumeration::get_values('ACTI')
172 167 end
173 168
174 169 def destroy
175 170 render_404 and return unless @time_entry
176 171 render_403 and return unless @time_entry.editable_by?(User.current)
177 172 @time_entry.destroy
178 173 flash[:notice] = l(:notice_successful_delete)
179 174 redirect_to :back
180 175 rescue RedirectBackError
181 176 redirect_to :action => 'details', :project_id => @time_entry.project
182 177 end
183 178
184 179 private
185 180 def find_project
186 181 if params[:id]
187 182 @time_entry = TimeEntry.find(params[:id])
188 183 @project = @time_entry.project
189 184 elsif params[:issue_id]
190 185 @issue = Issue.find(params[:issue_id])
191 186 @project = @issue.project
192 187 elsif params[:project_id]
193 188 @project = Project.find(params[:project_id])
194 189 else
195 190 render_404
196 191 return false
197 192 end
198 193 rescue ActiveRecord::RecordNotFound
199 194 render_404
200 195 end
201 196
202 197 # Retrieves the date range based on predefined ranges or specific from/to param dates
203 198 def retrieve_date_range
204 199 @free_period = false
205 200 @from, @to = nil, nil
206 201
207 202 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
208 203 case params[:period].to_s
209 204 when 'today'
210 205 @from = @to = Date.today
211 206 when 'yesterday'
212 207 @from = @to = Date.today - 1
213 208 when 'current_week'
214 209 @from = Date.today - (Date.today.cwday - 1)%7
215 210 @to = @from + 6
216 211 when 'last_week'
217 212 @from = Date.today - 7 - (Date.today.cwday - 1)%7
218 213 @to = @from + 6
219 214 when '7_days'
220 215 @from = Date.today - 7
221 216 @to = Date.today
222 217 when 'current_month'
223 218 @from = Date.civil(Date.today.year, Date.today.month, 1)
224 219 @to = (@from >> 1) - 1
225 220 when 'last_month'
226 221 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
227 222 @to = (@from >> 1) - 1
228 223 when '30_days'
229 224 @from = Date.today - 30
230 225 @to = Date.today
231 226 when 'current_year'
232 227 @from = Date.civil(Date.today.year, 1, 1)
233 228 @to = Date.civil(Date.today.year, 12, 31)
234 229 end
235 230 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
236 231 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
237 232 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
238 233 @free_period = true
239 234 else
240 235 # default
241 236 end
242 237
243 238 @from, @to = @to, @from if @from && @to && @from > @to
239 @from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today) - 1
240 @to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today)
244 241 end
245 242 end
@@ -1,16 +1,26
1 1 <fieldset><legend><%= l(:label_date_range) %></legend>
2 2 <p>
3 3 <%= radio_button_tag 'period_type', '1', !@free_period %>
4 4 <%= select_tag 'period', options_for_period_select(params[:period]),
5 5 :onchange => 'this.form.onsubmit();',
6 6 :onfocus => '$("period_type_1").checked = true;' %>
7 7 </p>
8 8 <p>
9 9 <%= radio_button_tag 'period_type', '2', @free_period %>
10 10 <%= l(:label_date_from) %>
11 11 <%= text_field_tag 'from', @from, :size => 10, :onfocus => '$("period_type_2").checked = true;' %> <%= calendar_for('from') %>
12 12 <%= l(:label_date_to) %>
13 13 <%= text_field_tag 'to', @to, :size => 10, :onfocus => '$("period_type_2").checked = true;' %> <%= calendar_for('to') %>
14 14 <%= submit_tag l(:button_apply), :name => nil, :onclick => '$("period_type_2").checked = true;' %>
15 15 </p>
16 16 </fieldset>
17
18 <div class="tabs">
19 <% url_params = @free_period ? { :from => @from, :to => @to } : { :period => params[:period] } %>
20 <ul>
21 <li><%= link_to(l(:label_details), url_params.merge({:controller => 'timelog', :action => 'details', :project_id => @project }),
22 :class => (@controller.action_name == 'details' ? 'selected' : nil)) %></li>
23 <li><%= link_to(l(:label_report), url_params.merge({:controller => 'timelog', :action => 'report', :project_id => @project}),
24 :class => (@controller.action_name == 'report' ? 'selected' : nil)) %></li>
25 </ul>
26 </div>
@@ -1,17 +1,19
1 1 <% @hours.collect {|h| h[criterias[level]]}.uniq.each do |value| %>
2 2 <% hours_for_value = select_hours(hours, criterias[level], value) -%>
3 3 <% next if hours_for_value.empty? -%>
4 4 <tr class="<%= cycle('odd', 'even') %> <%= 'last-level' unless criterias.length > level+1 %>">
5 5 <%= '<td></td>' * level %>
6 6 <td><%= value.nil? ? l(:label_none) : @available_criterias[criterias[level]][:klass].find_by_id(value) %></td>
7 7 <%= '<td></td>' * (criterias.length - level - 1) -%>
8 <% total = 0 -%>
8 9 <% @periods.each do |period| -%>
9 <% sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s)) %>
10 <% sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s)); total += sum -%>
10 11 <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
11 12 <% end -%>
13 <td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td>
12 14 </tr>
13 15 <% if criterias.length > level+1 -%>
14 16 <%= render(:partial => 'report_criteria', :locals => {:criterias => criterias, :hours => hours_for_value, :level => (level + 1)}) %>
15 17 <% end -%>
16 18
17 19 <% end %>
@@ -1,32 +1,31
1 1 <div class="contextual">
2 <%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}, :class => 'icon icon-report') %>
3 2 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time' %>
4 3 </div>
5 4
6 5 <h2><%= l(:label_spent_time) %></h2>
7 6
8 7 <% if @issue %>
9 8 <h3><%= link_to(@project.name, {:action => 'details', :project_id => @project}) %> / <%= link_to_issue(@issue) %></h3>
10 9 <% end %>
11 10
12 11 <% form_remote_tag( :url => {}, :method => :get, :update => 'content' ) do %>
13 12 <%= hidden_field_tag 'project_id', params[:project_id] %>
14 13 <%= hidden_field_tag 'issue_id', params[:issue_id] if @issue %>
15 14 <%= render :partial => 'date_range' %>
16 15 <% end %>
17 16
18 17 <div class="total-hours">
19 18 <p><%= l(:label_total) %>: <%= html_hours(lwr(:label_f_hour, @total_hours)) %></p>
20 19 </div>
21 20
22 21 <% unless @entries.empty? %>
23 22 <%= render :partial => 'list', :locals => { :entries => @entries }%>
24 23 <p class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></p>
25 24
26 25 <p class="other-formats">
27 26 <%= l(:label_export_to) %>
28 27 <span><%= link_to 'CSV', params.merge(:format => 'csv'), :class => 'csv' %></span>
29 28 </p>
30 29 <% end %>
31 30
32 31 <% html_title l(:label_spent_time), l(:label_details) %>
@@ -1,62 +1,67
1 1 <div class="contextual">
2 <%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}, :class => 'icon icon-details') %>
3 2 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time' %>
4 3 </div>
5 4
6 5 <h2><%= l(:label_spent_time) %></h2>
7 6
8 7 <% form_remote_tag(:url => {}, :update => 'content') do %>
9 8 <% @criterias.each do |criteria| %>
10 9 <%= hidden_field_tag 'criterias[]', criteria, :id => nil %>
11 10 <% end %>
12 11 <%= hidden_field_tag 'project_id', params[:project_id] %>
13 12 <%= render :partial => 'date_range' %>
14 13
15 14 <p><%= l(:label_details) %>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'],
16 15 [l(:label_month), 'month'],
17 [l(:label_week), 'week']], @columns),
16 [l(:label_week), 'week'],
17 [l(:label_day_plural).titleize, 'day']], @columns),
18 18 :onchange => "this.form.onsubmit();" %>
19 19
20 20 <%= l(:button_add) %>: <%= select_tag('criterias[]', options_for_select([[]] + (@available_criterias.keys - @criterias).collect{|k| [l(@available_criterias[k][:label]), k]}),
21 21 :onchange => "this.form.onsubmit();",
22 22 :style => 'width: 200px',
23 23 :id => nil,
24 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'},
26 :class => 'icon icon-reload' %></p>
25 <%= link_to_remote l(:button_clear), {:url => {:project_id => @project, :period_type => params[:period_type], :period => params[:period], :from => @from, :to => @to, :columns => @columns},
26 :update => 'content'
27 }, :class => 'icon icon-reload' %></p>
27 28 <% end %>
28 29
29 30 <% unless @criterias.empty? %>
30 31 <div class="total-hours">
31 32 <p><%= l(:label_total) %>: <%= html_hours(lwr(:label_f_hour, @total_hours)) %></p>
32 33 </div>
33 34
34 35 <% unless @hours.empty? %>
35 36 <table class="list" id="time-report">
36 37 <thead>
37 38 <tr>
38 39 <% @criterias.each do |criteria| %>
39 40 <th><%= l(@available_criterias[criteria][:label]) %></th>
40 41 <% end %>
42 <% columns_width = (40 / (@periods.length+1)).to_i %>
41 43 <% @periods.each do |period| %>
42 <th class="period" width="<%= (40 / @periods.length).to_i %>%"><%= period %></th>
44 <th class="period" width="<%= columns_width %>%"><%= period %></th>
43 45 <% end %>
46 <th class="total" width="<%= columns_width %>%"><%= l(:label_total) %></th>
44 47 </tr>
45 48 </thead>
46 49 <tbody>
47 50 <%= render :partial => 'report_criteria', :locals => {:criterias => @criterias, :hours => @hours, :level => 0} %>
48 51 <tr class="total">
49 52 <td><%= l(:label_total) %></td>
50 53 <%= '<td></td>' * (@criterias.size - 1) %>
54 <% total = 0 -%>
51 55 <% @periods.each do |period| -%>
52 <% sum = sum_hours(select_hours(@hours, @columns, period.to_s)) %>
56 <% sum = sum_hours(select_hours(@hours, @columns, period.to_s)); total += sum -%>
53 57 <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
54 58 <% end -%>
59 <td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td>
55 60 </tr>
56 61 </tbody>
57 62 </table>
58 63 <% end %>
59 64 <% end %>
60 65
61 66 <% html_title l(:label_spent_time), l(:label_report) %>
62 67
@@ -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, table#time-report th.period { text-align: right; padding-right: 0.5em; }
186 table#time-report td.hours, table#time-report th.period, table#time-report th.total { 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,58 +1,58
1 1 ---
2 2 time_entries_001:
3 3 created_on: 2007-03-23 12:54:18 +01:00
4 4 tweek: 12
5 5 tmonth: 3
6 6 project_id: 1
7 7 comments: My hours
8 8 updated_on: 2007-03-23 12:54:18 +01:00
9 9 activity_id: 9
10 10 spent_on: 2007-03-23
11 11 issue_id: 1
12 12 id: 1
13 13 hours: 4.25
14 14 user_id: 2
15 15 tyear: 2007
16 16 time_entries_002:
17 17 created_on: 2007-03-23 14:11:04 +01:00
18 tweek: 12
18 tweek: 11
19 19 tmonth: 3
20 20 project_id: 1
21 21 comments: ""
22 22 updated_on: 2007-03-23 14:11:04 +01:00
23 23 activity_id: 9
24 24 spent_on: 2007-03-12
25 25 issue_id: 1
26 26 id: 2
27 27 hours: 150.0
28 28 user_id: 1
29 29 tyear: 2007
30 30 time_entries_003:
31 31 created_on: 2007-04-21 12:20:48 +02:00
32 32 tweek: 16
33 33 tmonth: 4
34 34 project_id: 1
35 35 comments: ""
36 36 updated_on: 2007-04-21 12:20:48 +02:00
37 37 activity_id: 9
38 38 spent_on: 2007-04-21
39 39 issue_id: 2
40 40 id: 3
41 41 hours: 1.0
42 42 user_id: 1
43 43 tyear: 2007
44 44 time_entries_004:
45 45 created_on: 2007-04-22 12:20:48 +02:00
46 46 tweek: 16
47 47 tmonth: 4
48 48 project_id: 3
49 49 comments: Time spent on a subproject
50 50 updated_on: 2007-04-22 12:20:48 +02:00
51 51 activity_id: 10
52 52 spent_on: 2007-04-22
53 53 issue_id:
54 54 id: 4
55 55 hours: 7.65
56 56 user_id: 1
57 57 tyear: 2007
58 58 No newline at end of file
@@ -1,171 +1,180
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 fixtures :projects, :roles, :members, :issues, :time_entries, :users, :trackers, :enumerations, :issue_statuses
25 fixtures :projects, :enabled_modules, :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 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 def test_report_all_time_by_day
91 get :report, :project_id => 1, :criterias => ['project', 'issue'], :columns => 'day'
92 assert_response :success
93 assert_template 'report'
94 assert_not_nil assigns(:total_hours)
95 assert_equal "162.90", "%.2f" % assigns(:total_hours)
96 assert_tag :tag => 'th', :content => '2007-03-12'
97 end
98
90 99 def test_report_one_criteria
91 100 get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project']
92 101 assert_response :success
93 102 assert_template 'report'
94 103 assert_not_nil assigns(:total_hours)
95 104 assert_equal "8.65", "%.2f" % assigns(:total_hours)
96 105 end
97 106
98 107 def test_report_two_criterias
99 108 get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criterias => ["member", "activity"]
100 109 assert_response :success
101 110 assert_template 'report'
102 111 assert_not_nil assigns(:total_hours)
103 112 assert_equal "162.90", "%.2f" % assigns(:total_hours)
104 113 end
105 114
106 115 def test_report_one_criteria_no_result
107 116 get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criterias => ['project']
108 117 assert_response :success
109 118 assert_template 'report'
110 119 assert_not_nil assigns(:total_hours)
111 120 assert_equal "0.00", "%.2f" % assigns(:total_hours)
112 121 end
113 122
114 123 def test_details_at_project_level
115 124 get :details, :project_id => 1
116 125 assert_response :success
117 126 assert_template 'details'
118 127 assert_not_nil assigns(:entries)
119 128 assert_equal 4, assigns(:entries).size
120 129 # project and subproject
121 130 assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
122 131 assert_not_nil assigns(:total_hours)
123 132 assert_equal "162.90", "%.2f" % assigns(:total_hours)
124 133 # display all time by default
125 assert_nil assigns(:from)
126 assert_nil assigns(:to)
134 assert_equal '2007-03-11'.to_date, assigns(:from)
135 assert_equal '2007-04-22'.to_date, assigns(:to)
127 136 end
128 137
129 138 def test_details_at_project_level_with_date_range
130 139 get :details, :project_id => 1, :from => '2007-03-20', :to => '2007-04-30'
131 140 assert_response :success
132 141 assert_template 'details'
133 142 assert_not_nil assigns(:entries)
134 143 assert_equal 3, assigns(:entries).size
135 144 assert_not_nil assigns(:total_hours)
136 145 assert_equal "12.90", "%.2f" % assigns(:total_hours)
137 146 assert_equal '2007-03-20'.to_date, assigns(:from)
138 147 assert_equal '2007-04-30'.to_date, assigns(:to)
139 148 end
140 149
141 150 def test_details_at_project_level_with_period
142 151 get :details, :project_id => 1, :period => '7_days'
143 152 assert_response :success
144 153 assert_template 'details'
145 154 assert_not_nil assigns(:entries)
146 155 assert_not_nil assigns(:total_hours)
147 156 assert_equal Date.today - 7, assigns(:from)
148 157 assert_equal Date.today, assigns(:to)
149 158 end
150 159
151 160 def test_details_at_issue_level
152 161 get :details, :issue_id => 1
153 162 assert_response :success
154 163 assert_template 'details'
155 164 assert_not_nil assigns(:entries)
156 165 assert_equal 2, assigns(:entries).size
157 166 assert_not_nil assigns(:total_hours)
158 167 assert_equal 154.25, assigns(:total_hours)
159 168 # display all time by default
160 assert_nil assigns(:from)
161 assert_nil assigns(:to)
169 assert_equal '2007-03-11'.to_date, assigns(:from)
170 assert_equal '2007-04-22'.to_date, assigns(:to)
162 171 end
163 172
164 173 def test_details_csv_export
165 174 get :details, :project_id => 1, :format => 'csv'
166 175 assert_response :success
167 176 assert_equal 'text/csv', @response.content_type
168 177 assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment\n")
169 178 assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,2,Feature request,Add ingredients categories,1.0,\"\"\n")
170 179 end
171 180 end
General Comments 0
You need to be logged in to leave comments. Login now