mirror of
https://github.com/rickbarrette/stopwatch.git
synced 2026-04-02 01:41:58 -04:00
works, basically
This commit is contained in:
@@ -1,12 +1,23 @@
|
||||
# weiter
|
||||
#
|
||||
# - update time entry row after context menu stop (reflect saved time)
|
||||
# - show current time in menu item / window title
|
||||
# - remember last activity, preselect that in 'new' form
|
||||
# - same for project, unless we are in a project context
|
||||
# - focus first field that needs an action, depending on above
|
||||
#
|
||||
class StopwatchTimersController < ApplicationController
|
||||
helper :timelog
|
||||
|
||||
before_action :require_login
|
||||
before_action :find_optional_data, only: %i(new create)
|
||||
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 :authorize_edit_time, only: %i(edit update)
|
||||
|
||||
def new
|
||||
@time_entry = new_time_entry
|
||||
respond_to :js
|
||||
load_todays_entries
|
||||
end
|
||||
|
||||
def create
|
||||
@@ -23,15 +34,54 @@ class StopwatchTimersController < ApplicationController
|
||||
end
|
||||
|
||||
def edit
|
||||
|
||||
@entries = load_todays_entries #.where.not(id: @time_entry.id)
|
||||
end
|
||||
|
||||
def update
|
||||
# todo update entry
|
||||
if params[:continue]
|
||||
new
|
||||
end
|
||||
end
|
||||
|
||||
def start
|
||||
r = Stopwatch::StartTimer.new(@time_entry).call
|
||||
if r.success?
|
||||
@started_time_entry = @time_entry
|
||||
else
|
||||
logger.error "unable to start timer: #{r.error}"
|
||||
end
|
||||
new unless params[:context]
|
||||
end
|
||||
|
||||
def stop
|
||||
r = Stopwatch::StopTimer.new().call
|
||||
unless r.success?
|
||||
logger.error "unable to stop timer"
|
||||
end
|
||||
new unless params[:context]
|
||||
render action: 'start'
|
||||
end
|
||||
|
||||
def current
|
||||
timer = Stopwatch::Timer.new User.current
|
||||
render json: timer.to_json
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_time_entry
|
||||
@time_entry = time_entries.find params[:id]
|
||||
end
|
||||
|
||||
def load_todays_entries
|
||||
@entries = time_entries.where(spent_on: User.current.today).order(created_on: :asc)
|
||||
end
|
||||
|
||||
def time_entries
|
||||
TimeEntry.where(user: User.current)
|
||||
end
|
||||
|
||||
def new_time_entry
|
||||
TimeEntry.new(project: @project, issue: @issue,
|
||||
user: User.current, spent_on: User.current.today)
|
||||
@@ -48,4 +98,12 @@ class StopwatchTimersController < ApplicationController
|
||||
render_404
|
||||
end
|
||||
|
||||
def authorize_log_time
|
||||
User.current.allowed_to?(:log_time, nil, global: true) or
|
||||
deny_access
|
||||
end
|
||||
|
||||
def authorize_edit_time
|
||||
@time_entry.editable_by?(User.current) or deny_access
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<%= javascript_tag do %>
|
||||
Stopwatch.currentTimerUrl = '<%= j current_stopwatch_timers_url %>';
|
||||
<% end %>
|
||||
@@ -0,0 +1,2 @@
|
||||
<%= javascript_include_tag "stopwatch", plugin: "stopwatch" %>
|
||||
<%= stylesheet_link_tag "stopwatch", plugin: "stopwatch" %>
|
||||
@@ -0,0 +1,10 @@
|
||||
<% if @time_entries.one? %>
|
||||
<% time_entry = @time_entries.first %>
|
||||
<li>
|
||||
<% 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-time', remote: true, method: :put %>
|
||||
<% else %>
|
||||
<%= context_menu_link l(:label_stopwatch_start), start_stopwatch_timer_path(time_entry, context: '1'), class: 'icon-time', remote: true, method: :put %>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
@@ -1,11 +1,14 @@
|
||||
<h3 class="title"><%= l(:label_spent_time) %></h3>
|
||||
<h3 class="title"><%= l(:button_log_time) %> - <%= format_date User.current.today %></h3>
|
||||
|
||||
<%= labelled_form_for @time_entry, url: stopwatch_path, remote: true do |f| %>
|
||||
<% @time_entry.hours ||= 0 %>
|
||||
<%= render partial: 'timelog/form', locals: { f: f } %>
|
||||
<%= render partial: 'entries_list', locals: { entries: @entries } %>
|
||||
|
||||
<%= labelled_form_for @time_entry, url: stopwatch_timer_path(@time_entry), method: :put, remote: true do |f| %>
|
||||
<%= hidden_field_tag :time_entry_id, @time_entry.id %>
|
||||
<%= render partial: 'entry_form', locals: { f: f } %>
|
||||
|
||||
<p class="buttons">
|
||||
<%= submit_tag l(:button_create) %>
|
||||
<%= submit_tag l(:button_cancel), name: nil, onclick: "hideModal(this);", type: 'button' %>
|
||||
<%= submit_tag l(:button_save) %>
|
||||
<%= submit_tag t('.button_save_and_continue'), name: 'continue' %>
|
||||
<%#= submit_tag l(:button_cancel), name: nil, onclick: "hideModal(this);", type: 'button' %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
31
app/views/stopwatch_timers/_entries_list.html.erb
Normal file
31
app/views/stopwatch_timers/_entries_list.html.erb
Normal file
@@ -0,0 +1,31 @@
|
||||
<% if entries.any? %>
|
||||
<table class="list time-entries odd-even">
|
||||
<thead><tr>
|
||||
<th><%= l(:label_activity) %></th>
|
||||
<th><%= l(:label_project) %></th>
|
||||
<th><%= l(:field_comments) %></th>
|
||||
<th><%= l(:field_hours) %></th>
|
||||
<th></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<% for entry in entries %>
|
||||
<tr id="time-entry-<%= entry.id %>" class="time-entry <%= cycle "odd", "even" %>">
|
||||
<td class="activity"><%= entry.activity %></td>
|
||||
<td class="subject"><%= entry.project %> <%= h(' - ') + link_to_issue(entry.issue, truncate: 50) if entry.issue %></td>
|
||||
<td class="comments"><%= entry.comments %></td>
|
||||
<td class="hours"><%= html_hours(format_hours(entry.hours)) %></td>
|
||||
<td class="buttons">
|
||||
<% if User.current.is_running_timer?(entry) %>
|
||||
<%= link_to t(:label_stopwatch_stop), stop_stopwatch_timer_path(entry), remote: true, class: "icon-only icon-time", method: :put %>
|
||||
<% else %>
|
||||
<%= link_to t(:label_stopwatch_start), start_stopwatch_timer_path(entry), remote: true, class: "icon-only icon-time", method: :put %>
|
||||
<% end %>
|
||||
<%= link_to l(:button_edit), edit_stopwatch_timer_path(entry), remote: true, class: "icon-only icon-edit" if entry.editable_by? User.current %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end -%>
|
||||
</tbody>
|
||||
</table>
|
||||
<% end %>
|
||||
|
||||
|
||||
74
app/views/stopwatch_timers/_entry_form.html.erb
Normal file
74
app/views/stopwatch_timers/_entry_form.html.erb
Normal file
@@ -0,0 +1,74 @@
|
||||
<%= error_messages_for 'time_entry' %>
|
||||
|
||||
<fieldset class="box tabular">
|
||||
<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>
|
||||
<%= f.text_field :issue_id, :size => 6, :required => Setting.timelog_required_fields.include?('issue_id') %>
|
||||
<span id="time_entry_issue">
|
||||
<%= link_to_issue(@time_entry.issue) if @time_entry.issue.try(:visible?) %>
|
||||
</span>
|
||||
</p>
|
||||
<!--
|
||||
<p><%= f.date_field :spent_on, :size => 10, :required => true %><%= calendar_for('time_entry_spent_on') %></p>
|
||||
-->
|
||||
<p><%= f.text_field :comments, :size => 100, :maxlength => 1024, :required => Setting.timelog_required_fields.include?('comments') %></p>
|
||||
<p><%= f.hours_field :hours, :size => 6, :required => true %></p>
|
||||
<% @time_entry.custom_field_values.each do |value| %>
|
||||
<p><%= custom_field_tag_with_label :time_entry, value %></p>
|
||||
<% end %>
|
||||
<%= call_hook(:view_timelog_edit_form_bottom, { :time_entry => @time_entry, :form => f }) %>
|
||||
</fieldset>
|
||||
|
||||
<%= javascript_tag do %>
|
||||
$(document).ready(function(){
|
||||
$('#time_entry_project_id').change(function(){
|
||||
$('#time_entry_issue_id').val('');
|
||||
});
|
||||
$('#time_entry_project_id, #time_entry_issue_id').change(function(){
|
||||
$.ajax({
|
||||
url: '<%= escape_javascript(@time_entry.new_record? ? new_time_entry_path(format: 'js') : edit_time_entry_path(@time_entry, format: 'js')) %>',
|
||||
type: 'post',
|
||||
data: $(this).closest('form').serialize()
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
observeAutocompleteField('time_entry_issue_id',
|
||||
function(request, callback) {
|
||||
var url = '<%= j auto_complete_issues_path %>';
|
||||
var data = {
|
||||
term: request.term
|
||||
};
|
||||
var project_id;
|
||||
<% if @time_entry.new_record? && @project %>
|
||||
project_id = '<%= @project.id %>';
|
||||
<% else %>
|
||||
project_id = $('#time_entry_project_id').val();
|
||||
<% end %>
|
||||
if(project_id){
|
||||
data['project_id'] = project_id;
|
||||
} else {
|
||||
data['scope'] = 'all';
|
||||
}
|
||||
|
||||
$.get(url, data, null, 'json')
|
||||
.done(function(data){
|
||||
callback(data);
|
||||
})
|
||||
.fail(function(jqXHR, status, error){
|
||||
callback([]);
|
||||
});
|
||||
},
|
||||
{
|
||||
select: function(event, ui) {
|
||||
$('#time_entry_issue').text('');
|
||||
$('#time_entry_issue_id').val(ui.item.value).change();
|
||||
}
|
||||
}
|
||||
);
|
||||
<% end %>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<h3 class="title"><%= l(:button_log_time) %></h3>
|
||||
<h3 class="title"><%= l(:button_log_time) %> - <%= format_date User.current.today %></h3>
|
||||
|
||||
<%= labelled_form_for @time_entry, url: stopwatch_timer_path, method: :post, remote: true do |f| %>
|
||||
<%= render partial: 'entries_list', locals: { entries: @entries } %>
|
||||
|
||||
<%= labelled_form_for @time_entry, url: stopwatch_timers_path, method: :post, remote: true do |f| %>
|
||||
<% @time_entry.hours ||= 0 %>
|
||||
<%= render partial: 'timelog/form', locals: { f: f } %>
|
||||
<%= render partial: 'entry_form', locals: { f: f } %>
|
||||
|
||||
<p class="buttons">
|
||||
<%= submit_tag l(:button_create) %>
|
||||
<%= submit_tag l(:button_cancel), name: nil, onclick: "hideModal(this);", type: 'button' %>
|
||||
<%#= submit_tag l(:button_cancel), name: nil, onclick: "hideModal(this);", type: 'button' %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
|
||||
1
app/views/stopwatch_timers/create.js.erb
Normal file
1
app/views/stopwatch_timers/create.js.erb
Normal file
@@ -0,0 +1 @@
|
||||
hideModal();
|
||||
4
app/views/stopwatch_timers/edit.js.erb
Normal file
4
app/views/stopwatch_timers/edit.js.erb
Normal file
@@ -0,0 +1,4 @@
|
||||
hideModal();
|
||||
$('#ajax-modal').html('<%= j render partial: 'stopwatch_timers/edit' %>');
|
||||
showModal('ajax-modal', '700px');
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
$('#ajax-modal').html('<%= j render partial: 'stopwatch_timers/new' %>');
|
||||
showModal('ajax-modal', '600px');
|
||||
showModal('ajax-modal', '700px');
|
||||
|
||||
9
app/views/stopwatch_timers/start.js.erb
Normal file
9
app/views/stopwatch_timers/start.js.erb
Normal file
@@ -0,0 +1,9 @@
|
||||
<% if params[:context] %>
|
||||
contextMenuHide();
|
||||
<% else %>
|
||||
hideModal();
|
||||
$('#ajax-modal').html('<%= j render partial: 'new' %>');
|
||||
showModal('ajax-modal', '700px');
|
||||
<% end %>
|
||||
Stopwatch.highlightTimer('<%= @started_time_entry.id if @started_time_entry %>');
|
||||
|
||||
5
app/views/stopwatch_timers/update.js.erb
Normal file
5
app/views/stopwatch_timers/update.js.erb
Normal file
@@ -0,0 +1,5 @@
|
||||
hideModal();
|
||||
<% if params[:continue] %>
|
||||
$('#ajax-modal').html('<%= j render partial: 'stopwatch_timers/new' %>');
|
||||
showModal('ajax-modal', '600px');
|
||||
<% end %>
|
||||
24
assets/javascripts/stopwatch.js
Normal file
24
assets/javascripts/stopwatch.js
Normal file
@@ -0,0 +1,24 @@
|
||||
window.Stopwatch = {
|
||||
highlightRunningTimer: function(){
|
||||
$.get(Stopwatch.currentTimerUrl, function(data){
|
||||
if (data.running && data.time_entry_id) {
|
||||
Stopwatch.highlightTimer(data.time_entry_id);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
highlightTimer: function(id){
|
||||
$('table.time-entries tr.time-entry.running').removeClass('running');
|
||||
if(id && id != ''){
|
||||
$('table.time-entries tr.time-entry').each(function(idx, el){
|
||||
var tr = $(el);
|
||||
var trId = tr.attr('id');
|
||||
if (trId && trId == 'time-entry-'+id) {
|
||||
tr.addClass('running');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$(document).on('ready', Stopwatch.highlightRunningTimer);
|
||||
3
assets/stylesheets/stopwatch.css
Normal file
3
assets/stylesheets/stopwatch.css
Normal file
@@ -0,0 +1,3 @@
|
||||
tr.time-entry.running td.hours {
|
||||
color: red;
|
||||
}
|
||||
@@ -1 +1,12 @@
|
||||
en:
|
||||
label_stopwatch_start: Start tracking
|
||||
label_stopwatch_stop: Stop tracking
|
||||
stopwatch_timers:
|
||||
entries_list:
|
||||
button_stop: Stop
|
||||
button_resume: Resume
|
||||
entry_form:
|
||||
legend_new: New entry
|
||||
legend_edit: Edit entry
|
||||
edit:
|
||||
button_save_and_continue: Save and continue
|
||||
|
||||
@@ -1,2 +1,10 @@
|
||||
resource :stopwatch_timer, only: %i(new create edit update)
|
||||
resources :stopwatch_timers, only: %i(new create edit update) do
|
||||
collection do
|
||||
get :current
|
||||
end
|
||||
member do
|
||||
put :start
|
||||
put :stop
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
module Stopwatch
|
||||
class Hooks < Redmine::Hook::ViewListener
|
||||
render_on :view_layouts_base_html_head,
|
||||
partial: 'stopwatch/hooks/layouts_base_html_head'
|
||||
render_on :view_layouts_base_body_bottom,
|
||||
partial: 'stopwatch/hooks/layouts_base_body_bottom'
|
||||
render_on :view_time_entries_context_menu_start,
|
||||
partial: 'stopwatch/hooks/time_entries_context_menu_start'
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,16 +9,20 @@ module Stopwatch
|
||||
end
|
||||
|
||||
def call
|
||||
if @time_entry.project && @user.allowed_to?(:log_time, @time_entry.project)
|
||||
if @time_entry.project && !@user.allowed_to?(:log_time, @time_entry.project)
|
||||
return Result.new(error: :unauthorized)
|
||||
end
|
||||
|
||||
stop_existing_timer
|
||||
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
|
||||
# new entry with 0 hours was created.
|
||||
# new entries with hours > 0 are just saved as is.
|
||||
start_timer = !@time_entry.new_record? || @time_entry.hours == 0
|
||||
|
||||
if @time_entry.save
|
||||
start_new_timer
|
||||
start_new_timer if start_timer
|
||||
return Result.new(success: true)
|
||||
else
|
||||
Rails.logger.error("could not save time entry: \n#{@time_entry.errors.inspect}")
|
||||
@@ -33,12 +37,5 @@ module Stopwatch
|
||||
timer.start @time_entry
|
||||
end
|
||||
|
||||
def stop_existing_timer
|
||||
timer = Timer.new @user
|
||||
if timer.running?
|
||||
timer.stop
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
20
lib/stopwatch/stop_timer.rb
Normal file
20
lib/stopwatch/stop_timer.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
module Stopwatch
|
||||
class StopTimer
|
||||
|
||||
Result = ImmutableStruct.new(:success?, :error)
|
||||
|
||||
def initialize(user: User.current)
|
||||
@user = user
|
||||
end
|
||||
|
||||
def call
|
||||
timer = Timer.new @user
|
||||
if timer.running?
|
||||
timer.stop
|
||||
end
|
||||
return Result.new(success: true)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@@ -21,7 +21,7 @@ module Stopwatch
|
||||
|
||||
def stop
|
||||
if hours = runtime_hours and
|
||||
time_entry = TimeEntry.find_by_id(data[:time_entry_id])
|
||||
time_entry = TimeEntry.find_by_id(time_entry_id)
|
||||
|
||||
time_entry.update_column :hours, time_entry.hours + hours
|
||||
end
|
||||
@@ -29,11 +29,22 @@ module Stopwatch
|
||||
save
|
||||
end
|
||||
|
||||
def to_json
|
||||
{
|
||||
time_entry_id: time_entry_id,
|
||||
running: running?
|
||||
}.to_json
|
||||
end
|
||||
|
||||
def save
|
||||
user.pref[:current_timer] = data.to_json
|
||||
user.pref.save
|
||||
end
|
||||
|
||||
def time_entry_id
|
||||
data[:time_entry_id]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def data
|
||||
|
||||
@@ -7,5 +7,10 @@ module Stopwatch
|
||||
def timer_running?
|
||||
Stopwatch::Timer.new(self).running?
|
||||
end
|
||||
|
||||
def is_running_timer?(time_entry)
|
||||
timer = Stopwatch::Timer.new(self)
|
||||
timer.running? and timer.time_entry_id == time_entry.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,11 +2,13 @@ require_relative '../test_helper'
|
||||
|
||||
class StartTimerTest < ActiveSupport::TestCase
|
||||
fixtures :users, :user_preferences, :time_entries, :projects,
|
||||
:roles, :member_roles, :members, :enumerations
|
||||
:roles, :member_roles, :members, :enumerations, :enabled_modules
|
||||
|
||||
setup do
|
||||
@user = User.find 1
|
||||
@time_entry = TimeEntry.last
|
||||
@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 start timer" do
|
||||
|
||||
34
test/unit/stop_timer_test.rb
Normal file
34
test/unit/stop_timer_test.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
require_relative '../test_helper'
|
||||
|
||||
class StopTimerTest < 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 and save timer" do
|
||||
hours = @time_entry.hours
|
||||
r = Stopwatch::StartTimer.new(@time_entry, user: @user).call
|
||||
assert r.success?
|
||||
t = Stopwatch::Timer.new(@user)
|
||||
data = t.send(:data)
|
||||
data[:started_at] = 1.hour.ago.to_i
|
||||
t.save
|
||||
|
||||
@time_entry.reload
|
||||
assert_equal hours, @time_entry.hours
|
||||
r = Stopwatch::StopTimer.new(user: @user).call
|
||||
assert r.success?
|
||||
@time_entry.reload
|
||||
assert_equal hours+1, @time_entry.hours
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user