Project

General

Profile

Patch #1650 » timelogging.patch

Riccardo Delpopolo, 2009-03-19 13:36

View differences:

db/migrate/20090316220000_add_start_and_end_time_to_time_entries.rb (revision 0)
1
class AddStartAndEndTimeToTimeEntries < ActiveRecord::Migration
2
  def self.up
3
    add_column :time_entries, :start_time, :datetime
4
    add_column :time_entries, :end_time, :datetime
5
  end
6

  
7
  def self.down
8
    remove_column :time_entries, :start_time
9
    remove_column :time_entries, :end_time
10
  end
11
end
db/migrate/20090316220001_allow_null_time_entry_hours.rb (revision 0)
1
class AllowNullTimeEntryHours < ActiveRecord::Migration
2
  def self.up
3
    change_column :time_entries, :hours, :float, :null => true
4
  end
5

  
6
  def self.down
7
    # not needed
8
  end
9
end
app/views/issues/_edit.rhtml (working copy)
1
<% content_for :header_tags do %>
2
    <%= javascript_include_tag 'time' %>
3
<% end %>
1 4
<% labelled_tabular_form_for :issue, @issue,
2 5
                             :url => {:action => 'edit', :id => @issue},
3 6
                             :html => {:id => 'issue-form',
......
18 21
    <% if authorize_for('timelog', 'edit') %>
19 22
        <fieldset class="tabular"><legend><%= l(:button_log_time) %></legend>
20 23
        <% fields_for :time_entry, @time_entry, { :builder => TabularFormBuilder, :lang => current_language} do |time_entry| %>
24
        <%= time_entry.hidden_field :id %>
21 25
        <div class="splitcontentleft">
22 26
        <p><%= time_entry.text_field :hours, :size => 6, :label => :label_spent_time %> <%= l(:field_hours) %></p>
27
        <p><%= time_entry.time_field :start_time %></p>
23 28
        </div>
24 29
        <div class="splitcontentright">
25 30
        <p><%= time_entry.select :activity_id, activity_collection_for_select_options %></p>
31
        <p><%= time_entry.time_field :end_time %></p>
26 32
        </div>
27 33
        <p><%= time_entry.text_field :comments, :size => 60 %></p>
28 34
        <% @time_entry.custom_field_values.each do |value| %>
app/views/issues/show.rhtml (working copy)
34 34
    <td class="category"><b><%=l(:field_category)%>:</b></td><td><%=h @issue.category ? @issue.category.name : "-" %></td>
35 35
    <% if User.current.allowed_to?(:view_time_entries, @project) %>
36 36
    <td class="spent-time"><b><%=l(:label_spent_time)%>:</b></td>
37
    <td class="spent-hours"><%= @issue.spent_hours > 0 ? (link_to l_hours(@issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}) : "-" %></td>
37
    <td><%= spent_hours(@issue) %></td>
38 38
    <% end %>
39 39
</tr>
40 40
<tr>
app/views/timelog/_list.rhtml (working copy)
2 2
<thead>
3 3
<tr>
4 4
<%= sort_header_tag('spent_on', :caption => l(:label_date), :default_order => 'desc') %>
5
<%= sort_header_tag('start_time', :caption => l(:field_start_time), :default_order => 'desc') %>
6
<%= sort_header_tag('end_time', :caption => l(:field_end_time), :default_order => 'desc') %>
7
<%= sort_header_tag('hours', :caption => l(:field_hours)) %>
5 8
<%= sort_header_tag('user', :caption => l(:label_member)) %>
6 9
<%= sort_header_tag('activity', :caption => l(:label_activity)) %>
7 10
<%= sort_header_tag('project', :caption => l(:label_project)) %>
8 11
<%= sort_header_tag('issue', :caption => l(:label_issue), :default_order => 'desc') %>
9 12
<th><%= l(:field_comments) %></th>
10
<%= sort_header_tag('hours', :caption => l(:field_hours)) %>
11 13
<th></th>
12 14
</tr>
13 15
</thead>
......
15 17
<% entries.each do |entry| -%>
16 18
<tr class="time-entry <%= cycle("odd", "even") %>">
17 19
<td class="spent_on"><%= format_date(entry.spent_on) %></td>
20
<td class="start_time"><%= format_time(entry.start_time) %></td>
21
<td class="end_time"><%= format_time(entry.end_time) %></td>
22
<td class="hours"><%= hours(entry) %></td>
18 23
<td class="user"><%=h entry.user %></td>
19 24
<td class="activity"><%=h entry.activity %></td>
20 25
<td class="project"><%=h entry.project %></td>
......
24 29
<% end -%>
25 30
</td>
26 31
<td class="comments"><%=h entry.comments %></td>
27
<td class="hours"><%= html_hours("%.2f" % entry.hours) %></td>
28 32
<td align="center">
29 33
<% if entry.editable_by?(User.current) -%>
30 34
    <%= link_to image_tag('edit.png'), {:controller => 'timelog', :action => 'edit', :id => entry, :project_id => nil},
app/views/timelog/edit.rhtml (working copy)
3 3
<% labelled_tabular_form_for :time_entry, @time_entry, :url => {:action => 'edit', :project_id => @time_entry.project} do |f| %>
4 4
<%= error_messages_for 'time_entry' %>
5 5
<%= back_url_hidden_field_tag %>
6
<% content_for :header_tags do %>
7
    <%= javascript_include_tag 'time' %>
8
<% end %>
6 9

  
10
<% content_for :header_tags do %>
11
  <script language="javascript">
12
    window.onload = function() {
13
      $('<%= @activate_field %>').activate();
14
    };
15
  </script>
16
<% end %>
17

  
7 18
<div class="box">
8 19
<p><%= f.text_field :issue_id, :size => 6 %> <em><%= h("#{@time_entry.issue.tracker.name} ##{@time_entry.issue.id}: #{@time_entry.issue.subject}") if @time_entry.issue %></em></p>
9 20
<p><%= f.text_field :spent_on, :size => 10, :required => true %><%= calendar_for('time_entry_spent_on') %></p>
10
<p><%= f.text_field :hours, :size => 6, :required => true %></p>
21
<p><%= f.time_field :start_time %></p>
22
<p><%= f.time_field :end_time %></p>
23
<p><%= f.text_field :hours, :size => 6 %> <em> <%= l(:text_clear_to_recalculate_time_by_range) %></em> </p> 
11 24
<p><%= f.text_field :comments, :size => 100 %></p>
12 25
<p><%= f.select :activity_id, activity_collection_for_select_options, :required => true %></p>
13 26
</div>
lib/tabular_form_builder.rb (working copy)
33 33
    END_SRC
34 34
    class_eval src, __FILE__, __LINE__
35 35
  end
36

  
37
  def time_field(field)
38
    value = nil
39
    value = @object.send(field) if @object
40
    value = value.strftime('%Y-%m-%d %H:%M') if value
41
    result = text_field field, :size => 15, :value => value
42
    result += (' <input type="button" onclick="maybeSetTimeNow($(\'%s\'))' + 
43
      '" value="' + l(:button_now) + 
44
      '" name="action"/>') % "#{@object_name}_#{field}"
45
    result
46
  end
36 47
  
37 48
  def select(field, choices, options = {}, html_options = {}) 
38 49
    label_for_field(field, options) + super
app/controllers/application.rb (working copy)
249 249
  def filename_for_content_disposition(name)
250 250
    request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
251 251
  end
252

  
253
  def url_path(url_parameters)
254
    rs = ::ActionController::Routing::Routes
255
    rs.generate url_parameters
256
  end
252 257
end
app/controllers/issues_controller.rb (working copy)
103 103
    @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
104 104
    @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
105 105
    @priorities = Enumeration.priorities
106
    @time_entry = TimeEntry.new
106
    @time_entry = @issue.time_entry_in_progress(User.current) || TimeEntry.new
107 107
    respond_to do |format|
108 108
      format.html { render :template => 'issues/show.rhtml' }
109 109
      format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
......
178 178
    end
179 179

  
180 180
    if request.post?
181
      @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
181
      @time_entry = TimeEntry.find_by_id(params[:time_entry][:id]) if params[:time_entry]
182
      @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, 
183
              :user => User.current, :spent_on => Date.today)
