mirror of
https://github.com/rickbarrette/stopwatch.git
synced 2026-04-02 09:51:57 -04:00
initial commit
This commit is contained in:
51
app/controllers/stopwatch_timers_controller.rb
Normal file
51
app/controllers/stopwatch_timers_controller.rb
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
class StopwatchTimersController < ApplicationController
|
||||||
|
helper :timelog
|
||||||
|
|
||||||
|
before_action :require_login
|
||||||
|
before_action :find_optional_data, only: %i(new create)
|
||||||
|
|
||||||
|
def new
|
||||||
|
@time_entry = new_time_entry
|
||||||
|
respond_to :js
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@time_entry = new_time_entry
|
||||||
|
@time_entry.safe_attributes = params[:time_entry]
|
||||||
|
@result = Stopwatch::StartTimer.new(@time_entry).call
|
||||||
|
unless @result.success?
|
||||||
|
if @result.error == :unauthorized
|
||||||
|
render_403
|
||||||
|
else
|
||||||
|
render_error status: 422, message: "could not start timer: #{@result.error}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def new_time_entry
|
||||||
|
TimeEntry.new(project: @project, issue: @issue,
|
||||||
|
user: User.current, spent_on: User.current.today)
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_optional_data
|
||||||
|
if params[:issue_id].present?
|
||||||
|
@issue = Issue.find(params[:issue_id])
|
||||||
|
@project = @issue.project
|
||||||
|
elsif params[:project_id].present?
|
||||||
|
@project = Project.find(params[:project_id])
|
||||||
|
end
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
render_404
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
11
app/views/stopwatch_timers/_edit.html.erb
Normal file
11
app/views/stopwatch_timers/_edit.html.erb
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<h3 class="title"><%= l(:label_spent_time) %></h3>
|
||||||
|
|
||||||
|
<%= labelled_form_for @time_entry, url: stopwatch_path, remote: true do |f| %>
|
||||||
|
<% @time_entry.hours ||= 0 %>
|
||||||
|
<%= render partial: 'timelog/form', locals: { f: f } %>
|
||||||
|
|
||||||
|
<p class="buttons">
|
||||||
|
<%= submit_tag l(:button_create) %>
|
||||||
|
<%= submit_tag l(:button_cancel), name: nil, onclick: "hideModal(this);", type: 'button' %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
12
app/views/stopwatch_timers/_new.html.erb
Normal file
12
app/views/stopwatch_timers/_new.html.erb
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<h3 class="title"><%= l(:button_log_time) %></h3>
|
||||||
|
|
||||||
|
<%= labelled_form_for @time_entry, url: stopwatch_timer_path, method: :post, remote: true do |f| %>
|
||||||
|
<% @time_entry.hours ||= 0 %>
|
||||||
|
<%= render partial: 'timelog/form', locals: { f: f } %>
|
||||||
|
|
||||||
|
<p class="buttons">
|
||||||
|
<%= submit_tag l(:button_create) %>
|
||||||
|
<%= submit_tag l(:button_cancel), name: nil, onclick: "hideModal(this);", type: 'button' %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
2
app/views/stopwatch_timers/new.js.erb
Normal file
2
app/views/stopwatch_timers/new.js.erb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
$('#ajax-modal').html('<%= j render partial: 'stopwatch_timers/new' %>');
|
||||||
|
showModal('ajax-modal', '600px');
|
||||||
1
config/locales/en.yml
Normal file
1
config/locales/en.yml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
en:
|
||||||
2
config/routes.rb
Normal file
2
config/routes.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
resource :stopwatch_timer, only: %i(new create edit update)
|
||||||
|
|
||||||
25
init.rb
Normal file
25
init.rb
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
require 'redmine'
|
||||||
|
require_dependency 'stopwatch'
|
||||||
|
require 'stopwatch/hooks'
|
||||||
|
|
||||||
|
Redmine::Plugin.register :stopwatch do
|
||||||
|
name 'Redmine Stopwatch Plugin'
|
||||||
|
author 'Jens Krämer'
|
||||||
|
author_url 'https://jkraemer.net/'
|
||||||
|
description 'Adds start/stop timer functionality'
|
||||||
|
version '0.1.0'
|
||||||
|
|
||||||
|
requires_redmine version_or_higher: '3.4.0'
|
||||||
|
|
||||||
|
menu :account_menu, :stopwatch,
|
||||||
|
:new_stopwatch_timer_path,
|
||||||
|
caption: :button_log_time,
|
||||||
|
html: { method: :get, id: 'stopwatch-toggle', 'data-remote': true },
|
||||||
|
before: :my_account,
|
||||||
|
if: ->(*_){ User.current.logged? }
|
||||||
|
end
|
||||||
|
|
||||||
|
Rails.configuration.to_prepare do
|
||||||
|
Stopwatch::UserPatch.apply
|
||||||
|
end
|
||||||
|
|
||||||
3
lib/stopwatch.rb
Normal file
3
lib/stopwatch.rb
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module Stopwatch
|
||||||
|
|
||||||
|
end
|
||||||
0
lib/stopwatch/hooks.rb
Normal file
0
lib/stopwatch/hooks.rb
Normal file
44
lib/stopwatch/start_timer.rb
Normal file
44
lib/stopwatch/start_timer.rb
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
module Stopwatch
|
||||||
|
class StartTimer
|
||||||
|
|
||||||
|
Result = ImmutableStruct.new(:success?, :error)
|
||||||
|
|
||||||
|
def initialize(time_entry, user: User.current)
|
||||||
|
@time_entry = time_entry
|
||||||
|
@user = user
|
||||||
|
end
|
||||||
|
|
||||||
|
def call
|
||||||
|
if @time_entry.project && @user.allowed_to?(:log_time, @time_entry.project)
|
||||||
|
return Result.new(error: :unauthorized)
|
||||||
|
end
|
||||||
|
|
||||||
|
stop_existing_timer
|
||||||
|
|
||||||
|
@time_entry.hours = 0 if @time_entry.hours.nil?
|
||||||
|
|
||||||
|
if @time_entry.save
|
||||||
|
start_new_timer
|
||||||
|
return Result.new(success: true)
|
||||||
|
else
|
||||||
|
Rails.logger.error("could not save time entry: \n#{@time_entry.errors.inspect}")
|
||||||
|
return Result.new(error: :invalid)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def start_new_timer
|
||||||
|
timer = Timer.new @user
|
||||||
|
timer.start @time_entry
|
||||||
|
end
|
||||||
|
|
||||||
|
def stop_existing_timer
|
||||||
|
timer = Timer.new @user
|
||||||
|
if timer.running?
|
||||||
|
timer.stop
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
51
lib/stopwatch/timer.rb
Normal file
51
lib/stopwatch/timer.rb
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
require 'json'
|
||||||
|
|
||||||
|
module Stopwatch
|
||||||
|
class Timer
|
||||||
|
attr_reader :user
|
||||||
|
|
||||||
|
def initialize(user)
|
||||||
|
@user = user
|
||||||
|
end
|
||||||
|
|
||||||
|
def running?
|
||||||
|
data[:started_at].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def start(time_entry = nil)
|
||||||
|
fail "timer is already running" if running?
|
||||||
|
data[:started_at] = Time.now.to_i
|
||||||
|
data[:time_entry_id] = time_entry.id if time_entry
|
||||||
|
save
|
||||||
|
end
|
||||||
|
|
||||||
|
def stop
|
||||||
|
if hours = runtime_hours and
|
||||||
|
time_entry = TimeEntry.find_by_id(data[:time_entry_id])
|
||||||
|
|
||||||
|
time_entry.update_column :hours, time_entry.hours + hours
|
||||||
|
end
|
||||||
|
data[:started_at] = nil
|
||||||
|
save
|
||||||
|
end
|
||||||
|
|
||||||
|
def save
|
||||||
|
user.pref[:current_timer] = data.to_json
|
||||||
|
user.pref.save
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def data
|
||||||
|
@data ||= (t = user.pref[:current_timer]) ? JSON.parse(t).symbolize_keys : {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def runtime_hours
|
||||||
|
if start = data[:started_at]
|
||||||
|
runtime = Time.now.to_i - start
|
||||||
|
return runtime.to_f / 1.hour
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
11
lib/stopwatch/user_patch.rb
Normal file
11
lib/stopwatch/user_patch.rb
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
module Stopwatch
|
||||||
|
module UserPatch
|
||||||
|
def self.apply
|
||||||
|
User.prepend self unless User < self
|
||||||
|
end
|
||||||
|
|
||||||
|
def timer_running?
|
||||||
|
Stopwatch::Timer.new(self).running?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
5
test/test_helper.rb
Normal file
5
test/test_helper.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper')
|
||||||
|
|
||||||
|
class ActiveSupport::TestCase
|
||||||
|
|
||||||
|
end
|
||||||
40
test/unit/start_timer_test.rb
Normal file
40
test/unit/start_timer_test.rb
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
require_relative '../test_helper'
|
||||||
|
|
||||||
|
class StartTimerTest < ActiveSupport::TestCase
|
||||||
|
fixtures :users, :user_preferences, :time_entries, :projects,
|
||||||
|
:roles, :member_roles, :members, :enumerations
|
||||||
|
|
||||||
|
setup do
|
||||||
|
@user = User.find 1
|
||||||
|
@time_entry = TimeEntry.last
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should start timer" do
|
||||||
|
refute User.find(@user.id).timer_running?
|
||||||
|
assert r = Stopwatch::StartTimer.new(@time_entry, user: @user).call
|
||||||
|
assert r.success?, r.inspect
|
||||||
|
assert User.find(@user.id).timer_running?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should stop and save existing 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
|
||||||
|
another = TimeEntry.new(@time_entry.attributes)
|
||||||
|
another.user = @user
|
||||||
|
r = Stopwatch::StartTimer.new(another, user: @user).call
|
||||||
|
assert r.success?
|
||||||
|
@time_entry.reload
|
||||||
|
assert_equal hours+1, @time_entry.hours
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
52
test/unit/timer_test.rb
Normal file
52
test/unit/timer_test.rb
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
require_relative '../test_helper'
|
||||||
|
|
||||||
|
class TimerTest < ActiveSupport::TestCase
|
||||||
|
fixtures :users, :user_preferences, :time_entries
|
||||||
|
|
||||||
|
setup do
|
||||||
|
@user = User.find 1
|
||||||
|
@timer = Stopwatch::Timer.new @user
|
||||||
|
@data = @timer.send :data
|
||||||
|
@time_entry = TimeEntry.last
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should know wether its running" do
|
||||||
|
refute @timer.running?
|
||||||
|
@data[:started_at] = 5.minutes.ago
|
||||||
|
assert @timer.running?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should start timer" do
|
||||||
|
assert_nil @data[:started_at]
|
||||||
|
@timer.start @time_entry
|
||||||
|
assert @data[:started_at].present?
|
||||||
|
assert_equal @time_entry.id, @data[:time_entry_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should stop timer and update time entry" do
|
||||||
|
hours = @time_entry.hours
|
||||||
|
@timer.start @time_entry
|
||||||
|
@data[:started_at] = 1.hour.ago.to_i
|
||||||
|
@timer.stop
|
||||||
|
|
||||||
|
@time_entry.reload
|
||||||
|
assert_equal hours + 1, @time_entry.hours
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should save and restore" do
|
||||||
|
hours = @time_entry.hours
|
||||||
|
@timer.start @time_entry
|
||||||
|
@data[:started_at] = 1.hour.ago.to_i
|
||||||
|
@timer.save
|
||||||
|
|
||||||
|
timer = Stopwatch::Timer.new User.find(@user.id)
|
||||||
|
assert timer.running?
|
||||||
|
timer.stop
|
||||||
|
|
||||||
|
@time_entry.reload
|
||||||
|
assert_equal hours + 1, @time_entry.hours
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
21
test/unit/user_test.rb
Normal file
21
test/unit/user_test.rb
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
require_relative '../test_helper'
|
||||||
|
|
||||||
|
class UserTest < ActiveSupport::TestCase
|
||||||
|
fixtures :users, :user_preferences
|
||||||
|
|
||||||
|
setup do
|
||||||
|
@user = User.find 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should get inactive timer state" do
|
||||||
|
refute @user.timer_running?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should start timer and get running state" do
|
||||||
|
t = Stopwatch::Timer.new @user
|
||||||
|
t.start
|
||||||
|
t.save
|
||||||
|
assert @user.timer_running?
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user