mirror of
https://github.com/rickbarrette/stopwatch.git
synced 2026-04-02 09:51:57 -04:00
Compare commits
34 Commits
screenshot
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 60dfa7ae4a | |||
| 490fbd476d | |||
|
|
527f72f93b | ||
|
|
8e34eb627c | ||
|
|
3c709b1329 | ||
|
|
776c0cd823 | ||
|
|
e40840b974 | ||
|
|
efc8c6c620 | ||
|
|
33e8d85f8f | ||
|
|
7f0581aff2 | ||
|
|
c496fac5e2 | ||
|
|
45d15917b3 | ||
|
|
fd7a5375e2 | ||
|
|
8db0686e3a | ||
|
|
e435d5f350 | ||
|
|
8e572e79b5 | ||
|
|
88c1748703 | ||
|
|
84a49f1d1c | ||
|
|
78e47f5b75 | ||
|
|
033e1739d2 | ||
|
|
bd6711a59f | ||
|
|
d32845cc6e | ||
|
|
317aa08365 | ||
|
|
b4af7e0eb3 | ||
|
|
a2096e3208 | ||
|
|
53d5fbafd2 | ||
|
|
6c51ef795a | ||
|
|
9802307e5e | ||
|
|
742157a1f1 | ||
|
|
4c9539c59a | ||
|
|
c53f1d006c | ||
|
|
37d47f58cf | ||
|
|
049cbd6c8f | ||
|
|
1ed6942f0c |
31
.github/workflows/redmine.yml
vendored
Normal file
31
.github/workflows/redmine.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
name: Test with Redmine
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
redmine:
|
||||||
|
- '6.0'
|
||||||
|
- '6.1'
|
||||||
|
ruby:
|
||||||
|
- '3.3'
|
||||||
|
database:
|
||||||
|
- postgresql
|
||||||
|
- mysql
|
||||||
|
steps:
|
||||||
|
- uses: eXolnet/action-redmine-plugin@v1
|
||||||
|
with:
|
||||||
|
plugin_name: stopwatch
|
||||||
|
redmine_version: ${{ matrix.redmine }}
|
||||||
|
ruby_version: ${{ matrix.ruby }}
|
||||||
|
database: ${{ matrix.database }}
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Stopwatch Plugin for Redmine
|
# Stopwatch Plugin for Redmine [](https://github.com/jkraemer/stopwatch/actions/workflows/redmine.yml)
|
||||||
|
|
||||||
Minimal plugin that aims to make tracking your time with Redmine much easier.
|
Minimal plugin that aims to make tracking your time with Redmine much easier.
|
||||||
|
|
||||||
|
|||||||
12
app/controllers/stopwatch_controller.rb
Normal file
12
app/controllers/stopwatch_controller.rb
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# base class for stopwatch controllers
|
||||||
|
class StopwatchController < ApplicationController
|
||||||
|
helper :timelog, :custom_fields
|
||||||
|
|
||||||
|
before_action :require_login
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def authorize_edit_time
|
||||||
|
@time_entry.editable_by?(User.current) or deny_access
|
||||||
|
end
|
||||||
|
end
|
||||||
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
|
||||||
@@ -8,10 +8,8 @@
|
|||||||
# - same for project, unless we are in a project context
|
# - same for project, unless we are in a project context
|
||||||
# - focus first field that needs an action, depending on above
|
# - focus first field that needs an action, depending on above
|
||||||
#
|
#
|
||||||
class StopwatchTimersController < ApplicationController
|
class StopwatchTimersController < StopwatchController
|
||||||
helper :timelog
|
|
||||||
|
|
||||||
before_action :require_login
|
|
||||||
before_action :find_optional_data, only: %i(new create)
|
before_action :find_optional_data, only: %i(new create)
|
||||||
before_action :authorize_log_time, only: %i(new create start stop current)
|
before_action :authorize_log_time, only: %i(new create start stop current)
|
||||||
before_action :find_time_entry, only: %i(edit update start stop)
|
before_action :find_time_entry, only: %i(edit update start stop)
|
||||||
@@ -62,7 +60,9 @@ class StopwatchTimersController < ApplicationController
|
|||||||
|
|
||||||
def stop
|
def stop
|
||||||
r = Stopwatch::StopTimer.new.call
|
r = Stopwatch::StopTimer.new.call
|
||||||
unless r.success?
|
if r.success?
|
||||||
|
@stopped_time_entry = @time_entry
|
||||||
|
else
|
||||||
logger.error "unable to stop timer"
|
logger.error "unable to stop timer"
|
||||||
end
|
end
|
||||||
new unless params[:context]
|
new unless params[:context]
|
||||||
@@ -73,6 +73,17 @@ class StopwatchTimersController < ApplicationController
|
|||||||
render json: @timer.to_json
|
render json: @timer.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_form
|
||||||
|
if id = params[:time_entry_id].presence
|
||||||
|
@time_entry = TimeEntry.visible.find id
|
||||||
|
else
|
||||||
|
@time_entry = TimeEntry.new
|
||||||
|
end
|
||||||
|
@time_entry.safe_attributes = params[:time_entry]
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
head 404
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def find_timer
|
def find_timer
|
||||||
@@ -82,11 +93,12 @@ class StopwatchTimersController < ApplicationController
|
|||||||
|
|
||||||
def find_time_entry
|
def find_time_entry
|
||||||
@time_entry = time_entries.find params[:id]
|
@time_entry = time_entries.find params[:id]
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_todays_entries
|
def load_todays_entries
|
||||||
@entries = time_entries.where(spent_on: User.current.today).order(created_on: :asc)
|
@entries = time_entries.where(spent_on: User.current.today).or(
|
||||||
|
time_entries.where(id: User.current.running_time_entry_id)
|
||||||
|
).order(created_on: :asc)
|
||||||
end
|
end
|
||||||
|
|
||||||
def time_entries
|
def time_entries
|
||||||
|
|||||||
9
app/views/stopwatch/_settings.html.erb
Normal file
9
app/views/stopwatch/_settings.html.erb
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<p>
|
||||||
|
<label for="settings_default_activity"><%= t '.label_default_activity' %> </label>
|
||||||
|
<%= select_tag 'settings[default_activity]', options_from_collection_for_select( [['always_ask', t('.label_always_ask')], ['system', t('.label_system')]] + TimeEntryActivity.system.active.to_a.pluck(:id, :name), :first, :last, Stopwatch.settings['default_activity'] ) %>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="settings_highlighted_links"><%= t '.label_highlighted_links' %> </label>
|
||||||
|
<%= check_box_tag 'settings[highlighted_links]', 1, Setting.plugin_stopwatch[:highlighted_links] %>
|
||||||
|
</p>
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
<%= javascript_tag do %>
|
<%= javascript_tag do %>
|
||||||
window.stopwatch = window.initStopwatch({
|
window.stopwatch = window.initStopwatch({
|
||||||
currentTimerUrl: '<%= j current_stopwatch_timers_url %>',
|
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? %>
|
<% if User.current.logged? %>
|
||||||
window.stopwatch.highlightRunningTimer(
|
window.stopwatch.highlightRunningTimer(
|
||||||
<%= raw Stopwatch::Timer.new(User.current).to_json %>
|
<%= raw Stopwatch::Timer.new(User.current).to_json %>
|
||||||
)
|
);
|
||||||
|
window.stopwatch.setProjectId('<%= j @project&.id.to_s %>');
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
<%= javascript_include_tag "stopwatch", plugin: "stopwatch" %>
|
<%= javascript_include_tag "stopwatch", plugin: "stopwatch" %>
|
||||||
<%= stylesheet_link_tag "stopwatch", plugin: "stopwatch" %>
|
<%= stylesheet_link_tag "stopwatch", plugin: "stopwatch" %>
|
||||||
|
<%= stylesheet_link_tag "highlighted_links", plugin: "stopwatch" if Stopwatch.highlighted_links %>
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
<% time_entry = @time_entries.first %>
|
<% time_entry = @time_entries.first %>
|
||||||
<li>
|
<li>
|
||||||
<% if User.current.is_running_timer? time_entry %>
|
<% if User.current.is_running_timer? time_entry %>
|
||||||
<%= context_menu_link l(:label_stopwatch_stop), stop_stopwatch_timer_path(time_entry, context: '1'), class: 'icon icon-time', remote: true, method: :put %>
|
<%= context_menu_link sprite_icon(:time, l(:label_stopwatch_stop)), stop_stopwatch_timer_path(time_entry, context: '1'), class: 'icon', remote: true, method: :put %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= context_menu_link l(:label_stopwatch_start), start_stopwatch_timer_path(time_entry, context: '1'), class: 'icon icon-time', remote: true, method: :put %>
|
<%= context_menu_link sprite_icon(:time, l(:label_stopwatch_start)), start_stopwatch_timer_path(time_entry, context: '1'), class: 'icon', remote: true, method: :put %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
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 %>
|
||||||
|
|
||||||
9
app/views/stopwatch_issue_timers/start.js.erb
Normal file
9
app/views/stopwatch_issue_timers/start.js.erb
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<% if @started_time_entry %>
|
||||||
|
contextMenuHide();
|
||||||
|
window.stopwatch.timerStarted(
|
||||||
|
<%= raw Stopwatch::Timer.new(User.current).to_json %>
|
||||||
|
);
|
||||||
|
<% else %>
|
||||||
|
$('#ajax-modal').html('<%= j render partial: 'new' %>');
|
||||||
|
showModal('ajax-modal', '700px');
|
||||||
|
<% end %>
|
||||||
6
app/views/stopwatch_issue_timers/stop.js.erb
Normal file
6
app/views/stopwatch_issue_timers/stop.js.erb
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
contextMenuHide();
|
||||||
|
window.stopwatch.updateStartStopLink(
|
||||||
|
'#stopwatch_stop_timer_<%= @issue.id %>',
|
||||||
|
'<%= j Stopwatch::IssueLinks.new(@issue, self).start_timer %>'
|
||||||
|
);
|
||||||
|
window.stopwatch.timerStopped();
|
||||||
@@ -17,11 +17,11 @@
|
|||||||
<td class="hours"><%= html_hours(format_hours(entry.hours)) %></td>
|
<td class="hours"><%= html_hours(format_hours(entry.hours)) %></td>
|
||||||
<td class="buttons">
|
<td class="buttons">
|
||||||
<% if running %>
|
<% if running %>
|
||||||
<%= link_to t(:label_stopwatch_stop), stop_stopwatch_timer_path(entry), remote: true, class: "icon-only icon-time", method: :put %>
|
<%= link_to sprite_icon(:time, t(:label_stopwatch_stop)), stop_stopwatch_timer_path(entry), remote: true, class: "icon-only", method: :put %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= link_to t(:label_stopwatch_start), start_stopwatch_timer_path(entry), remote: true, class: "icon-only icon-time", method: :put %>
|
<%= link_to sprite_icon(:time, t(:label_stopwatch_start)), start_stopwatch_timer_path(entry), remote: true, class: "icon-only", method: :put %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= link_to l(:button_edit), edit_stopwatch_timer_path(entry), remote: true, class: "icon-only icon-edit" if entry.editable_by? User.current %>
|
<%= link_to sprite_icon(:edit, l(:button_edit)), edit_stopwatch_timer_path(entry), remote: true, class: "icon-only" if entry.editable_by? User.current %>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% end -%>
|
<% end -%>
|
||||||
|
|||||||
@@ -2,16 +2,19 @@
|
|||||||
|
|
||||||
<fieldset class="box tabular">
|
<fieldset class="box tabular">
|
||||||
<legend><%= t @time_entry.new_record? ? '.legend_new' : '.legend_edit' %></legend>
|
<legend><%= t @time_entry.new_record? ? '.legend_new' : '.legend_edit' %></legend>
|
||||||
<p><%= f.select :activity_id, activity_collection_for_select_options(@time_entry), :required => true %></p>
|
|
||||||
|
|
||||||
<p><%= f.select :project_id, project_tree_options_for_select(Project.allowed_to(:log_time).to_a, :selected => @time_entry.project, :include_blank => true), :required => true %></p>
|
<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><label for="stopwatch_time_entry_issue_id"><%= l :field_issue %>
|
||||||
<p>
|
<% if Setting.timelog_required_fields.include?('issue_id') %>
|
||||||
<%= f.text_field :issue_id, :size => 6, :required => Setting.timelog_required_fields.include?('issue_id') %>
|
<span class="required"> *</span>
|
||||||
<span id="time_entry_issue">
|
<% 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?) %>
|
<%= link_to_issue(@time_entry.issue) if @time_entry.issue.try(:visible?) %>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
<p><label for="stopwatch_time_entry_activity_id"><%= l :field_activity %><span class="required"> *</span></label><%= f.select :activity_id, activity_collection_for_select_options(@time_entry), { no_label: true }, id: 'stopwatch_time_entry_activity_id' %></p>
|
||||||
<!--
|
<!--
|
||||||
<p><%= f.date_field :spent_on, :size => 10, :required => true %><%= calendar_for('time_entry_spent_on') %></p>
|
<p><%= f.date_field :spent_on, :size => 10, :required => true %><%= calendar_for('time_entry_spent_on') %></p>
|
||||||
-->
|
-->
|
||||||
@@ -25,19 +28,19 @@
|
|||||||
|
|
||||||
<%= javascript_tag do %>
|
<%= javascript_tag do %>
|
||||||
$(document).ready(function(){
|
$(document).ready(function(){
|
||||||
$('#time_entry_project_id').change(function(){
|
$('#stopwatch_time_entry_project_id').change(function(){
|
||||||
$('#time_entry_issue_id').val('');
|
$('#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({
|
$.ajax({
|
||||||
url: '<%= escape_javascript(@time_entry.new_record? ? new_time_entry_path(format: 'js') : edit_time_entry_path(@time_entry, format: 'js')) %>',
|
url: '<%= j update_form_stopwatch_timers_path(time_entry_id: @time_entry.id, format: 'js') %>',
|
||||||
type: 'post',
|
type: 'post',
|
||||||
data: $(this).closest('form').serialize()
|
data: $(this).closest('form').serialize()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
observeAutocompleteField('time_entry_issue_id',
|
observeAutocompleteField('stopwatch_time_entry_issue_id',
|
||||||
function(request, callback) {
|
function(request, callback) {
|
||||||
var url = '<%= j auto_complete_issues_path %>';
|
var url = '<%= j auto_complete_issues_path %>';
|
||||||
var data = {
|
var data = {
|
||||||
@@ -47,7 +50,7 @@
|
|||||||
<% if @time_entry.new_record? && @project %>
|
<% if @time_entry.new_record? && @project %>
|
||||||
project_id = '<%= @project.id %>';
|
project_id = '<%= @project.id %>';
|
||||||
<% else %>
|
<% else %>
|
||||||
project_id = $('#time_entry_project_id').val();
|
project_id = $('#stopwatch_time_entry_project_id').val();
|
||||||
<% end %>
|
<% end %>
|
||||||
if(project_id){
|
if(project_id){
|
||||||
data['project_id'] = project_id;
|
data['project_id'] = project_id;
|
||||||
@@ -65,8 +68,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
select: function(event, ui) {
|
select: function(event, ui) {
|
||||||
$('#time_entry_issue').text('');
|
$('#stopwatch_time_entry_issue').text('');
|
||||||
$('#time_entry_issue_id').val(ui.item.value).change();
|
$('#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>
|
<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| %>
|
<%= labelled_form_for @time_entry, url: stopwatch_timers_path, method: :post, remote: true do |f| %>
|
||||||
<% @time_entry.hours ||= 0 %>
|
<% @time_entry.hours ||= 0 %>
|
||||||
<%= render partial: 'entry_form', locals: { f: f } %>
|
<%= render partial: 'stopwatch_timers/entry_form', locals: { f: f } %>
|
||||||
|
|
||||||
<p class="buttons">
|
<p class="buttons">
|
||||||
<%= submit_tag l(:button_create) %>
|
<%= submit_tag l(:button_create) %>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
hideModal();
|
hideModal();
|
||||||
window.stopwatch.highlightRunningTimer(
|
window.stopwatch.timerStarted(
|
||||||
<%= raw Stopwatch::Timer.new(User.current).to_json %>
|
<%= raw Stopwatch::Timer.new(User.current).to_json %>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,10 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% if @started_time_entry %>
|
<% if @started_time_entry %>
|
||||||
window.stopwatch.timerStarted('<%= @started_time_entry.id %>',
|
window.stopwatch.timerStarted(
|
||||||
'<%= j format_hours @started_time_entry.hours %>');
|
<%= raw Stopwatch::Timer.new(User.current).to_json %>
|
||||||
|
);
|
||||||
<% else %>
|
<% else %>
|
||||||
window.stopwatch.timerStopped();
|
window.stopwatch.timerStopped();
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|||||||
5
app/views/stopwatch_timers/update_form.js.erb
Normal file
5
app/views/stopwatch_timers/update_form.js.erb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
$('#stopwatch_time_entry_activity_id').html('<%= escape_javascript options_for_select(activity_collection_for_select_options(@time_entry), @time_entry.activity_id) %>');
|
||||||
|
$('#stopwatch_time_entry_issue').html('<%= escape_javascript link_to_issue(@time_entry.issue) if @time_entry.issue.try(:visible?) %>');
|
||||||
|
$('#stopwatch_time_entry_project_id').html('<%= escape_javascript project_tree_options_for_select(Project.allowed_to(:log_time).to_a, :selected => @time_entry.project, :include_blank => true) %>');
|
||||||
|
|
||||||
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
window.initStopwatch = function(config){
|
window.initStopwatch = function(config){
|
||||||
var currentTimerUrl = config.currentTimerUrl;
|
var currentTimerUrl = config.currentTimerUrl;
|
||||||
var hourFormat = config.hourFormat;
|
var hourFormat = config.hourFormat;
|
||||||
|
var locales = config.locales;
|
||||||
|
|
||||||
var hoursRe = hourFormat.replace(/0+/g, '\\d+').replace(/\./g, '\\.');
|
var hoursRe = hourFormat.replace(/0+/g, '\\d+').replace(/\./g, '\\.');
|
||||||
var titleRegexp = new RegExp('^(' + hoursRe + ' - )?(.*)$');
|
var titleRegexp = new RegExp('^(' + hoursRe + ' - )?(.*)$');
|
||||||
@@ -68,13 +69,48 @@ window.initStopwatch = function(config){
|
|||||||
highlightRunningTimer: highlightRunningTimer,
|
highlightRunningTimer: highlightRunningTimer,
|
||||||
timerStopped: function(){
|
timerStopped: function(){
|
||||||
highlightRunningTimer();
|
highlightRunningTimer();
|
||||||
},
|
// fix up any issue timer start/stop links in the UI
|
||||||
timerStarted: function(entryId, spentTime){
|
// no running timer -> all links will start a timer
|
||||||
highlightRunningTimer({
|
$('a.stopwatch_issue_timer').each(function(){
|
||||||
running: true,
|
var a = $(this);
|
||||||
time_entry_id: entryId,
|
a.attr('href', a.attr('href').replace(/stop$/, 'start'));
|
||||||
time_spent: spentTime
|
a.find('span').text(locales.startTimer);
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
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.find('span').text(locales.stopTimer);
|
||||||
|
} else {
|
||||||
|
a.attr('href', href.replace(/stop$/, 'start'));
|
||||||
|
a.find('span').text(locales.startTimer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateStartStopLink: function(id, replacement){
|
||||||
|
$(id).replaceWith(function(){
|
||||||
|
return $(replacement, { html: $(this).html() });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setProjectId: function(projectId){
|
||||||
|
var a = $('a#stopwatch-menu');
|
||||||
|
a.attr('href', a.attr('href').replace(/\/new.*$/, '/new?project_id='+projectId));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
22
assets/stylesheets/highlighted_links.css
Normal file
22
assets/stylesheets/highlighted_links.css
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
a.stopwatch_issue_timer[href*="stop"] {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.stopwatch_issue_timer[href*="start"] {
|
||||||
|
color: limegreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile only */
|
||||||
|
@media (max-width: 899px) {
|
||||||
|
a.stopwatch_issue_timer[href*="stop"] {
|
||||||
|
color: red;
|
||||||
|
background-color: #ffe5e5;
|
||||||
|
border: 1px solid red;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.stopwatch_issue_timer[href*="start"] {
|
||||||
|
color: limegreen;
|
||||||
|
background-color: #eaffea;
|
||||||
|
border: 1px solid limegreen;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
tr.time-entry.running td.hours {
|
tr.time-entry.running td.hours {
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
en:
|
en:
|
||||||
label_stopwatch_start: Start tracking
|
label_stopwatch_start: Start tracking
|
||||||
label_stopwatch_stop: Stop tracking
|
label_stopwatch_stop: Stop tracking
|
||||||
|
stopwatch:
|
||||||
|
settings:
|
||||||
|
label_always_ask: 'Always ask'
|
||||||
|
label_default_activity: 'Default activity for "Start tracking"'
|
||||||
|
label_highlighted_links: "Color-code Start/Stop"
|
||||||
|
label_system: 'Use system default'
|
||||||
stopwatch_timers:
|
stopwatch_timers:
|
||||||
entries_list:
|
entries_list:
|
||||||
button_stop: Stop
|
button_stop: Stop
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
resources :stopwatch_timers, only: %i(new create edit update) do
|
resources :stopwatch_timers, only: %i(new create edit update) do
|
||||||
collection do
|
collection do
|
||||||
get :current
|
get :current
|
||||||
|
post :update_form
|
||||||
end
|
end
|
||||||
member do
|
member do
|
||||||
put :start
|
put :start
|
||||||
@@ -8,3 +9,8 @@ resources :stopwatch_timers, only: %i(new create edit update) do
|
|||||||
end
|
end
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
14
init.rb
14
init.rb
@@ -1,13 +1,14 @@
|
|||||||
require 'stopwatch/hooks'
|
|
||||||
|
|
||||||
Redmine::Plugin.register :stopwatch do
|
Redmine::Plugin.register :stopwatch do
|
||||||
name 'Redmine Stopwatch Plugin'
|
name 'Redmine Stopwatch Plugin'
|
||||||
author 'Jens Krämer'
|
author 'Jens Krämer'
|
||||||
author_url 'https://jkraemer.net/'
|
author_url 'https://jkraemer.net/'
|
||||||
description "Start/stop timer and quick access to today's time bookings for Redmine"
|
description "Start/stop timer and quick access to today's time bookings for Redmine"
|
||||||
version '0.1.0'
|
version '1.1.0'
|
||||||
|
|
||||||
requires_redmine version_or_higher: '3.4.0'
|
requires_redmine version_or_higher: '6.0.0'
|
||||||
|
settings default: {
|
||||||
|
'default_activity' => 'always_ask',
|
||||||
|
}, partial: 'stopwatch/settings'
|
||||||
|
|
||||||
menu :account_menu, :stopwatch,
|
menu :account_menu, :stopwatch,
|
||||||
:new_stopwatch_timer_path,
|
:new_stopwatch_timer_path,
|
||||||
@@ -17,7 +18,4 @@ Redmine::Plugin.register :stopwatch do
|
|||||||
if: ->(*_){ User.current.logged? and User.current.allowed_to?(:log_time, nil, global: true) }
|
if: ->(*_){ User.current.logged? and User.current.allowed_to?(:log_time, nil, global: true) }
|
||||||
end
|
end
|
||||||
|
|
||||||
Rails.configuration.to_prepare do
|
Stopwatch.setup
|
||||||
Stopwatch::UserPatch.apply
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,48 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Stopwatch
|
module Stopwatch
|
||||||
|
def self.setup
|
||||||
|
Stopwatch::ContextMenusControllerPatch.apply
|
||||||
|
Stopwatch::IssuesControllerPatch.apply
|
||||||
|
::TimeEntry.prepend Stopwatch::TimeEntryPatch
|
||||||
|
::User.prepend Stopwatch::UserPatch
|
||||||
|
Stopwatch::Hooks # just load it
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.settings
|
||||||
|
Setting.plugin_stopwatch
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.default_activity
|
||||||
|
if id = settings['default_activity'].presence
|
||||||
|
if id.to_s =~ /^\d+$/
|
||||||
|
TimeEntryActivity.find_by_id id
|
||||||
|
else
|
||||||
|
id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.default_activity_for(time_entry)
|
||||||
|
default = Stopwatch.default_activity
|
||||||
|
return nil if default == 'always_ask'
|
||||||
|
|
||||||
|
project = time_entry.project || time_entry.issue&.project
|
||||||
|
|
||||||
|
if project.nil?
|
||||||
|
activities = TimeEntryActivity.shared.active
|
||||||
|
else
|
||||||
|
activities = project.activities
|
||||||
|
end
|
||||||
|
|
||||||
|
if default == 'system'
|
||||||
|
activities.detect(&:is_default?) || activities.detect{|a| a.parent&.is_default?} || (activities.one? && activities[0]).presence
|
||||||
|
else
|
||||||
|
return activities.detect{ |a| a == default || a.parent == default }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.highlighted_links
|
||||||
|
settings[:highlighted_links]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
28
lib/stopwatch/context_menus_controller_patch.rb
Normal file
28
lib/stopwatch/context_menus_controller_patch.rb
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Stopwatch
|
||||||
|
module ContextMenusControllerPatch
|
||||||
|
module Helper
|
||||||
|
def watcher_link(objects, user)
|
||||||
|
link = +''
|
||||||
|
if params[:action] == 'issues' and
|
||||||
|
objects.one? and (issue = objects[0]).is_a?(Issue) and
|
||||||
|
User.current.allowed_to?(:log_time, issue.project)
|
||||||
|
t = Stopwatch::IssueTimer.new(issue: issue)
|
||||||
|
if t.running?
|
||||||
|
link << IssueLinks.new(issue, self).stop_timer
|
||||||
|
else
|
||||||
|
link << IssueLinks.new(issue, self).start_timer
|
||||||
|
end
|
||||||
|
end
|
||||||
|
super + content_tag(:li, link.html_safe)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.apply
|
||||||
|
ContextMenusController.class_eval do
|
||||||
|
helper Helper
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
28
lib/stopwatch/issue_links.rb
Normal file
28
lib/stopwatch/issue_links.rb
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
module Stopwatch
|
||||||
|
class IssueLinks < Struct.new(:issue, :context)
|
||||||
|
def start_timer
|
||||||
|
context.link_to(
|
||||||
|
context.sprite_icon(:time, I18n.t(:label_stopwatch_start)),
|
||||||
|
context.start_issue_timer_path(issue),
|
||||||
|
class: 'icon stopwatch_issue_timer',
|
||||||
|
data: { issue_id: issue.id },
|
||||||
|
remote: true,
|
||||||
|
method: 'post'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def stop_timer
|
||||||
|
context.link_to(
|
||||||
|
context.sprite_icon(:time, I18n.t(:label_stopwatch_stop)),
|
||||||
|
context.stop_issue_timer_path(issue),
|
||||||
|
class: 'icon 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, self).stop_timer
|
||||||
|
else
|
||||||
|
link << IssueLinks.new(issue, self).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
|
module Stopwatch
|
||||||
class StartTimer
|
class StartTimer
|
||||||
|
|
||||||
Result = ImmutableStruct.new(:success?, :error)
|
Result = ImmutableStruct.new(:success?, :error, :started)
|
||||||
|
|
||||||
def initialize(time_entry, user: User.current)
|
def initialize(time_entry, user: User.current)
|
||||||
@time_entry = time_entry
|
@time_entry = time_entry
|
||||||
@@ -13,7 +13,6 @@ module Stopwatch
|
|||||||
return Result.new(error: :unauthorized)
|
return Result.new(error: :unauthorized)
|
||||||
end
|
end
|
||||||
|
|
||||||
StopTimer.new(user: @user).call
|
|
||||||
|
|
||||||
@time_entry.hours = 0 if @time_entry.hours.nil?
|
@time_entry.hours = 0 if @time_entry.hours.nil?
|
||||||
# we want to start tracking time if this is an existing time entry, or a
|
# 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.
|
# new entries with hours > 0 are just saved as is.
|
||||||
start_timer = !@time_entry.new_record? || @time_entry.hours == 0
|
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
|
if @time_entry.save
|
||||||
start_new_timer if start_timer
|
start_new_timer if start_timer
|
||||||
return Result.new(success: true)
|
return Result.new(success: true, started: start_timer)
|
||||||
else
|
else
|
||||||
Rails.logger.error("could not save time entry: \n#{@time_entry.errors.inspect}")
|
Rails.logger.error("could not save time entry: \n#{@time_entry.errors.inspect}")
|
||||||
return Result.new(error: :invalid)
|
return Result.new(error: :invalid)
|
||||||
|
|||||||
14
lib/stopwatch/time_entry_patch.rb
Normal file
14
lib/stopwatch/time_entry_patch.rb
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
module Stopwatch
|
||||||
|
module TimeEntryPatch
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
prepended do
|
||||||
|
before_destroy :stop_timer
|
||||||
|
end
|
||||||
|
|
||||||
|
def stop_timer
|
||||||
|
t = Stopwatch::Timer.new(user)
|
||||||
|
t.update(stop: true) if t.running?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -45,7 +45,7 @@ module Stopwatch
|
|||||||
# timestamp to either nil (stop: true) or current time.
|
# timestamp to either nil (stop: true) or current time.
|
||||||
def update(stop: false)
|
def update(stop: false)
|
||||||
if hours = runtime_hours and entry = self.time_entry
|
if hours = runtime_hours and entry = self.time_entry
|
||||||
time_entry.update_column :hours, entry.hours + hours
|
time_entry.update_column :hours, entry.read_attribute(:hours) + hours
|
||||||
end
|
end
|
||||||
data[:started_at] = stop || !running? ? nil : Time.now.to_i
|
data[:started_at] = stop || !running? ? nil : Time.now.to_i
|
||||||
save
|
save
|
||||||
@@ -65,7 +65,8 @@ module Stopwatch
|
|||||||
time_entry_id: time_entry_id,
|
time_entry_id: time_entry_id,
|
||||||
time_spent: formatter.format_hours,
|
time_spent: formatter.format_hours,
|
||||||
html_time_spent: formatter.html_hours,
|
html_time_spent: formatter.html_hours,
|
||||||
running: running?
|
running: running?,
|
||||||
|
issue_id: time_entry&.issue_id
|
||||||
}.to_json
|
}.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
module Stopwatch
|
module Stopwatch
|
||||||
module UserPatch
|
module UserPatch
|
||||||
def self.apply
|
extend ActiveSupport::Concern
|
||||||
User.prepend self unless User < self
|
|
||||||
end
|
|
||||||
|
|
||||||
def timer_running?
|
def timer_running?
|
||||||
Stopwatch::Timer.new(self).running?
|
Stopwatch::Timer.new(self).running?
|
||||||
end
|
end
|
||||||
|
|
||||||
def is_running_timer?(time_entry)
|
def is_running_timer?(time_entry)
|
||||||
|
id = running_time_entry_id
|
||||||
|
id.present? and time_entry.id == id
|
||||||
|
end
|
||||||
|
|
||||||
|
def running_time_entry_id
|
||||||
timer = Stopwatch::Timer.new(self)
|
timer = Stopwatch::Timer.new(self)
|
||||||
timer.running? and timer.time_entry_id == time_entry.id
|
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
|
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', __dir__)
|
||||||
|
|
||||||
|
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_response :success
|
||||||
|
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 :ok
|
||||||
|
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 :created
|
||||||
|
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 :created
|
||||||
|
end
|
||||||
|
assert te = TimeEntry.last
|
||||||
|
assert_equal 1, te.issue_id
|
||||||
|
assert_equal 9, te.activity_id
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def assert_not_running
|
||||||
|
assert_not Stopwatch::Timer.new(User.find(@user.id)).running?
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_running
|
||||||
|
assert Stopwatch::Timer.new(User.find(@user.id)).running?
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,5 +1,2 @@
|
|||||||
require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper')
|
require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper')
|
||||||
|
|
||||||
class ActiveSupport::TestCase
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
require_relative '../test_helper'
|
require File.expand_path('../../test_helper', __FILE__)
|
||||||
|
|
||||||
class StartTimerTest < ActiveSupport::TestCase
|
class StartTimerTest < ActiveSupport::TestCase
|
||||||
fixtures :users, :user_preferences, :time_entries, :projects,
|
fixtures :users, :user_preferences, :time_entries, :projects,
|
||||||
@@ -29,7 +29,7 @@ class StartTimerTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
@time_entry.reload
|
@time_entry.reload
|
||||||
assert_equal hours, @time_entry.hours
|
assert_equal hours, @time_entry.hours
|
||||||
another = TimeEntry.new(@time_entry.attributes)
|
another = TimeEntry.new(@time_entry.attributes.except('id'))
|
||||||
another.user = @user
|
another.user = @user
|
||||||
r = Stopwatch::StartTimer.new(another, user: @user).call
|
r = Stopwatch::StartTimer.new(another, user: @user).call
|
||||||
assert r.success?
|
assert r.success?
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
require_relative '../test_helper'
|
require File.expand_path('../../test_helper', __FILE__)
|
||||||
|
|
||||||
class StopTimerTest < ActiveSupport::TestCase
|
class StopTimerTest < ActiveSupport::TestCase
|
||||||
fixtures :users, :user_preferences, :time_entries, :projects,
|
fixtures :users, :user_preferences, :time_entries, :projects,
|
||||||
|
|||||||
78
test/unit/stopwatch_test.rb
Normal file
78
test/unit/stopwatch_test.rb
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
require File.expand_path('../../test_helper', __FILE__)
|
||||||
|
|
||||||
|
class StopwatchTest < ActiveSupport::TestCase
|
||||||
|
fixtures :projects, :enabled_modules, :enumerations
|
||||||
|
|
||||||
|
setup do
|
||||||
|
@project = Project.find 'ecookbook'
|
||||||
|
@te = TimeEntry.new project: @project
|
||||||
|
end
|
||||||
|
|
||||||
|
test "default value for default_activity" do
|
||||||
|
assert_equal 'always_ask', Stopwatch.default_activity
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should find default activity" do
|
||||||
|
with_settings plugin_stopwatch: { 'default_activity' => 9} do
|
||||||
|
assert_equal 'Design', Stopwatch.default_activity.name
|
||||||
|
end
|
||||||
|
with_settings plugin_stopwatch: { 'default_activity' => '11'} do
|
||||||
|
assert_equal 'QA', Stopwatch.default_activity.name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
### 'system' -> ask if there is no sys default / more than one availabale activity
|
||||||
|
|
||||||
|
test "should use system default activity for te" do
|
||||||
|
with_settings plugin_stopwatch: { 'default_activity' => 'system' } do
|
||||||
|
assert_equal 'Development', Stopwatch.default_activity_for(@te).name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should use single active project activity for time entry" do
|
||||||
|
with_settings plugin_stopwatch: { 'default_activity' => 'system' } do
|
||||||
|
TimeEntryActivity.create! active: false, project: @project, name: 'Development', parent_id: 10
|
||||||
|
TimeEntryActivity.create! active: false, project: @project, name: 'QA', parent_id: 11
|
||||||
|
assert_equal 'Design', Stopwatch.default_activity_for(@te).name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should ask if more than one and no default" do
|
||||||
|
with_settings plugin_stopwatch: { 'default_activity' => 'system' } do
|
||||||
|
TimeEntryActivity.update_all is_default: false
|
||||||
|
assert_nil Stopwatch.default_activity_for(@te)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
### always_ask (the default)
|
||||||
|
|
||||||
|
test "should return nil if always ask is set" do
|
||||||
|
assert_nil Stopwatch.default_activity_for(@te)
|
||||||
|
|
||||||
|
TimeEntryActivity.create! active: false, project: @project, name: 'Development'
|
||||||
|
assert_nil Stopwatch.default_activity_for(@te)
|
||||||
|
|
||||||
|
TimeEntryActivity.where(name: 'Development').delete_all
|
||||||
|
assert_nil Stopwatch.default_activity_for(@te)
|
||||||
|
end
|
||||||
|
|
||||||
|
### specific activity -> ask only if this is not available in project
|
||||||
|
|
||||||
|
test "should use configured default activity for time entry" do
|
||||||
|
with_settings plugin_stopwatch: { 'default_activity' => '9' } do
|
||||||
|
assert_equal 'Design', Stopwatch.default_activity_for(@te).name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not have default if unavailable" do
|
||||||
|
TimeEntryActivity.create! active: false, project: @project, name: 'Design', parent_id: 9
|
||||||
|
TimeEntryActivity.create! active: false, project: @project, name: 'Development', parent_id: 11
|
||||||
|
assert_equal [10], @project.activities.pluck(:id)
|
||||||
|
|
||||||
|
with_settings plugin_stopwatch: { 'default_activity' => '9'} do
|
||||||
|
assert_nil Stopwatch.default_activity_for(@te)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
24
test/unit/time_entry_test.rb
Normal file
24
test/unit/time_entry_test.rb
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
require File.expand_path('../../test_helper', __FILE__)
|
||||||
|
|
||||||
|
class StartTimerTest < ActiveSupport::TestCase
|
||||||
|
fixtures :users, :user_preferences, :time_entries, :projects,
|
||||||
|
:roles, :member_roles, :members, :enumerations, :enabled_modules
|
||||||
|
|
||||||
|
setup do
|
||||||
|
@user = User.find 1
|
||||||
|
@time_entry = TimeEntry.where(user_id: 1).first
|
||||||
|
# so we dont have to load all the issue and related fixtures:
|
||||||
|
@time_entry.update_column :issue_id, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should stop timer before destroy" do
|
||||||
|
assert r = Stopwatch::StartTimer.new(@time_entry, user: @user).call
|
||||||
|
assert r.success?, r.inspect
|
||||||
|
assert User.find(@user.id).timer_running?
|
||||||
|
|
||||||
|
@time_entry.destroy
|
||||||
|
|
||||||
|
refute User.find(@user.id).timer_running?
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
require_relative '../test_helper'
|
require File.expand_path('../../test_helper', __FILE__)
|
||||||
|
|
||||||
class TimerTest < ActiveSupport::TestCase
|
class TimerTest < ActiveSupport::TestCase
|
||||||
fixtures :users, :user_preferences, :time_entries
|
fixtures :users, :user_preferences, :time_entries
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
require_relative '../test_helper'
|
require File.expand_path('../../test_helper', __FILE__)
|
||||||
|
|
||||||
class UserTest < ActiveSupport::TestCase
|
class UserTest < ActiveSupport::TestCase
|
||||||
fixtures :users, :user_preferences
|
fixtures :users, :user_preferences, :issues
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
@user = User.find 1
|
@user = User.find 1
|
||||||
@@ -18,4 +18,13 @@ class UserTest < ActiveSupport::TestCase
|
|||||||
assert @user.timer_running?
|
assert @user.timer_running?
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user