184

  
182 185
      @time_entry.attributes = params[:time_entry]
183 186
      attachments = attach_files(@issue, params[:attachments])
184 187
      attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
app/controllers/timelog_controller.rb (working copy)
194 194
  end
195 195
  
196 196
  def edit
197
    return if redirect_if_in_progress
198

  
197 199
    render_403 and return if @time_entry && !@time_entry.editable_by?(User.current)
198 200
    @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
199 201
    @time_entry.attributes = params[:time_entry]
202
    if !@time_entry.hours
203
      if !@time_entry.start_time
204
        @activate_field = 'time_entry_start_time'         
205
      else
206
        @activate_field = 'time_entry_end_time'
207
      end
208
    end
209
    
210
    url_writer = lambda do |entry| 
211
      "<a href = \"#{url_path(:controller => :timelog, :action => :edit, 
212
        :id => entry.id)}\">##{entry.issue_id}-#{entry.id}</a>"
213
    end
214

  
200 215
    if request.post? and @time_entry.save
201
      flash[:notice] = l(:notice_successful_update)
216
      intersecting = @time_entry.find_intersecting_entries
217
      logger.debug "intersecting = #{intersecting.inspect}"
218
      msg = l(:notice_successful_update)
219
      if !intersecting.empty? 
220
        
