initial commit

This commit is contained in:
Jens Kraemer
2020-04-22 18:03:56 +08:00
commit 70257cdee0
16 changed files with 331 additions and 0 deletions

View 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

View 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 %>

View 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 %>

View 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
View File

@@ -0,0 +1 @@
en:

2
config/routes.rb Normal file
View File

@@ -0,0 +1,2 @@
resource :stopwatch_timer, only: %i(new create edit update)

25
init.rb Normal file
View 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
View File

@@ -0,0 +1,3 @@
module Stopwatch
end

0
lib/stopwatch/hooks.rb Normal file
View File

View 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
View 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

View 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
View File

@@ -0,0 +1,5 @@
require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper')
class ActiveSupport::TestCase
end

View 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
View 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
View 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