##// END OF EJS Templates
Merged r9783 from trunk....
Jean-Philippe Lang -
r9607:7b7bca0b594f
parent child
Show More
@@ -1,120 +1,120
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class TimeEntry < ActiveRecord::Base
18 class TimeEntry < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20 # could have used polymorphic association
20 # could have used polymorphic association
21 # project association here allows easy loading of time entries at project level with one database trip
21 # project association here allows easy loading of time entries at project level with one database trip
22 belongs_to :project
22 belongs_to :project
23 belongs_to :issue
23 belongs_to :issue
24 belongs_to :user
24 belongs_to :user
25 belongs_to :activity, :class_name => 'TimeEntryActivity', :foreign_key => 'activity_id'
25 belongs_to :activity, :class_name => 'TimeEntryActivity', :foreign_key => 'activity_id'
26
26
27 attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek
27 attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek
28
28
29 acts_as_customizable
29 acts_as_customizable
30 acts_as_event :title => Proc.new {|o| "#{l_hours(o.hours)} (#{(o.issue || o.project).event_title})"},
30 acts_as_event :title => Proc.new {|o| "#{l_hours(o.hours)} (#{(o.issue || o.project).event_title})"},
31 :url => Proc.new {|o| {:controller => 'timelog', :action => 'index', :project_id => o.project, :issue_id => o.issue}},
31 :url => Proc.new {|o| {:controller => 'timelog', :action => 'index', :project_id => o.project, :issue_id => o.issue}},
32 :author => :user,
32 :author => :user,
33 :description => :comments
33 :description => :comments
34
34
35 acts_as_activity_provider :timestamp => "#{table_name}.created_on",
35 acts_as_activity_provider :timestamp => "#{table_name}.created_on",
36 :author_key => :user_id,
36 :author_key => :user_id,
37 :find_options => {:include => :project}
37 :find_options => {:include => :project}
38
38
39 validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
39 validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
40 validates_numericality_of :hours, :allow_nil => true, :message => :invalid
40 validates_numericality_of :hours, :allow_nil => true, :message => :invalid
41 validates_length_of :comments, :maximum => 255, :allow_nil => true
41 validates_length_of :comments, :maximum => 255, :allow_nil => true
42 before_validation :set_project_if_nil
42 before_validation :set_project_if_nil
43 validate :validate_time_entry
43 validate :validate_time_entry
44
44
45 named_scope :visible, lambda {|*args| {
45 named_scope :visible, lambda {|*args| {
46 :include => :project,
46 :include => :project,
47 :conditions => Project.allowed_to_condition(args.shift || User.current, :view_time_entries, *args)
47 :conditions => Project.allowed_to_condition(args.shift || User.current, :view_time_entries, *args)
48 }}
48 }}
49 named_scope :on_issue, lambda {|issue| {
49 named_scope :on_issue, lambda {|issue| {
50 :include => :issue,
50 :include => :issue,
51 :conditions => "#{Issue.table_name}.root_id = #{issue.root_id} AND #{Issue.table_name}.lft >= #{issue.lft} AND #{Issue.table_name}.rgt <= #{issue.rgt}"
51 :conditions => "#{Issue.table_name}.root_id = #{issue.root_id} AND #{Issue.table_name}.lft >= #{issue.lft} AND #{Issue.table_name}.rgt <= #{issue.rgt}"
52 }}
52 }}
53 named_scope :on_project, lambda {|project, include_subprojects| {
53 named_scope :on_project, lambda {|project, include_subprojects| {
54 :include => :project,
54 :include => :project,
55 :conditions => project.project_condition(include_subprojects)
55 :conditions => project.project_condition(include_subprojects)
56 }}
56 }}
57 named_scope :spent_between, lambda {|from, to|
57 named_scope :spent_between, lambda {|from, to|
58 if from && to
58 if from && to
59 {:conditions => ["#{TimeEntry.table_name}.spent_on BETWEEN ? AND ?", from, to]}
59 {:conditions => ["#{TimeEntry.table_name}.spent_on BETWEEN ? AND ?", from, to]}
60 elsif from
60 elsif from
61 {:conditions => ["#{TimeEntry.table_name}.spent_on >= ?", from]}
61 {:conditions => ["#{TimeEntry.table_name}.spent_on >= ?", from]}
62 elsif to
62 elsif to
63 {:conditions => ["#{TimeEntry.table_name}.spent_on <= ?", to]}
63 {:conditions => ["#{TimeEntry.table_name}.spent_on <= ?", to]}
64 else
64 else
65 {}
65 {}
66 end
66 end
67 }
67 }
68
68
69 safe_attributes 'hours', 'comments', 'issue_id', 'activity_id', 'spent_on', 'custom_field_values'
69 safe_attributes 'hours', 'comments', 'issue_id', 'activity_id', 'spent_on', 'custom_field_values', 'custom_fields'
70
70
71 def initialize(attributes=nil, *args)
71 def initialize(attributes=nil, *args)
72 super
72 super
73 if new_record? && self.activity.nil?
73 if new_record? && self.activity.nil?
74 if default_activity = TimeEntryActivity.default
74 if default_activity = TimeEntryActivity.default
75 self.activity_id = default_activity.id
75 self.activity_id = default_activity.id
76 end
76 end
77 self.hours = nil if hours == 0
77 self.hours = nil if hours == 0
78 end
78 end
79 end
79 end
80
80
81 def set_project_if_nil
81 def set_project_if_nil
82 self.project = issue.project if issue && project.nil?
82 self.project = issue.project if issue && project.nil?
83 end
83 end
84
84
85 def validate_time_entry
85 def validate_time_entry
86 errors.add :hours, :invalid if hours && (hours < 0 || hours >= 1000)
86 errors.add :hours, :invalid if hours && (hours < 0 || hours >= 1000)
87 errors.add :project_id, :invalid if project.nil?
87 errors.add :project_id, :invalid if project.nil?
88 errors.add :issue_id, :invalid if (issue_id && !issue) || (issue && project!=issue.project)
88 errors.add :issue_id, :invalid if (issue_id && !issue) || (issue && project!=issue.project)
89 end
89 end
90
90
91 def hours=(h)
91 def hours=(h)
92 write_attribute :hours, (h.is_a?(String) ? (h.to_hours || h) : h)
92 write_attribute :hours, (h.is_a?(String) ? (h.to_hours || h) : h)
93 end
93 end
94
94
95 def hours
95 def hours
96 h = read_attribute(:hours)
96 h = read_attribute(:hours)
97 if h.is_a?(Float)
97 if h.is_a?(Float)
98 h.round(2)
98 h.round(2)
99 else
99 else
100 h
100 h
101 end
101 end
102 end
102 end
103
103
104 # tyear, tmonth, tweek assigned where setting spent_on attributes
104 # tyear, tmonth, tweek assigned where setting spent_on attributes
105 # these attributes make time aggregations easier
105 # these attributes make time aggregations easier
106 def spent_on=(date)
106 def spent_on=(date)
107 super
107 super
108 if spent_on.is_a?(Time)
108 if spent_on.is_a?(Time)
109 self.spent_on = spent_on.to_date
109 self.spent_on = spent_on.to_date
110 end
110 end
111 self.tyear = spent_on ? spent_on.year : nil
111 self.tyear = spent_on ? spent_on.year : nil
112 self.tmonth = spent_on ? spent_on.month : nil
112 self.tmonth = spent_on ? spent_on.month : nil
113 self.tweek = spent_on ? Date.civil(spent_on.year, spent_on.month, spent_on.day).cweek : nil
113 self.tweek = spent_on ? Date.civil(spent_on.year, spent_on.month, spent_on.day).cweek : nil
114 end
114 end
115
115
116 # Returns true if the time entry can be edited by usr, otherwise false
116 # Returns true if the time entry can be edited by usr, otherwise false
117 def editable_by?(usr)
117 def editable_by?(usr)
118 (usr == user && usr.allowed_to?(:edit_own_time_entries, project)) || usr.allowed_to?(:edit_time_entries, project)
118 (usr == user && usr.allowed_to?(:edit_own_time_entries, project)) || usr.allowed_to?(:edit_time_entries, project)
119 end
119 end
120 end
120 end
@@ -1,148 +1,163
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class ApiTest::TimeEntriesTest < ActionController::IntegrationTest
20 class ApiTest::TimeEntriesTest < ActionController::IntegrationTest
21 fixtures :projects, :trackers, :issue_statuses, :issues,
21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 :enumerations, :users, :issue_categories,
22 :enumerations, :users, :issue_categories,
23 :projects_trackers,
23 :projects_trackers,
24 :roles,
24 :roles,
25 :member_roles,
25 :member_roles,
26 :members,
26 :members,
27 :enabled_modules,
27 :enabled_modules,
28 :workflows,
28 :workflows,
29 :time_entries
29 :time_entries
30
30
31 def setup
31 def setup
32 Setting.rest_api_enabled = '1'
32 Setting.rest_api_enabled = '1'
33 end
33 end
34
34
35 context "GET /time_entries.xml" do
35 context "GET /time_entries.xml" do
36 should "return time entries" do
36 should "return time entries" do
37 get '/time_entries.xml', {}, credentials('jsmith')
37 get '/time_entries.xml', {}, credentials('jsmith')
38 assert_response :success
38 assert_response :success
39 assert_equal 'application/xml', @response.content_type
39 assert_equal 'application/xml', @response.content_type
40 assert_tag :tag => 'time_entries',
40 assert_tag :tag => 'time_entries',
41 :child => {:tag => 'time_entry', :child => {:tag => 'id', :content => '2'}}
41 :child => {:tag => 'time_entry', :child => {:tag => 'id', :content => '2'}}
42 end
42 end
43
43
44 context "with limit" do
44 context "with limit" do
45 should "return limited results" do
45 should "return limited results" do
46 get '/time_entries.xml?limit=2', {}, credentials('jsmith')
46 get '/time_entries.xml?limit=2', {}, credentials('jsmith')
47 assert_response :success
47 assert_response :success
48 assert_equal 'application/xml', @response.content_type
48 assert_equal 'application/xml', @response.content_type
49 assert_tag :tag => 'time_entries',
49 assert_tag :tag => 'time_entries',
50 :children => {:count => 2}
50 :children => {:count => 2}
51 end
51 end
52 end
52 end
53 end
53 end
54
54
55 context "GET /time_entries/2.xml" do
55 context "GET /time_entries/2.xml" do
56 should "return requested time entry" do
56 should "return requested time entry" do
57 get '/time_entries/2.xml', {}, credentials('jsmith')
57 get '/time_entries/2.xml', {}, credentials('jsmith')
58 assert_response :success
58 assert_response :success
59 assert_equal 'application/xml', @response.content_type
59 assert_equal 'application/xml', @response.content_type
60 assert_tag :tag => 'time_entry',
60 assert_tag :tag => 'time_entry',
61 :child => {:tag => 'id', :content => '2'}
61 :child => {:tag => 'id', :content => '2'}
62 end
62 end
63 end
63 end
64
64
65 context "POST /time_entries.xml" do
65 context "POST /time_entries.xml" do
66 context "with issue_id" do
66 context "with issue_id" do
67 should "return create time entry" do
67 should "return create time entry" do
68 assert_difference 'TimeEntry.count' do
68 assert_difference 'TimeEntry.count' do
69 post '/time_entries.xml', {:time_entry => {:issue_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11'}}, credentials('jsmith')
69 post '/time_entries.xml', {:time_entry => {:issue_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11'}}, credentials('jsmith')
70 end
70 end
71 assert_response :created
71 assert_response :created
72 assert_equal 'application/xml', @response.content_type
72 assert_equal 'application/xml', @response.content_type
73
73
74 entry = TimeEntry.first(:order => 'id DESC')
74 entry = TimeEntry.first(:order => 'id DESC')
75 assert_equal 'jsmith', entry.user.login
75 assert_equal 'jsmith', entry.user.login
76 assert_equal Issue.find(1), entry.issue
76 assert_equal Issue.find(1), entry.issue
77 assert_equal Project.find(1), entry.project
77 assert_equal Project.find(1), entry.project
78 assert_equal Date.parse('2010-12-02'), entry.spent_on
78 assert_equal Date.parse('2010-12-02'), entry.spent_on
79 assert_equal 3.5, entry.hours
79 assert_equal 3.5, entry.hours
80 assert_equal TimeEntryActivity.find(11), entry.activity
80 assert_equal TimeEntryActivity.find(11), entry.activity
81 end
81 end
82
83 should "accept custom fields" do
84 field = TimeEntryCustomField.create!(:name => 'Test', :field_format => 'string')
85
86 assert_difference 'TimeEntry.count' do
87 post '/time_entries.xml', {:time_entry => {
88 :issue_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11', :custom_fields => [{:id => field.id.to_s, :value => 'accepted'}]
89 }}, credentials('jsmith')
90 end
91 assert_response :created
92 assert_equal 'application/xml', @response.content_type
93
94 entry = TimeEntry.first(:order => 'id DESC')
95 assert_equal 'accepted', entry.custom_field_value(field)
96 end
82 end
97 end
83
98
84 context "with project_id" do
99 context "with project_id" do
85 should "return create time entry" do
100 should "return create time entry" do
86 assert_difference 'TimeEntry.count' do
101 assert_difference 'TimeEntry.count' do
87 post '/time_entries.xml', {:time_entry => {:project_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11'}}, credentials('jsmith')
102 post '/time_entries.xml', {:time_entry => {:project_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11'}}, credentials('jsmith')
88 end
103 end
89 assert_response :created
104 assert_response :created
90 assert_equal 'application/xml', @response.content_type
105 assert_equal 'application/xml', @response.content_type
91
106
92 entry = TimeEntry.first(:order => 'id DESC')
107 entry = TimeEntry.first(:order => 'id DESC')
93 assert_equal 'jsmith', entry.user.login
108 assert_equal 'jsmith', entry.user.login
94 assert_nil entry.issue
109 assert_nil entry.issue
95 assert_equal Project.find(1), entry.project
110 assert_equal Project.find(1), entry.project
96 assert_equal Date.parse('2010-12-02'), entry.spent_on
111 assert_equal Date.parse('2010-12-02'), entry.spent_on
97 assert_equal 3.5, entry.hours
112 assert_equal 3.5, entry.hours
98 assert_equal TimeEntryActivity.find(11), entry.activity
113 assert_equal TimeEntryActivity.find(11), entry.activity
99 end
114 end
100 end
115 end
101
116
102 context "with invalid parameters" do
117 context "with invalid parameters" do
103 should "return errors" do
118 should "return errors" do
104 assert_no_difference 'TimeEntry.count' do
119 assert_no_difference 'TimeEntry.count' do
105 post '/time_entries.xml', {:time_entry => {:project_id => '1', :spent_on => '2010-12-02', :activity_id => '11'}}, credentials('jsmith')
120 post '/time_entries.xml', {:time_entry => {:project_id => '1', :spent_on => '2010-12-02', :activity_id => '11'}}, credentials('jsmith')
106 end
121 end
107 assert_response :unprocessable_entity
122 assert_response :unprocessable_entity
108 assert_equal 'application/xml', @response.content_type
123 assert_equal 'application/xml', @response.content_type
109
124
110 assert_tag 'errors', :child => {:tag => 'error', :content => "Hours can't be blank"}
125 assert_tag 'errors', :child => {:tag => 'error', :content => "Hours can't be blank"}
111 end
126 end
112 end
127 end
113 end
128 end
114
129
115 context "PUT /time_entries/2.xml" do
130 context "PUT /time_entries/2.xml" do
116 context "with valid parameters" do
131 context "with valid parameters" do
117 should "update time entry" do
132 should "update time entry" do
118 assert_no_difference 'TimeEntry.count' do
133 assert_no_difference 'TimeEntry.count' do
119 put '/time_entries/2.xml', {:time_entry => {:comments => 'API Update'}}, credentials('jsmith')
134 put '/time_entries/2.xml', {:time_entry => {:comments => 'API Update'}}, credentials('jsmith')
120 end
135 end
121 assert_response :ok
136 assert_response :ok
122 assert_equal 'API Update', TimeEntry.find(2).comments
137 assert_equal 'API Update', TimeEntry.find(2).comments
123 end
138 end
124 end
139 end
125
140
126 context "with invalid parameters" do
141 context "with invalid parameters" do
127 should "return errors" do
142 should "return errors" do
128 assert_no_difference 'TimeEntry.count' do
143 assert_no_difference 'TimeEntry.count' do
129 put '/time_entries/2.xml', {:time_entry => {:hours => '', :comments => 'API Update'}}, credentials('jsmith')
144 put '/time_entries/2.xml', {:time_entry => {:hours => '', :comments => 'API Update'}}, credentials('jsmith')
130 end
145 end
131 assert_response :unprocessable_entity
146 assert_response :unprocessable_entity
132 assert_equal 'application/xml', @response.content_type
147 assert_equal 'application/xml', @response.content_type
133
148
134 assert_tag 'errors', :child => {:tag => 'error', :content => "Hours can't be blank"}
149 assert_tag 'errors', :child => {:tag => 'error', :content => "Hours can't be blank"}
135 end
150 end
136 end
151 end
137 end
152 end
138
153
139 context "DELETE /time_entries/2.xml" do
154 context "DELETE /time_entries/2.xml" do
140 should "destroy time entry" do
155 should "destroy time entry" do
141 assert_difference 'TimeEntry.count', -1 do
156 assert_difference 'TimeEntry.count', -1 do
142 delete '/time_entries/2.xml', {}, credentials('jsmith')
157 delete '/time_entries/2.xml', {}, credentials('jsmith')
143 end
158 end
144 assert_response :ok
159 assert_response :ok
145 assert_nil TimeEntry.find_by_id(2)
160 assert_nil TimeEntry.find_by_id(2)
146 end
161 end
147 end
162 end
148 end
163 end
General Comments 0
You need to be logged in to leave comments. Login now