221
        list = l_hours(:text_time_entry_intersecting_notice_entry) + ' ' + intersecting.
222
          map { |entry| url_writer.call(entry) }.
223
          to_sentence(:skip_last_comma => true, :connector => l(:text_and))
224
        
225
        msg += ' ' + l(:text_time_entry_intersecting_notice, 
226
          url_writer.call(@time_entry), list)
227
      end
228
      flash[:notice] = msg
202 229
      redirect_back_or_default :action => 'details', :project_id => @time_entry.project
203 230
      return
204 231
    end    
......
287 314
    @from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today) - 1
288 315
    @to   ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today)
289 316
  end
317
  
318
  def redirect_if_in_progress
319
    if !@time_entry && @issue
320
      in_progress_entry = @issue.time_entry_in_progress(User.current)
321
      if in_progress_entry
322
        #in order to avoid :id form parameter and not complicate :find_project filter
323
        redirect_to(:controller => 'timelog', :action => 'edit', 
324
            :id => in_progress_entry)
325
        return true
326
      end
327
    end
328
    false
329
  end
290 330
end
app/helpers/issues_helper.rb (working copy)
196 196
    export.rewind
197 197
    export
198 198
  end
199
  
200
  def spent_hours(issue)
201
    return '-' if issue.time_entries.empty?
202
    spent_hours = l_hours(issue.spent_hours)
203
    spent_hours += " (#{l(:text_in_progress)})" if issue.time_entry_in_progress
204
    link_to spent_hours, {:controller => 'timelog', :action => 'details', :project_id => issue.project, :issue_id => issue}, :class => 'icon icon-time'
205
  end
199 206
end
app/helpers/timelog_helper.rb (working copy)
46 46
    sum
47 47
  end
48 48
  
49
  def hours(entry)
50
    return '<span class="hours hours-dec">[%s]</span>' % l(:text_in_progress) if !entry.hours
51
    html_hours("%.2f" % entry.hours)
52
  end
53
  
49 54
  def options_for_period_select(value)
