mirror of
https://github.com/rickbarrette/stopwatch.git
synced 2026-04-02 09:51:57 -04:00
'Start/Stop tracking' on issues#show
This commit is contained in:
48
app/controllers/stopwatch_issue_timers_controller.rb
Normal file
48
app/controllers/stopwatch_issue_timers_controller.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
class StopwatchIssueTimersController < StopwatchController
|
||||
before_action :find_issue
|
||||
before_action :authorize_log_time
|
||||
|
||||
def start
|
||||
t = Stopwatch::IssueTimer.new(issue: @issue)
|
||||
if t.running?
|
||||
head 422; return
|
||||
end
|
||||
|
||||
time_entry = User.current.todays_time_entry_for(@issue)
|
||||
if time_entry.new_record?
|
||||
sys_default_activity = time_entry.activity
|
||||
time_entry.activity = Stopwatch.default_activity_for time_entry
|
||||
end
|
||||
|
||||
r = Stopwatch::StartTimer.new(time_entry).call
|
||||
if r.success?
|
||||
@started_time_entry = time_entry
|
||||
render status: :created
|
||||
else
|
||||
@time_entry = time_entry
|
||||
# in case the setting is 'always ask', still preset the form to the global default
|
||||
@time_entry.activity ||= sys_default_activity
|
||||
@time_entry.errors.clear
|
||||
render status: :ok
|
||||
end
|
||||
end
|
||||
|
||||
def stop
|
||||
r = Stopwatch::StopTimer.new.call
|
||||
unless r.success?
|
||||
logger.error "unable to stop timer"
|
||||
head 422; return
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorize_log_time
|
||||
User.current.allowed_to?(:log_time, @project) or deny_access
|
||||
end
|
||||
|
||||
def find_issue
|
||||
@issue = Issue.find params[:issue_id]
|
||||
@project = @issue.project
|
||||
end
|
||||
end
|
||||
@@ -60,7 +60,9 @@ class StopwatchTimersController < StopwatchController
|
||||
|
||||
def stop
|
||||
r = Stopwatch::StopTimer.new.call
|
||||
unless r.success?
|
||||
if r.success?
|
||||
@stopped_time_entry = @time_entry
|
||||
else
|
||||
logger.error "unable to stop timer"
|
||||
end
|
||||
new unless params[:context]
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<%= javascript_tag do %>
|
||||
window.stopwatch = window.initStopwatch({
|
||||
currentTimerUrl: '<%= j current_stopwatch_timers_url %>',
|
||||
hourFormat: '<%= j format_hours 0.0 %>'
|
||||
hourFormat: '<%= j format_hours 0.0 %>',
|
||||
locales: {
|
||||
startTimer: '<%= j l :label_stopwatch_start %>',
|
||||
stopTimer: '<%= j l :label_stopwatch_stop %>'
|
||||
},
|
||||
});
|
||||
<% if User.current.logged? %>
|
||||
window.stopwatch.highlightRunningTimer(
|
||||
|
||||
20
app/views/stopwatch_issue_timers/_new.html.erb
Normal file
20
app/views/stopwatch_issue_timers/_new.html.erb
Normal file
@@ -0,0 +1,20 @@
|
||||
<h3 class="title"><%= l(:button_log_time) %> - <%= format_date User.current.today %> - <%= "#{@issue.tracker} #{@issue.id}: #{truncate @issue.subject}" %></h3>
|
||||
|
||||
<%= labelled_form_for @time_entry, url: stopwatch_timers_path, method: :post, remote: true do |f| %>
|
||||
<% @time_entry.hours ||= 0 %>
|
||||
<%= f.hidden_field :issue_id, value: @issue.id %>
|
||||
|
||||
<fieldset class="box tabular">
|
||||
<legend><%= t 'stopwatch_timers.entry_form.legend_new' %></legend>
|
||||
<p><%= f.select :activity_id, activity_collection_for_select_options(@time_entry), :required => true %></p>
|
||||
<p><%= f.text_field :comments, :size => 100, :maxlength => 1024, :required => Setting.timelog_required_fields.include?('comments') %></p>
|
||||
<% @time_entry.custom_field_values.each do |value| %>
|
||||
<p><%= custom_field_tag_with_label :time_entry, value %></p>
|
||||
<% end %>
|
||||
</fieldset>
|
||||
|
||||
<p class="buttons">
|
||||
<%= submit_tag l(:button_create) %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
8
app/views/stopwatch_issue_timers/start.js.erb
Normal file
8
app/views/stopwatch_issue_timers/start.js.erb
Normal file
@@ -0,0 +1,8 @@
|
||||
<% if @started_time_entry %>
|
||||
window.stopwatch.timerStarted(
|
||||
<%= raw Stopwatch::Timer.new(User.current).to_json %>
|
||||
);
|
||||
<% else %>
|
||||
$('#ajax-modal').html('<%= j render partial: 'new' %>');
|
||||
showModal('ajax-modal', '700px');
|
||||
<% end %>
|
||||
5
app/views/stopwatch_issue_timers/stop.js.erb
Normal file
5
app/views/stopwatch_issue_timers/stop.js.erb
Normal file
@@ -0,0 +1,5 @@
|
||||
window.stopwatch.updateStartStopLink(
|
||||
'#stopwatch_stop_timer_<%= @issue.id %>',
|
||||
'<%= j Stopwatch::IssueLinks.new(@issue).start_timer %>'
|
||||
);
|
||||
window.stopwatch.timerStopped();
|
||||
@@ -4,8 +4,12 @@
|
||||
<legend><%= t @time_entry.new_record? ? '.legend_new' : '.legend_edit' %></legend>
|
||||
|
||||
<p><label for="stopwatch_time_entry_project_id"><%= l :field_project %><span class="required"> *</span></label><%= f.select :project_id, project_tree_options_for_select(Project.allowed_to(:log_time).to_a, :selected => @time_entry.project, :include_blank => true), { no_label: true }, id: 'stopwatch_time_entry_project_id' %></p>
|
||||
<p>
|
||||
<%= f.text_field :issue_id, :size => 6, :required => Setting.timelog_required_fields.include?('issue_id') %>
|
||||
<p><label for="stopwatch_time_entry_issue_id"><%= l :field_issue %>
|
||||
<% if Setting.timelog_required_fields.include?('issue_id') %>
|
||||
<span class="required"> *</span>
|
||||
<% end %>
|
||||
</label>
|
||||
<%= f.text_field :issue_id, :size => 6, no_label: true, id: 'stopwatch_time_entry_issue_id' %>
|
||||
<span id="stopwatch_time_entry_issue">
|
||||
<%= link_to_issue(@time_entry.issue) if @time_entry.issue.try(:visible?) %>
|
||||
</span>
|
||||
@@ -24,10 +28,10 @@
|
||||
|
||||
<%= javascript_tag do %>
|
||||
$(document).ready(function(){
|
||||
$('#time_entry_project_id').change(function(){
|
||||
$('#time_entry_issue_id').val('');
|
||||
$('#stopwatch_time_entry_project_id').change(function(){
|
||||
$('#stopwatch_time_entry_issue_id').val('');
|
||||
});
|
||||
$('#time_entry_project_id, #time_entry_issue_id').change(function(){
|
||||
$('#stopwatch_time_entry_project_id, #stopwatch_time_entry_issue_id').change(function(){
|
||||
$.ajax({
|
||||
url: '<%= j update_form_stopwatch_timers_path(time_entry_id: @time_entry.id, format: 'js') %>',
|
||||
type: 'post',
|
||||
@@ -36,7 +40,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
observeAutocompleteField('time_entry_issue_id',
|
||||
observeAutocompleteField('stopwatch_time_entry_issue_id',
|
||||
function(request, callback) {
|
||||
var url = '<%= j auto_complete_issues_path %>';
|
||||
var data = {
|
||||
@@ -46,7 +50,7 @@
|
||||
<% if @time_entry.new_record? && @project %>
|
||||
project_id = '<%= @project.id %>';
|
||||
<% else %>
|
||||
project_id = $('#time_entry_project_id').val();
|
||||
project_id = $('#stopwatch_time_entry_project_id').val();
|
||||
<% end %>
|
||||
if(project_id){
|
||||
data['project_id'] = project_id;
|
||||
@@ -64,8 +68,8 @@
|
||||
},
|
||||
{
|
||||
select: function(event, ui) {
|
||||
$('#time_entry_issue').text('');
|
||||
$('#time_entry_issue_id').val(ui.item.value).change();
|
||||
$('#stopwatch_time_entry_issue').text('');
|
||||
$('#stopwatch_time_entry_issue_id').val(ui.item.value).change();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<h3 class="title"><%= l(:button_log_time) %> - <%= format_date User.current.today %></h3>
|
||||
|
||||
<%= render partial: 'entries_list', locals: { entries: @entries } %>
|
||||
<% if @entries %>
|
||||
<%= render partial: 'stopwatch_timers/entries_list', locals: { entries: @entries } %>
|
||||
<% end %>
|
||||
|
||||
<%= labelled_form_for @time_entry, url: stopwatch_timers_path, method: :post, remote: true do |f| %>
|
||||
<% @time_entry.hours ||= 0 %>
|
||||
<%= render partial: 'entry_form', locals: { f: f } %>
|
||||
<%= render partial: 'stopwatch_timers/entry_form', locals: { f: f } %>
|
||||
|
||||
<p class="buttons">
|
||||
<%= submit_tag l(:button_create) %>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
hideModal();
|
||||
window.stopwatch.highlightRunningTimer(
|
||||
window.stopwatch.timerStarted(
|
||||
<%= raw Stopwatch::Timer.new(User.current).to_json %>
|
||||
);
|
||||
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
<% end %>
|
||||
|
||||
<% if @started_time_entry %>
|
||||
window.stopwatch.timerStarted('<%= @started_time_entry.id %>',
|
||||
'<%= j format_hours @started_time_entry.hours %>');
|
||||
window.stopwatch.timerStarted(
|
||||
<%= raw Stopwatch::Timer.new(User.current).to_json %>
|
||||
);
|
||||
<% else %>
|
||||
window.stopwatch.timerStopped();
|
||||
<% end %>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
window.initStopwatch = function(config){
|
||||
var currentTimerUrl = config.currentTimerUrl;
|
||||
var hourFormat = config.hourFormat;
|
||||
var locales = config.locales;
|
||||
|
||||
var hoursRe = hourFormat.replace(/0+/g, '\\d+').replace(/\./g, '\\.');
|
||||
var titleRegexp = new RegExp('^(' + hoursRe + ' - )?(.*)$');
|
||||
@@ -68,12 +69,43 @@ window.initStopwatch = function(config){
|
||||
highlightRunningTimer: highlightRunningTimer,
|
||||
timerStopped: function(){
|
||||
highlightRunningTimer();
|
||||
// fix up any issue timer start/stop links in the UI
|
||||
// no running timer -> all links will start a timer
|
||||
$('a.stopwatch_issue_timer').each(function(){
|
||||
var a = $(this);
|
||||
a.attr('href', a.attr('href').replace(/stop$/, 'start'));
|
||||
a.text(locales.startTimer);
|
||||
});
|
||||
},
|
||||
timerStarted: function(entryId, spentTime){
|
||||
highlightRunningTimer({
|
||||
running: true,
|
||||
time_entry_id: entryId,
|
||||
time_spent: spentTime
|
||||
timerStarted: function(data){
|
||||
highlightRunningTimer(data);
|
||||
// {
|
||||
// running: true,
|
||||
// time_entry_id: entryId,
|
||||
// time_spent: spentTime
|
||||
// });
|
||||
// fix up any issue timer start/stop links in the UI
|
||||
// all links will start a timer, except the one for the current issue,
|
||||
// which has to be turned into a stop link.
|
||||
if(data.running) {
|
||||
$('a.stopwatch_issue_timer').each(function(){
|
||||
var a = $(this);
|
||||
var href = a.attr('href');
|
||||
if(data.issue_id) {
|
||||
if(a.data('issueId') == data.issue_id) {
|
||||
a.attr('href', href.replace(/start$/, 'stop'));
|
||||
a.text(locales.stopTimer);
|
||||
} else {
|
||||
a.attr('href', href.replace(/stop$/, 'start'));
|
||||
a.text(locales.startTimer);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
updateStartStopLink: function(id, replacement){
|
||||
$(id).replaceWith(function(){
|
||||
return $(replacement, { html: $(this).html() });
|
||||
});
|
||||
},
|
||||
setProjectId: function(projectId){
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
en:
|
||||
label_stopwatch_start: Start tracking
|
||||
label_stopwatch_stop: Stop tracking
|
||||
stopwatch:
|
||||
settings:
|
||||
label_always_ask: 'Always ask'
|
||||
label_default_activity: 'Default activity for "Start tracking"'
|
||||
label_system: 'Use system default'
|
||||
stopwatch_timers:
|
||||
entries_list:
|
||||
button_stop: Stop
|
||||
|
||||
@@ -9,3 +9,8 @@ resources :stopwatch_timers, only: %i(new create edit update) do
|
||||
end
|
||||
end
|
||||
|
||||
scope 'issues/:issue_id' do
|
||||
post 'timer/start', to: 'stopwatch_issue_timers#start', as: :start_issue_timer
|
||||
post 'timer/stop', to: 'stopwatch_issue_timers#stop', as: :stop_issue_timer
|
||||
end
|
||||
|
||||
|
||||
3
init.rb
3
init.rb
@@ -6,7 +6,7 @@ Redmine::Plugin.register :stopwatch do
|
||||
author 'Jens Krämer'
|
||||
author_url 'https://jkraemer.net/'
|
||||
description "Start/stop timer and quick access to today's time bookings for Redmine"
|
||||
version '0.1.0'
|
||||
version '0.2.0'
|
||||
|
||||
requires_redmine version_or_higher: '3.4.0'
|
||||
settings default: {
|
||||
@@ -24,5 +24,6 @@ end
|
||||
Rails.configuration.to_prepare do
|
||||
Stopwatch::UserPatch.apply
|
||||
Stopwatch::TimeEntryPatch.apply
|
||||
Stopwatch::IssuesControllerPatch.apply
|
||||
end
|
||||
|
||||
|
||||
29
lib/stopwatch/issue_links.rb
Normal file
29
lib/stopwatch/issue_links.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
module Stopwatch
|
||||
class IssueLinks < Struct.new(:issue)
|
||||
include ActionView::Helpers::UrlHelper
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
|
||||
def start_timer
|
||||
link_to(I18n.t(:label_stopwatch_start),
|
||||
start_issue_timer_path(issue),
|
||||
class: 'icon icon-time stopwatch_issue_timer',
|
||||
data: { issue_id: issue.id },
|
||||
remote: true,
|
||||
method: 'post')
|
||||
end
|
||||
|
||||
def stop_timer
|
||||
link_to(I18n.t(:label_stopwatch_stop),
|
||||
stop_issue_timer_path(issue),
|
||||
class: 'icon icon-time stopwatch_issue_timer',
|
||||
data: { issue_id: issue.id },
|
||||
remote: true,
|
||||
method: 'post')
|
||||
|
||||
end
|
||||
|
||||
# to make route helpers happy
|
||||
def controller; nil end
|
||||
end
|
||||
end
|
||||
18
lib/stopwatch/issue_timer.rb
Normal file
18
lib/stopwatch/issue_timer.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
module Stopwatch
|
||||
class IssueTimer
|
||||
|
||||
def initialize(issue:, user: User.current)
|
||||
@issue = issue
|
||||
@user = user
|
||||
end
|
||||
|
||||
def running?
|
||||
running_time_entry.present?
|
||||
end
|
||||
|
||||
def running_time_entry
|
||||
@running_time_entry ||= @issue.time_entries.find_by_id(@user.running_time_entry_id)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
26
lib/stopwatch/issues_controller_patch.rb
Normal file
26
lib/stopwatch/issues_controller_patch.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Stopwatch
|
||||
module IssuesControllerPatch
|
||||
module Helper
|
||||
def watcher_link(issue, user)
|
||||
link = +''
|
||||
if User.current.allowed_to?(:log_time, issue.project)
|
||||
t = Stopwatch::IssueTimer.new(issue: issue)
|
||||
if t.running?
|
||||
link << IssueLinks.new(issue).stop_timer
|
||||
else
|
||||
link << IssueLinks.new(issue).start_timer
|
||||
end
|
||||
end
|
||||
link.html_safe + super
|
||||
end
|
||||
end
|
||||
|
||||
def self.apply
|
||||
IssuesController.class_eval do
|
||||
helper Helper
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,7 @@
|
||||
module Stopwatch
|
||||
class StartTimer
|
||||
|
||||
Result = ImmutableStruct.new(:success?, :error)
|
||||
Result = ImmutableStruct.new(:success?, :error, :started)
|
||||
|
||||
def initialize(time_entry, user: User.current)
|
||||
@time_entry = time_entry
|
||||
@@ -13,7 +13,6 @@ module Stopwatch
|
||||
return Result.new(error: :unauthorized)
|
||||
end
|
||||
|
||||
StopTimer.new(user: @user).call
|
||||
|
||||
@time_entry.hours = 0 if @time_entry.hours.nil?
|
||||
# we want to start tracking time if this is an existing time entry, or a
|
||||
@@ -21,9 +20,13 @@ module Stopwatch
|
||||
# new entries with hours > 0 are just saved as is.
|
||||
start_timer = !@time_entry.new_record? || @time_entry.hours == 0
|
||||
|
||||
# stop currently running timer, but only when there is a chance for us
|
||||
# to succeed creating the new one.
|
||||
StopTimer.new(user: @user).call if @time_entry.valid?
|
||||
|
||||
if @time_entry.save
|
||||
start_new_timer if start_timer
|
||||
return Result.new(success: true)
|
||||
return Result.new(success: true, started: start_timer)
|
||||
else
|
||||
Rails.logger.error("could not save time entry: \n#{@time_entry.errors.inspect}")
|
||||
return Result.new(error: :invalid)
|
||||
|
||||
@@ -65,7 +65,8 @@ module Stopwatch
|
||||
time_entry_id: time_entry_id,
|
||||
time_spent: formatter.format_hours,
|
||||
html_time_spent: formatter.html_hours,
|
||||
running: running?
|
||||
running: running?,
|
||||
issue_id: time_entry&.issue_id
|
||||
}.to_json
|
||||
end
|
||||
|
||||
|
||||
@@ -17,5 +17,10 @@ module Stopwatch
|
||||
timer = Stopwatch::Timer.new(self)
|
||||
timer.time_entry_id if timer.running?
|
||||
end
|
||||
|
||||
def todays_time_entry_for(issue)
|
||||
TimeEntry.order(created_on: :desc).
|
||||
find_or_initialize_by(user: self, issue: issue, spent_on: today)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
118
test/integration/ticket_timer_test.rb
Normal file
118
test/integration/ticket_timer_test.rb
Normal file
@@ -0,0 +1,118 @@
|
||||
require File.expand_path('../../test_helper', __FILE__)
|
||||
|
||||
class TicketTimerTest < Redmine::IntegrationTest
|
||||
include ActiveJob::TestHelper
|
||||
|
||||
fixtures :projects,
|
||||
:users, :email_addresses,
|
||||
:roles,
|
||||
:members,
|
||||
:member_roles,
|
||||
:trackers,
|
||||
:projects_trackers,
|
||||
:enabled_modules,
|
||||
:issue_statuses,
|
||||
:issues,
|
||||
:enumerations,
|
||||
:custom_fields,
|
||||
:custom_values,
|
||||
:custom_fields_trackers,
|
||||
:attachments
|
||||
|
||||
setup do
|
||||
@issue = Issue.find 1
|
||||
@user = User.find_by_login 'jsmith'
|
||||
end
|
||||
|
||||
test "should create / stop / resume timer for ticket" do
|
||||
log_user 'jsmith', 'jsmith'
|
||||
|
||||
assert_not_running
|
||||
|
||||
get "/issues/1"
|
||||
assert_select "div.contextual a", text: /start tracking/i
|
||||
assert_no_difference ->{TimeEntry.count} do
|
||||
post "/issues/1/timer/start", xhr: true
|
||||
assert_response :success
|
||||
end
|
||||
assert_not_running
|
||||
|
||||
assert_difference ->{TimeEntry.count} do
|
||||
post "/stopwatch_timers", xhr: true, params: {
|
||||
time_entry: { issue_id: 1, activity_id: 9 }
|
||||
}
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
assert_running
|
||||
|
||||
get "/issues/1"
|
||||
assert_select "div.contextual a", text: /stop tracking/i
|
||||
assert_no_difference ->{TimeEntry.count} do
|
||||
post "/issues/1/timer/stop", xhr: true
|
||||
end
|
||||
|
||||
assert_not_running
|
||||
|
||||
get "/issues/1"
|
||||
assert_select "div.contextual a", text: /start tracking/i
|
||||
assert_no_difference ->{TimeEntry.count} do
|
||||
post "/issues/1/timer/start", xhr: true
|
||||
end
|
||||
assert_response :success
|
||||
|
||||
assert_running
|
||||
|
||||
get "/issues/1"
|
||||
assert_select "div.contextual a", text: /stop tracking/i
|
||||
assert_no_difference ->{TimeEntry.count} do
|
||||
post "/issues/1/timer/stop", xhr: true
|
||||
end
|
||||
|
||||
assert_not_running
|
||||
end
|
||||
|
||||
test "should ask by default" do
|
||||
log_user 'jsmith', 'jsmith'
|
||||
TimeEntry.delete_all
|
||||
assert_no_difference ->{TimeEntry.count} do
|
||||
post "/issues/1/timer/start", xhr: true
|
||||
assert_response 200
|
||||
end
|
||||
end
|
||||
|
||||
test "should use global default actvity" do
|
||||
log_user 'jsmith', 'jsmith'
|
||||
TimeEntry.delete_all
|
||||
with_settings plugin_stopwatch: { 'default_activity' => 'system'} do
|
||||
post "/issues/1/timer/start", xhr: true
|
||||
assert_response 201
|
||||
end
|
||||
assert te = TimeEntry.last
|
||||
assert_equal 1, te.issue_id
|
||||
assert_equal 10, te.activity_id
|
||||
end
|
||||
|
||||
test "should use configured default actvity" do
|
||||
log_user 'jsmith', 'jsmith'
|
||||
TimeEntry.delete_all
|
||||
with_settings plugin_stopwatch: { 'default_activity' => '9'} do
|
||||
post "/issues/1/timer/start", xhr: true
|
||||
assert_response 201
|
||||
end
|
||||
assert te = TimeEntry.last
|
||||
assert_equal 1, te.issue_id
|
||||
assert_equal 9, te.activity_id
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
def assert_not_running
|
||||
refute Stopwatch::Timer.new(User.find(@user.id)).running?
|
||||
end
|
||||
|
||||
def assert_running
|
||||
assert Stopwatch::Timer.new(User.find(@user.id)).running?
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,7 @@
|
||||
require_relative '../test_helper'
|
||||
|
||||
class UserTest < ActiveSupport::TestCase
|
||||
fixtures :users, :user_preferences
|
||||
fixtures :users, :user_preferences, :issues
|
||||
|
||||
setup do
|
||||
@user = User.find 1
|
||||
@@ -18,4 +18,13 @@ class UserTest < ActiveSupport::TestCase
|
||||
assert @user.timer_running?
|
||||
end
|
||||
|
||||
test "should build time entry for issue" do
|
||||
i = Issue.find 1
|
||||
te = @user.todays_time_entry_for i
|
||||
assert te.new_record?
|
||||
assert_equal @user, te.user
|
||||
assert_equal i, te.issue
|
||||
assert_equal @user.today, te.spent_on
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user