Revision 1297:0a574315af3e .svn/pristine/b0
| .svn/pristine/b0/b0017ec26c9070575c2b102bc7db593493d5910c.svn-base | ||
|---|---|---|
| 1 |
# Redmine - project management software |
|
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang |
|
| 3 |
# |
|
| 4 |
# This program is free software; you can redistribute it and/or |
|
| 5 |
# modify it under the terms of the GNU General Public License |
|
| 6 |
# as published by the Free Software Foundation; either version 2 |
|
| 7 |
# of the License, or (at your option) any later version. |
|
| 8 |
# |
|
| 9 |
# This program is distributed in the hope that it will be useful, |
|
| 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 12 |
# GNU General Public License for more details. |
|
| 13 |
# |
|
| 14 |
# You should have received a copy of the GNU General Public License |
|
| 15 |
# along with this program; if not, write to the Free Software |
|
| 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
| 17 |
|
|
| 18 |
class Board < ActiveRecord::Base |
|
| 19 |
include Redmine::SafeAttributes |
|
| 20 |
belongs_to :project |
|
| 21 |
has_many :topics, :class_name => 'Message', :conditions => "#{Message.table_name}.parent_id IS NULL", :order => "#{Message.table_name}.created_on DESC"
|
|
| 22 |
has_many :messages, :dependent => :destroy, :order => "#{Message.table_name}.created_on DESC"
|
|
| 23 |
belongs_to :last_message, :class_name => 'Message', :foreign_key => :last_message_id |
|
| 24 |
acts_as_tree :dependent => :nullify |
|
| 25 |
acts_as_list :scope => '(project_id = #{project_id} AND parent_id #{parent_id ? "= #{parent_id}" : "IS NULL"})'
|
|
| 26 |
acts_as_watchable |
|
| 27 |
|
|
| 28 |
validates_presence_of :name, :description |
|
| 29 |
validates_length_of :name, :maximum => 30 |
|
| 30 |
validates_length_of :description, :maximum => 255 |
|
| 31 |
validate :validate_board |
|
| 32 |
|
|
| 33 |
scope :visible, lambda {|*args| { :include => :project,
|
|
| 34 |
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_messages, *args) } } |
|
| 35 |
|
|
| 36 |
safe_attributes 'name', 'description', 'parent_id', 'move_to' |
|
| 37 |
|
|
| 38 |
def visible?(user=User.current) |
|
| 39 |
!user.nil? && user.allowed_to?(:view_messages, project) |
|
| 40 |
end |
|
| 41 |
|
|
| 42 |
def reload(*args) |
|
| 43 |
@valid_parents = nil |
|
| 44 |
super |
|
| 45 |
end |
|
| 46 |
|
|
| 47 |
def to_s |
|
| 48 |
name |
|
| 49 |
end |
|
| 50 |
|
|
| 51 |
def valid_parents |
|
| 52 |
@valid_parents ||= project.boards - self_and_descendants |
|
| 53 |
end |
|
| 54 |
|
|
| 55 |
def reset_counters! |
|
| 56 |
self.class.reset_counters!(id) |
|
| 57 |
end |
|
| 58 |
|
|
| 59 |
# Updates topics_count, messages_count and last_message_id attributes for +board_id+ |
|
| 60 |
def self.reset_counters!(board_id) |
|
| 61 |
board_id = board_id.to_i |
|
| 62 |
update_all("topics_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=#{board_id} AND parent_id IS NULL)," +
|
|
| 63 |
" messages_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=#{board_id})," +
|
|
| 64 |
" last_message_id = (SELECT MAX(id) FROM #{Message.table_name} WHERE board_id=#{board_id})",
|
|
| 65 |
["id = ?", board_id]) |
|
| 66 |
end |
|
| 67 |
|
|
| 68 |
def self.board_tree(boards, parent_id=nil, level=0) |
|
| 69 |
tree = [] |
|
| 70 |
boards.select {|board| board.parent_id == parent_id}.sort_by(&:position).each do |board|
|
|
| 71 |
tree << [board, level] |
|
| 72 |
tree += board_tree(boards, board.id, level+1) |
|
| 73 |
end |
|
| 74 |
if block_given? |
|
| 75 |
tree.each do |board, level| |
|
| 76 |
yield board, level |
|
| 77 |
end |
|
| 78 |
end |
|
| 79 |
tree |
|
| 80 |
end |
|
| 81 |
|
|
| 82 |
protected |
|
| 83 |
|
|
| 84 |
def validate_board |
|
| 85 |
if parent_id && parent_id_changed? |
|
| 86 |
errors.add(:parent_id, :invalid) unless valid_parents.include?(parent) |
|
| 87 |
end |
|
| 88 |
end |
|
| 89 |
end |
|
| .svn/pristine/b0/b068921bb34f750331c44877f44180d7da10bc0f.svn-base | ||
|---|---|---|
| 1 |
<%= form_tag({:action => 'edit', :tab => 'authentication'}) do %>
|
|
| 2 |
|
|
| 3 |
<div class="box tabular settings"> |
|
| 4 |
<p><%= setting_check_box :login_required %></p> |
|
| 5 |
|
|
| 6 |
<p><%= setting_select :autologin, [[l(:label_disabled), 0]] + [1, 7, 30, 365].collect{|days| [l('datetime.distance_in_words.x_days', :count => days), days.to_s]} %></p>
|
|
| 7 |
|
|
| 8 |
<p><%= setting_select :self_registration, [[l(:label_disabled), "0"], |
|
| 9 |
[l(:label_registration_activation_by_email), "1"], |
|
| 10 |
[l(:label_registration_manual_activation), "2"], |
|
| 11 |
[l(:label_registration_automatic_activation), "3"]] %></p> |
|
| 12 |
|
|
| 13 |
<p><%= setting_check_box :unsubscribe %></p> |
|
| 14 |
|
|
| 15 |
<p><%= setting_text_field :password_min_length, :size => 6 %></p> |
|
| 16 |
|
|
| 17 |
<p><%= setting_check_box :lost_password, :label => :label_password_lost %></p> |
|
| 18 |
|
|
| 19 |
<p><%= setting_check_box :openid, :disabled => !Object.const_defined?(:OpenID) %></p> |
|
| 20 |
|
|
| 21 |
<p><%= setting_check_box :rest_api_enabled %></p> |
|
| 22 |
</div> |
|
| 23 |
|
|
| 24 |
<fieldset class="box"> |
|
| 25 |
<legend><%= l(:label_session_expiration) %></legend> |
|
| 26 |
|
|
| 27 |
<div class="tabular settings"> |
|
| 28 |
<p><%= setting_select :session_lifetime, [[l(:label_disabled), 0]] + [1, 7, 30, 60, 365].collect{|days| [l('datetime.distance_in_words.x_days', :count => days), (days * 60 * 24).to_s]} %></p>
|
|
| 29 |
<p><%= setting_select :session_timeout, [[l(:label_disabled), 0]] + [1, 2, 4, 8, 12, 24, 48].collect{|hours| [l('datetime.distance_in_words.x_hours', :count => hours), (hours * 60).to_s]} %></p>
|
|
| 30 |
</div> |
|
| 31 |
|
|
| 32 |
<p><em class="info"><%= l(:text_session_expiration_settings) %></em></p> |
|
| 33 |
</fieldset> |
|
| 34 |
|
|
| 35 |
<%= submit_tag l(:button_save) %> |
|
| 36 |
<% end %> |
|
| .svn/pristine/b0/b08b8359ba61a19a3531e1766fc73fb2077273a2.svn-base | ||
|---|---|---|
| 1 |
# Redmine - project management software |
|
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang |
|
| 3 |
# |
|
| 4 |
# This program is free software; you can redistribute it and/or |
|
| 5 |
# modify it under the terms of the GNU General Public License |
|
| 6 |
# as published by the Free Software Foundation; either version 2 |
|
| 7 |
# of the License, or (at your option) any later version. |
|
| 8 |
# |
|
| 9 |
# This program is distributed in the hope that it will be useful, |
|
| 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 12 |
# GNU General Public License for more details. |
|
| 13 |
# |
|
| 14 |
# You should have received a copy of the GNU General Public License |
|
| 15 |
# along with this program; if not, write to the Free Software |
|
| 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
| 17 |
|
|
| 18 |
module Redmine |
|
| 19 |
module Acts |
|
| 20 |
module Customizable |
|
| 21 |
def self.included(base) |
|
| 22 |
base.extend ClassMethods |
|
| 23 |
end |
|
| 24 |
|
|
| 25 |
module ClassMethods |
|
| 26 |
def acts_as_customizable(options = {})
|
|
| 27 |
return if self.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods) |
|
| 28 |
cattr_accessor :customizable_options |
|
| 29 |
self.customizable_options = options |
|
| 30 |
has_many :custom_values, :as => :customized, |
|
| 31 |
:include => :custom_field, |
|
| 32 |
:order => "#{CustomField.table_name}.position",
|
|
| 33 |
:dependent => :delete_all, |
|
| 34 |
:validate => false |
|
| 35 |
send :include, Redmine::Acts::Customizable::InstanceMethods |
|
| 36 |
validate :validate_custom_field_values |
|
| 37 |
after_save :save_custom_field_values |
|
| 38 |
end |
|
| 39 |
end |
|
| 40 |
|
|
| 41 |
module InstanceMethods |
|
| 42 |
def self.included(base) |
|
| 43 |
base.extend ClassMethods |
|
| 44 |
end |
|
| 45 |
|
|
| 46 |
def available_custom_fields |
|
| 47 |
CustomField.find(:all, :conditions => "type = '#{self.class.name}CustomField'",
|
|
| 48 |
:order => 'position') |
|
| 49 |
end |
|
| 50 |
|
|
| 51 |
# Sets the values of the object's custom fields |
|
| 52 |
# values is an array like [{'id' => 1, 'value' => 'foo'}, {'id' => 2, 'value' => 'bar'}]
|
|
| 53 |
def custom_fields=(values) |
|
| 54 |
values_to_hash = values.inject({}) do |hash, v|
|
|
| 55 |
v = v.stringify_keys |
|
| 56 |
if v['id'] && v.has_key?('value')
|
|
| 57 |
hash[v['id']] = v['value'] |
|
| 58 |
end |
|
| 59 |
hash |
|
| 60 |
end |
|
| 61 |
self.custom_field_values = values_to_hash |
|
| 62 |
end |
|
| 63 |
|
|
| 64 |
# Sets the values of the object's custom fields |
|
| 65 |
# values is a hash like {'1' => 'foo', 2 => 'bar'}
|
|
| 66 |
def custom_field_values=(values) |
|
| 67 |
values = values.stringify_keys |
|
| 68 |
|
|
| 69 |
custom_field_values.each do |custom_field_value| |
|
| 70 |
key = custom_field_value.custom_field_id.to_s |
|
| 71 |
if values.has_key?(key) |
|
| 72 |
value = values[key] |
|
| 73 |
if value.is_a?(Array) |
|
| 74 |
value = value.reject(&:blank?).uniq |
|
| 75 |
if value.empty? |
|
| 76 |
value << '' |
|
| 77 |
end |
|
| 78 |
end |
|
| 79 |
custom_field_value.value = value |
|
| 80 |
end |
|
| 81 |
end |
|
| 82 |
@custom_field_values_changed = true |
|
| 83 |
end |
|
| 84 |
|
|
| 85 |
def custom_field_values |
|
| 86 |
@custom_field_values ||= available_custom_fields.collect do |field| |
|
| 87 |
x = CustomFieldValue.new |
|
| 88 |
x.custom_field = field |
|
| 89 |
x.customized = self |
|
| 90 |
if field.multiple? |
|
| 91 |
values = custom_values.select { |v| v.custom_field == field }
|
|
| 92 |
if values.empty? |
|
| 93 |
values << custom_values.build(:customized => self, :custom_field => field, :value => nil) |
|
| 94 |
end |
|
| 95 |
x.value = values.map(&:value) |
|
| 96 |
else |
|
| 97 |
cv = custom_values.detect { |v| v.custom_field == field }
|
|
| 98 |
cv ||= custom_values.build(:customized => self, :custom_field => field, :value => nil) |
|
| 99 |
x.value = cv.value |
|
| 100 |
end |
|
| 101 |
x |
|
| 102 |
end |
|
| 103 |
end |
|
| 104 |
|
|
| 105 |
def visible_custom_field_values |
|
| 106 |
custom_field_values.select(&:visible?) |
|
| 107 |
end |
|
| 108 |
|
|
| 109 |
def custom_field_values_changed? |
|
| 110 |
@custom_field_values_changed == true |
|
| 111 |
end |
|
| 112 |
|
|
| 113 |
def custom_value_for(c) |
|
| 114 |
field_id = (c.is_a?(CustomField) ? c.id : c.to_i) |
|
| 115 |
custom_values.detect {|v| v.custom_field_id == field_id }
|
|
| 116 |
end |
|
| 117 |
|
|
| 118 |
def custom_field_value(c) |
|
| 119 |
field_id = (c.is_a?(CustomField) ? c.id : c.to_i) |
|
| 120 |
custom_field_values.detect {|v| v.custom_field_id == field_id }.try(:value)
|
|
| 121 |
end |
|
| 122 |
|
|
| 123 |
def validate_custom_field_values |
|
| 124 |
if new_record? || custom_field_values_changed? |
|
| 125 |
custom_field_values.each(&:validate_value) |
|
| 126 |
end |
|
| 127 |
end |
|
| 128 |
|
|
| 129 |
def save_custom_field_values |
|
| 130 |
target_custom_values = [] |
|
| 131 |
custom_field_values.each do |custom_field_value| |
|
| 132 |
if custom_field_value.value.is_a?(Array) |
|
| 133 |
custom_field_value.value.each do |v| |
|
| 134 |
target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field && cv.value == v}
|
|
| 135 |
target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field, :value => v) |
|
| 136 |
target_custom_values << target |
|
| 137 |
end |
|
| 138 |
else |
|
| 139 |
target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field}
|
|
| 140 |
target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field) |
|
| 141 |
target.value = custom_field_value.value |
|
| 142 |
target_custom_values << target |
|
| 143 |
end |
|
| 144 |
end |
|
| 145 |
self.custom_values = target_custom_values |
|
| 146 |
custom_values.each(&:save) |
|
| 147 |
@custom_field_values_changed = false |
|
| 148 |
true |
|
| 149 |
end |
|
| 150 |
|
|
| 151 |
def reset_custom_values! |
|
| 152 |
@custom_field_values = nil |
|
| 153 |
@custom_field_values_changed = true |
|
| 154 |
end |
|
| 155 |
|
|
| 156 |
module ClassMethods |
|
| 157 |
end |
|
| 158 |
end |
|
| 159 |
end |
|
| 160 |
end |
|
| 161 |
end |
|
| .svn/pristine/b0/b09c0572e3793383576d19f17f4abdf1d9495a71.svn-base | ||
|---|---|---|
| 1 |
scope1: |
|
| 2 |
id: 1 |
|
| 3 |
body: Top Level |
|
| 4 |
lft: 1 |
|
| 5 |
rgt: 10 |
|
| 6 |
notable_id: 1 |
|
| 7 |
notable_type: Category |
|
| 8 |
child_1: |
|
| 9 |
id: 2 |
|
| 10 |
body: Child 1 |
|
| 11 |
parent_id: 1 |
|
| 12 |
lft: 2 |
|
| 13 |
rgt: 3 |
|
| 14 |
notable_id: 1 |
|
| 15 |
notable_type: Category |
|
| 16 |
child_2: |
|
| 17 |
id: 3 |
|
| 18 |
body: Child 2 |
|
| 19 |
parent_id: 1 |
|
| 20 |
lft: 4 |
|
| 21 |
rgt: 7 |
|
| 22 |
notable_id: 1 |
|
| 23 |
notable_type: Category |
|
| 24 |
child_3: |
|
| 25 |
id: 4 |
|
| 26 |
body: Child 3 |
|
| 27 |
parent_id: 1 |
|
| 28 |
lft: 8 |
|
| 29 |
rgt: 9 |
|
| 30 |
notable_id: 1 |
|
| 31 |
notable_type: Category |
|
| 32 |
scope2: |
|
| 33 |
id: 5 |
|
| 34 |
body: Top Level 2 |
|
| 35 |
lft: 1 |
|
| 36 |
rgt: 2 |
|
| 37 |
notable_id: 1 |
|
| 38 |
notable_type: Departments |
|
| .svn/pristine/b0/b0bccc208d7807b84f7eb5ca0db0c75765ec033a.svn-base | ||
|---|---|---|
| 1 |
# Redmine - project management software |
|
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang |
|
| 3 |
# |
|
| 4 |
# This program is free software; you can redistribute it and/or |
|
| 5 |
# modify it under the terms of the GNU General Public License |
|
| 6 |
# as published by the Free Software Foundation; either version 2 |
|
| 7 |
# of the License, or (at your option) any later version. |
|
| 8 |
# |
|
| 9 |
# This program is distributed in the hope that it will be useful, |
|
| 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 12 |
# GNU General Public License for more details. |
|
| 13 |
# |
|
| 14 |
# You should have received a copy of the GNU General Public License |
|
| 15 |
# along with this program; if not, write to the Free Software |
|
| 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
| 17 |
|
|
| 18 |
require File.expand_path('../../test_helper', __FILE__)
|
|
| 19 |
|
|
| 20 |
class CommentTest < ActiveSupport::TestCase |
|
| 21 |
fixtures :users, :news, :comments, :projects, :enabled_modules |
|
| 22 |
|
|
| 23 |
def setup |
|
| 24 |
@jsmith = User.find(2) |
|
| 25 |
@news = News.find(1) |
|
| 26 |
end |
|
| 27 |
|
|
| 28 |
def test_create |
|
| 29 |
comment = Comment.new(:commented => @news, :author => @jsmith, :comments => "my comment") |
|
| 30 |
assert comment.save |
|
| 31 |
@news.reload |
|
| 32 |
assert_equal 2, @news.comments_count |
|
| 33 |
end |
|
| 34 |
|
|
| 35 |
def test_create_should_send_notification |
|
| 36 |
Watcher.create!(:watchable => @news, :user => @jsmith) |
|
| 37 |
|
|
| 38 |
with_settings :notified_events => %w(news_comment_added) do |
|
| 39 |
assert_difference 'ActionMailer::Base.deliveries.size' do |
|
| 40 |
Comment.create!(:commented => @news, :author => @jsmith, :comments => "my comment") |
|
| 41 |
end |
|
| 42 |
end |
|
| 43 |
end |
|
| 44 |
|
|
| 45 |
def test_validate |
|
| 46 |
comment = Comment.new(:commented => @news) |
|
| 47 |
assert !comment.save |
|
| 48 |
assert_equal 2, comment.errors.count |
|
| 49 |
end |
|
| 50 |
|
|
| 51 |
def test_destroy |
|
| 52 |
comment = Comment.find(1) |
|
| 53 |
assert comment.destroy |
|
| 54 |
@news.reload |
|
| 55 |
assert_equal 0, @news.comments_count |
|
| 56 |
end |
|
| 57 |
end |
|
| .svn/pristine/b0/b0dd7f2e6e380d76ce0d76b315917633345e67d4.svn-base | ||
|---|---|---|
| 1 |
<div class="contextual"> |
|
| 2 |
<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'new', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time-add' %>
|
|
| 3 |
</div> |
|
| 4 |
|
|
| 5 |
<%= render_timelog_breadcrumb %> |
|
| 6 |
|
|
| 7 |
<h2><%= l(:label_spent_time) %></h2> |
|
| 8 |
|
|
| 9 |
<%= form_tag({:controller => 'timelog', :action => 'report',
|
|
| 10 |
:project_id => @project, :issue_id => @issue}, |
|
| 11 |
:method => :get, :id => 'query_form') do %> |
|
| 12 |
<% @report.criteria.each do |criterion| %> |
|
| 13 |
<%= hidden_field_tag 'criteria[]', criterion, :id => nil %> |
|
| 14 |
<% end %> |
|
| 15 |
<%= render :partial => 'timelog/date_range' %> |
|
| 16 |
|
|
| 17 |
<p><label for='columns'><%= l(:label_details) %></label>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'], |
|
| 18 |
[l(:label_month), 'month'], |
|
| 19 |
[l(:label_week), 'week'], |
|
| 20 |
[l(:label_day_plural).titleize, 'day']], @report.columns), |
|
| 21 |
:onchange => "this.form.submit();" %> |
|
| 22 |
|
|
| 23 |
<label for='criterias'><%= l(:button_add) %></label>: <%= select_tag('criteria[]', options_for_select([[]] + (@report.available_criteria.keys - @report.criteria).collect{|k| [l_or_humanize(@report.available_criteria[k][:label]), k]}),
|
|
| 24 |
:onchange => "this.form.submit();", |
|
| 25 |
:style => 'width: 200px', |
|
| 26 |
:id => nil, |
|
| 27 |
:disabled => (@report.criteria.length >= 3), :id => "criterias") %> |
|
| 28 |
<%= link_to l(:button_clear), {:project_id => @project, :issue_id => @issue, :period_type => params[:period_type], :period => params[:period], :from => @from, :to => @to, :columns => @report.columns}, :class => 'icon icon-reload' %></p>
|
|
| 29 |
<% end %> |
|
| 30 |
|
|
| 31 |
<% unless @report.criteria.empty? %> |
|
| 32 |
<div class="total-hours"> |
|
| 33 |
<p><%= l(:label_total) %>: <%= html_hours(l_hours(@report.total_hours)) %></p> |
|
| 34 |
</div> |
|
| 35 |
|
|
| 36 |
<% unless @report.hours.empty? %> |
|
| 37 |
<div class="autoscroll"> |
|
| 38 |
<table class="list" id="time-report"> |
|
| 39 |
<thead> |
|
| 40 |
<tr> |
|
| 41 |
<% @report.criteria.each do |criteria| %> |
|
| 42 |
<th><%= l_or_humanize(@report.available_criteria[criteria][:label]) %></th> |
|
| 43 |
<% end %> |
|
| 44 |
<% columns_width = (40 / (@report.periods.length+1)).to_i %> |
|
| 45 |
<% @report.periods.each do |period| %> |
|
| 46 |
<th class="period" width="<%= columns_width %>%"><%= period %></th> |
|
| 47 |
<% end %> |
|
| 48 |
<th class="total" width="<%= columns_width %>%"><%= l(:label_total) %></th> |
|
| 49 |
</tr> |
|
| 50 |
</thead> |
|
| 51 |
<tbody> |
|
| 52 |
<%= render :partial => 'report_criteria', :locals => {:criterias => @report.criteria, :hours => @report.hours, :level => 0} %>
|
|
| 53 |
<tr class="total"> |
|
| 54 |
<td><%= l(:label_total) %></td> |
|
| 55 |
<%= ('<td></td>' * (@report.criteria.size - 1)).html_safe %>
|
|
| 56 |
<% total = 0 -%> |
|
| 57 |
<% @report.periods.each do |period| -%> |
|
| 58 |
<% sum = sum_hours(select_hours(@report.hours, @report.columns, period.to_s)); total += sum -%> |
|
| 59 |
<td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
|
|
| 60 |
<% end -%> |
|
| 61 |
<td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td>
|
|
| 62 |
</tr> |
|
| 63 |
</tbody> |
|
| 64 |
</table> |
|
| 65 |
</div> |
|
| 66 |
|
|
| 67 |
<% other_formats_links do |f| %> |
|
| 68 |
<%= f.link_to 'CSV', :url => params %> |
|
| 69 |
<% end %> |
|
| 70 |
<% end %> |
|
| 71 |
<% end %> |
|
| 72 |
|
|
| 73 |
<% html_title l(:label_spent_time), l(:label_report) %> |
|
| 74 |
|
|
Also available in: Unified diff