50 55
    options_for_select([[l(:label_all_time), 'all'],
51 56
                        [l(:label_today), 'today'],
app/models/issue.rb (working copy)
278 278
  def to_s
279 279
    "#{tracker} ##{id}: #{subject}"
280 280
  end
281
  
282
  def time_entry_in_progress(user = nil)
283
    TimeEntry.find_by_issue_id(self.id,  
284
        :conditions => 'start_time IS NOT NULL and hours IS NULL' + (user ? " and user_id = #{user.id}" : ''))
285
  end
281 286
  
282 287
  private
283 288
  
app/models/time_entry.rb (copia locale)
26 26
  attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek
27 27

  
28 28
  acts_as_customizable
29
  acts_as_event :title => Proc.new {|o| "#{o.user}: #{l_hours(o.hours)} (#{(o.issue || o.project).event_title})"},
30
                :url => Proc.new {|o| {:controller => 'timelog', :action => 'details', :project_id => o.project}},
29
  title_proc = Proc.new do |entry|    
30
    hours = entry.hours ? l_hours(entry.hours) : "[#{l(:text_in_progress)}]"
31
    "#{entry.user}: #{hours} (#{(entry.issue || entry.project).event_title})"
32
  end
33
  
34
  acts_as_event :title => title_proc,
35
                :url => Proc.new {|entry| {:controller => 'timelog', :action => 'details', :project_id => entry.project}},
31 36
                :author => :user,
32 37
                :description => :comments
33 38
  
34
  validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
39
  validates_presence_of :user_id, :activity_id, :project_id, :spent_on
35 40
  validates_numericality_of :hours, :allow_nil => true, :message => :invalid
36 41
  validates_length_of :comments, :maximum => 255, :allow_nil => true
37 42

  
......
49 54
  
50 55
  def validate
51 56
    errors.add :hours, :invalid if hours && (hours < 0 || hours >= 1000)
57
    
58
    if !start_time && !hours
59
      #rather verbose, but l() always translate to English here for some reason
60
      errors.add :hours, ll(User.current.language, 
61
          :activerecord_error_field_must_be_set_if_other_is_not, 
62
          ll(User.current.language, :field_start_time))
63

  
64
      errors.add :start_time, ll(User.current.language, 
65
          :activerecord_error_field_must_be_set_if_other_is_not, 
66
          ll(User.current.language, :field_hours))
67
    end
68

  
52 69
    errors.add :project_id, :invalid if project.nil?
53 70
    errors.add :issue_id, :invalid if (issue_id && !issue) || (issue && project!=issue.project)
54 71
  end
......
76 93
      yield
77 94
    end
78 95
  end
96
  
97
  def before_save
98
    if !hours && start_time && end_time
99
      self.hours = (end_time - start_time) / 3600
100
    end
101
  end
102
  
103
  def find_intersecting_entries
104
    params = {:start_time => start_time, :end_time => end_time, :id => id}
105
    
106
    self.class.find_all_by_user_id(user_id, :conditions => 
107
        ["(" + 
108
          #this entry's start time or end time is between other's start_time and end_time
109
          "start_time < :start_time and :start_time < end_time OR " + 
110
          "start_time < :end_time and :end_time < end_time OR " + 
111
          #other's entry's start time or end time is between this entry's start_time and end_time
112
          "start_time > :start_time and start_time < :end_time OR " + 
113
          "end_time > :start_time and end_time < :end_time" +
114
          ")" + 
115
          "and id <> :id", params])
116
  end
79 117
end
public/javascripts/time.js (revision 0)
1
//taken from XPlanner
2
var dateFormatChars = "dMyhHmsa";
3

  
4
function formatDate(date, format) {
5
    return formatDate2(date, format, 0);
6
}
7

  
8
function formatDate2(date, format, offset) {
9
    if (offset >= format.length) {
10
        return "";
11
    } else if (dateFormatChars.indexOf(format.charAt(offset)) != -1) {
12
        return formatDateElement(date, format, offset);
13
    } else {
14
        return formatDateLiteral(date, format, offset);
15
    }
16
}
17

  
18
function formatDateElement(date, format, offset) {
19
    var end = offset;
20
    var ch = format.charAt(offset);
21
    while (++end < format.length && format.charAt(end) == ch);
22
    var count = end - offset;
23
    var value;
24
    if (ch == 'd') {
25
        value = padValue(count, date.getDate());
26
    }
27
    else if (ch == 'M') {
28
        value = padValue(count, date.getMonth()+1);
29
    }
30
    else if (ch == 'y') {
31
        value = padValue(count, date.getFullYear());
32
    }
33
    else if (ch == 'H') {
34
        value = padValue(count, date.getHours());
35
    }
36
    else if (ch == 'h') {
37
        value = padValue(count, date.getHours() % 12);
38
    }
39
    else if (ch == 'm') {
40
        value = padValue(count, date.getMinutes());
41
    }
42
    else if (ch == 's') {
43
        value = padValue(count, date.getSeconds());
44
    }
45
    else if (ch == 'a') {
46
        value = date.getHours() > 12 ? 'PM' : 'AM';
47
    }
48
    return value + formatDate2(date, format, end);
49
}
50

  
51
function padValue(count, value) {
52
    for (i = value.toString().length; i < count; i++) {
53
        value = '0'+value;
54
    }
55
    return value;
56
}
57

  
58
function formatDateLiteral(date, format, offset) {
59
    end = offset;
60
    while (++end < format.length && dateFormatChars.indexOf(format.charAt(end)) == -1);
61
    return format.substr(offset, end - offset) + formatDate2(date, format, end);
62
}
63

  
64
function maybeSetTimeNow(field) {
65
  if (field.value == "") {
66
    field.value = formatDate(new Date(), "yyyy-MM-dd HH:mm");
67
  }
68
}
test/fixtures/time_entries.yml (working copy)
55 55
  hours: 7.65
56 56
  user_id: 1
57 57
  tyear: 2007
58
  
58

  
59
#this is to run existing controller tests with Start/End time improvement - specific entries
60
time_entry_in_progress: 
61
  created_on: 2007-04-22 12:20:48 +02:00
62
  tweek: 16
63
  tmonth: 4
64
  project_id: 1
65
  comments: Time spent on a subproject (in progress)
66
  updated_on: 2007-04-22 12:20:48 +02:00
67
  activity_id: 10
68
  spent_on: 2007-04-22
69
  issue_id: 1
70
  id: 5
71
  user_id: 1
72
  tyear: 2007
73
  start_time: 2007-07-11 16:20
74

  
75
#same user but another issue and project, and intersects with time_entry_in_progress
76
intersecting_time_entry: 
77
  created_on: 2007-04-22 12:20:48 +02:00
78
  tweek: 16
79
  tmonth: 4
80
  project_id: 2
81
  comments: Time entry intersecting with time_entry_in_progress
82
  updated_on: 2007-04-22 12:20:48 +02:00
83
  activity_id: 10
84
  spent_on: 2007-04-22
85
  issue_id: 4
86
  id: 6
87
  user_id: 1
88
  tyear: 2007
89
  start_time: 2007-07-11 15:20
90
  end_time: 2007-07-11 17:20
91
  hours: 0 #to not break time calculation tests
92

  
93
#this spans several years - should intersect with time_entry_in_progress as well
94
big_intersecting_time_entry: 
95
  created_on: 2007-04-22 12:20:48 +02:00
96
  tweek: 16
97
  tmonth: 4
98
  project_id: 1
99
  comments: Time entry intersecting with time_entry_in_progress
100
  updated_on: 2007-04-22 12:20:48 +02:00
101
  activity_id: 10
102
  spent_on: 2007-04-22
103
  issue_id: 1
104
  id: 7
105
  user_id: 1
106
  tyear: 2007
107
  start_time: 2005-07-11 16:30
108
  end_time: 2007-07-11 17:20
109
  hours: 0 #to not break time calculation tests
test/functional/issues_controller_test.rb (working copy)
366 366
    assert_not_nil assigns(:issue)
367 367
  end
368 368

  
369
  def test_show_in_progress
370
    @request.session[:user_id] = 1
371
    get :show, :id => 1
372
    assert_equal time_entries(:time_entry_in_progress), assigns(:time_entry),
373
        'there should be an in-progress time entry for this user'
374
  end
375
  
376
  def test_show_not_in_progress
377
    @request.session[:user_id] = 2
378
    get :show, :id => 1
379
    assert_nil assigns(:time_entry).id, 'time entry for this user should be new'
380
  end
381
    
369 382
  def test_get_new
370 383
    @request.session[:user_id] = 2
371 384
    get :new, :project_id => 1, :tracker_id => 1
......
665 678
    issue = Issue.find(1)
666 679
    assert_equal 1, issue.status_id
667 680
    @request.session[:user_id] = 2
668
    assert_difference('TimeEntry.count', 0) do
681
    assert_difference('TimeEntry.count', 0) do #time entries count should not change because given entry is empty (hence invalid)
669 682
      post :edit,
670 683
           :id => 1,
671 684
           :issue => { :status_id => 2, :assigned_to_id => 3 },
......
683 696
    assert mail.body.include?("Status changed from New to Assigned")
684 697
  end
685 698
  
699
  def test_post_edit_with_task_in_progress
700
    user_id = 1
701
    @request.session[:user_id] = user_id
702
    issue = Issue.find(1)
703
    time_entry = issue.time_entry_in_progress(User.find(user_id))
704
    assert_not_nil time_entry, 'there should be time entry in progress'
705
    assert_difference('TimeEntry.count', 0) do #time entries count should not change because entry in progress should be updated
706
      post :edit,
707
           :id => 1,
708
           :issue => { },
709
           :notes => 'Assigned to dlopper',
710
           :time_entry => { :id => time_entry.id, :comments => 'xyz' }
711
    end
712
  end
713
  
686 714
  def test_post_edit_with_note_only
687 715
    notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
688 716
    # anonymous user
......
712 740
    
713 741
    issue = Issue.find(1)
714 742
    
715
    j = issue.journals.find(:first, :order => 'id DESC')
716
    assert_equal '2.5 hours added', j.notes
717
    assert_equal 0, j.details.size
743
    #:first doesn't work sometimes here with MySQL 5.0.51b
744
    e = issue.journals.find(:all, :order => 'id DESC')[0] 
745
    assert_equal '2.5 hours added', e.notes
746
    assert_equal 0, e.details.size
718 747
    
719
    t = issue.time_entries.find(:first, :order => 'id DESC')
748
    #:first doesn't work sometimes here with MySQL 5.0.51b
749
    t = issue.time_entries.find(:all, :order => 'id DESC')[0]
720 750
    assert_not_nil t
721 751
    assert_equal 2.5, t.hours
722 752
    assert_equal spent_hours_before + 2.5, issue.spent_hours
test/functional/timelog_controller_test.rb (copia locale)
62 62
                                 :content => 'Development'
63 63
  end
64 64
  
65
  def test_get_edit_in_progress_by_entry_id
66
    @request.session[:user_id] = 1 #to avoid 'no permission' error
67
    get :edit, :id => time_entries(:time_entry_in_progress)
68
    assert_response :success
69
    assert_equal 'time_entry_end_time', assigns(:activate_field)
70
  end
71
  
72
  def test_get_edit_not_in_progress
73
    @request.session[:user_id] = 2 #there are no in-progress entries of this user for the issue
74
    get :edit, :issue_id => 1
75
    assert_response :success
76
    assert_equal 'time_entry_start_time', assigns(:activate_field)
77
  end
78
  
79
  def test_get_edit_in_progress_by_issue_id
80
    user_id = 1
81
    @request.session[:user_id] = user_id #there is in-progress entry of this user for the issue
82
    issue_id = 1
83
    get :edit, :issue_id => issue_id
84
    assert_redirected_to :action => "edit", :id => Issue.find(issue_id).time_entry_in_progress(User.find(user_id)).id
85
  end
86
  
65 87
  def test_get_edit_existing_time
66 88
    @request.session[:user_id] = 2
67 89
    get :edit, :id => 2, :project_id => nil
......
103 125
    post :edit, :id => 1,
104 126
                :time_entry => {:issue_id => '2',
105 127
                                :hours => '8'}
128
    assert_no_errors(assigns(:time_entry))
106 129
    assert_redirected_to :action => 'details', :project_id => 'ecookbook'
107 130
    entry.reload
108 131
    
......
273 296
    assert_response :success
274 297
    assert_template 'details'
275 298
    assert_not_nil assigns(:entries)
276
    assert_equal 4, assigns(:entries).size
299
    assert_equal 6, assigns(:entries).size
277 300
    # project and subproject
278 301
    assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
279 302
    assert_not_nil assigns(:total_hours)
......
288 311
    assert_response :success
289 312
    assert_template 'details'
290 313
    assert_not_nil assigns(:entries)
291
    assert_equal 3, assigns(:entries).size
314
    assert_equal 5, assigns(:entries).size
292 315
    assert_not_nil assigns(:total_hours)
293 316
    assert_equal "12.90", "%.2f" % assigns(:total_hours)
294 317
    assert_equal '2007-03-20'.to_date, assigns(:from)
......
327 350
    assert_response :success
328 351
    assert_template 'details'
329 352
    assert_not_nil assigns(:entries)
330
    assert_equal 2, assigns(:entries).size
353
    assert_equal 4, assigns(:entries).size
331 354
    assert_not_nil assigns(:total_hours)
332 355
    assert_equal 154.25, assigns(:total_hours)
333 356
    # display all time by default
test/test_helper.rb (working copy)
64 64
    Dir.mkdir "#{RAILS_ROOT}/tmp/test/attachments" unless File.directory?("#{RAILS_ROOT}/tmp/test/attachments")
65 65
    Attachment.storage_path = "#{RAILS_ROOT}/tmp/test/attachments"
66 66
  end
67

  
68
  def assert_error_on(object, field)
69
    object.valid?
70
    assert object.errors.on(field), "expected error on #{field} attribute"
71
  end
72

  
73
  def assert_no_error_on(object, field)
74
    object.valid?
75
    assert !object.errors.on(field), "expected no error on #{field} attribute"
76
  end
77

  
78
  def assert_no_errors(object, options = {})
79
    object.valid? if options[:validate]
80
    assert_equal [], object.errors.full_messages
81
  end
67 82
  
68 83
  def with_settings(options, &block)
69 84
    saved_settings = options.keys.inject({}) {|h, k| h[k] = Setting[k].dup; h}
test/unit/issue_test.rb (working copy)
233 233
    assert_nil Issue.find_by_id(1)
234 234
    assert_nil TimeEntry.find_by_issue_id(1)
235 235
  end
236
  
237
  def test_find_in_progress_success
238
    assert_equal time_entries(:time_entry_in_progress), 
239
        issues(:issues_001).time_entry_in_progress(users(:users_001))
240
  end
241

  
242
  def test_find_in_progress_failure
243
    assert_nil issues(:issues_001).time_entry_in_progress(users(:users_002))
244
  end
236 245
  
237 246
  def test_overdue
238 247
    assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
test/unit/time_entry_test.rb (working copy)
20 20
class TimeEntryTest < Test::Unit::TestCase
21 21
  fixtures :issues, :projects, :users, :time_entries
22 22

  
23
  def setup
24
    User.current.language = 'en'
25
  end
26
  
23 27
  def test_hours_format
24 28
    assertions = { "2"      => 2.0,
25 29
                   "21.1"   => 21.1,
......
44 48
      assert_equal v, t.hours, "Converting #{k} failed:"
45 49
    end
46 50
  end
51
  
52
  def test_start_time_must_be_set_if_hours_are_not_and_reverse
53
    entry = TimeEntry.new
54
    assert_error_on(entry, :start_time)
55
    assert_error_on(entry, :hours)    
56
  end
57

  
58
  def test_start_time_can_be_absent_if_hours_are_set_and_reverse
59
    entry = TimeEntry.new :hours => 1
60
    entry.valid?
61
    assert_no_error_on(entry, :start_time)
62
    entry = TimeEntry.new :start_time => Time.now
63
    entry.valid?
64
    assert_no_error_on(entry, :hours)
65
  end
66

  
67
  def successful_params
68
    {:spent_on => '2008-07-13', :issue_id => 1, :user => users(:users_004), 
69
      :activity_id => Enumeration.get_values('ACTI').first}
70
  end
71
  
72
  def test_hours_not_calculated_if_set_explicitly
73
    #I worked on this time to time during the day, and it was 1 hour in sum
74
    entry = TimeEntry.new successful_params.merge(:hours => 1, 
75
      :start_time => '2008-07-13 10:56', :end_time => '2008-07-14 10:56')
76
      
77
    entry.save!    
78
    assert_equal 1, entry.hours
79
  end
80

  
81
  {['10:56', '11:56'] => 1, ['10:56', '11:26'] => 0.5, 
82
      ['10:56', '10:57'] => 0.0167,
83
      ['2008-07-13 23:50', '2008-07-14 00:20'] => 0.5}.each do |range, hours|
84
    
85
    define_method "test_hours_calculated_#{range[0]}_to_#{range[1]}" do
86
      
87
      #add default day if not specified
88
      range = range.map {|time| time['-'] ? time : '2008-07-13 ' + time} 
89
      
90
      entry = TimeEntry.new successful_params.merge(:hours => nil, 
91
        :start_time => range[0], :end_time => range[1])
92
      entry.save!    
93
      assert_in_delta hours, entry.hours, 0.0001
94
    end
95
  end
96
  
97
  def assert_intersects(source, dest)
98
    intersecting = time_entries(source).find_intersecting_entries
99
    
100
    assert !intersecting.empty?, 
101
      "there should be intersecting entries for #{source.inspect}"
102
    
103
    assert intersecting.map {|e| e.id}.
104
        include?(time_entries(dest).id), 
105
        "#{source.inspect} should intersect with #{dest.inspect}"
106
    
107
    intersecting
108
  end
109
  
110
  def test_find_intersecting_entries_for_incomplete
111
    assert_intersects(:time_entry_in_progress, :intersecting_time_entry)
112
  end
113

  
114
  def test_find_intersecting_entries_for_complete_doesnt_find_itself
115
    intersecting = assert_intersects(:intersecting_time_entry, 
116
      :time_entry_in_progress)
117

  
118
    assert !intersecting.map {|e| e.id}.include?(
119
      time_entries(:intersecting_time_entry)), 'time entry\'s ' + 
120
      'intersecting entries shouldn\'t include itself'
121
  end
122

  
123
  def test_find_intersecting_entries_for_big
124
    assert_intersects(:big_intersecting_time_entry, :time_entry_in_progress)
125
    assert_intersects(:big_intersecting_time_entry, :intersecting_time_entry)
126
  end
47 127
end
vendor/plugins/test_utils/MIT-LICENSE (revision 0)
1
Copyright (C) 2008 Texuna Technologies
2

  
3
Permission is hereby granted, free of charge, to any person obtaining
4
a copy of this software and associated documentation files (the
5
"Software"), to deal in the Software without restriction, including
6
without limitation the rights to use, copy, modify, merge, publish,
7
distribute, sublicense, and/or sell copies of the Software, and to
8
permit persons to whom the Software is furnished to do so, subject to
9
the following conditions:
10

  
11
The above copyright notice and this permission notice shall be
12
included in all copies or substantial portions of the Software.
13

  
14
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21

  
vendor/plugins/test_utils/tasks/test_utils.rake (revision 0)
1
##
2
# based on http://nubyonrails.com/articles/foscon-and-living-dangerously-with-rake
3
# Run a single test (or group of tests started with given string) in Rails.
4
#
5
#   rake blogs-list (or f_blogs-list)
6
#   => Runs test_list for BlogsController (functional test; use f_blogs-list to force it if unit test found)
7
#
8
#   rake blog-create (or u_blog-create)
9
#   => Runs test_create for BlogTest (unit test; use u_blog-create to force it if functional test found))
10

  
11
#test file will be matched in the order of this array items 
12
TEST_TYPES = [
13
    ['u_', "unit/[file_name]_test.rb"],
14
    ['f_', "functional/[file_name]_controller_test.rb"],
15
    ['i_', "integration/[file_name]_test.rb"],
16
    ['l_', "long/[file_name]_test.rb"],
17
]
18

  
19
rule "" do |t|
20
  all_flags = TEST_TYPES.map { |item| item[0] }
21
  if Regexp.new("(#{all_flags.join '|'}|)(.*)\\-([^.]+)$").match(t.name)
22
    flag = $1
23
    file_name = $2
24
    test_name = $3
25

  
26
    path_getter = lambda { |type_info| type_info[1].gsub '[file_name]', file_name}
27
    file_path = nil
28
    TEST_TYPES.each do |type_info|
29
      my_file_path = path_getter.call(type_info)
30
      if flag == type_info[0]
31
        type_info[1].match /((.+)\/)/
32
        type = $2
33
        puts "forced #{type} test"
34
        file_path = my_file_path
35
        break
36
      end
37
    end
38

  
39
    if file_path && !File.exist?("test/#{file_path}")
40
      raise "No file found for #{file_path}"
41
    end
42

  
43
    if !file_path
44
      TEST_TYPES.each do |type_info|
45
        my_file_path = path_getter.call(type_info)
46
        if File.exist? "test/#{my_file_path}"
47
          puts "found #{my_file_path}"
48
          file_path = my_file_path
49
          break
50
        end
51
      end
52
    end
53

  
54
    if !file_path
55
      raise "No file found for #{file_name}"
56
    end
57

  
58
		begin
59
      sh "ruby -Ilib:test test/#{file_path} -n /^test_#{test_name}/"
60
    rescue Exception => e
61
      #no logger here, oops!
62
    	#log.debug "error executing tests: #{e.inspect}"
63
      puts "error executing tests: #{e.inspect}"
64
    end
65
  end
66
end
(3-3/4)