commit 70257cdee00dc8d8c81001e89b262545dc5e3940 Author: Jens Kraemer Date: Wed Apr 22 18:03:56 2020 +0800 initial commit diff --git a/app/controllers/stopwatch_timers_controller.rb b/app/controllers/stopwatch_timers_controller.rb new file mode 100644 index 0000000..104e439 --- /dev/null +++ b/app/controllers/stopwatch_timers_controller.rb @@ -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 diff --git a/app/views/stopwatch_timers/_edit.html.erb b/app/views/stopwatch_timers/_edit.html.erb new file mode 100644 index 0000000..31638b0 --- /dev/null +++ b/app/views/stopwatch_timers/_edit.html.erb @@ -0,0 +1,11 @@ +

<%= l(:label_spent_time) %>

+ +<%= labelled_form_for @time_entry, url: stopwatch_path, remote: true do |f| %> + <% @time_entry.hours ||= 0 %> + <%= render partial: 'timelog/form', locals: { f: f } %> + +

+ <%= submit_tag l(:button_create) %> + <%= submit_tag l(:button_cancel), name: nil, onclick: "hideModal(this);", type: 'button' %> +

+<% end %> diff --git a/app/views/stopwatch_timers/_new.html.erb b/app/views/stopwatch_timers/_new.html.erb new file mode 100644 index 0000000..4888886 --- /dev/null +++ b/app/views/stopwatch_timers/_new.html.erb @@ -0,0 +1,12 @@ +

<%= l(:button_log_time) %>

+ +<%= 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 } %> + +

+ <%= submit_tag l(:button_create) %> + <%= submit_tag l(:button_cancel), name: nil, onclick: "hideModal(this);", type: 'button' %> +

+<% end %> + diff --git a/app/views/stopwatch_timers/new.js.erb b/app/views/stopwatch_timers/new.js.erb new file mode 100644 index 0000000..1ffb4da --- /dev/null +++ b/app/views/stopwatch_timers/new.js.erb @@ -0,0 +1,2 @@ +$('#ajax-modal').html('<%= j render partial: 'stopwatch_timers/new' %>'); +showModal('ajax-modal', '600px'); diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000..63f1c3e --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1 @@ +en: diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000..4b3347f --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,2 @@ +resource :stopwatch_timer, only: %i(new create edit update) + diff --git a/init.rb b/init.rb new file mode 100644 index 0000000..3dc3b7a --- /dev/null +++ b/init.rb @@ -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 + diff --git a/lib/stopwatch.rb b/lib/stopwatch.rb new file mode 100644 index 0000000..9e64f03 --- /dev/null +++ b/lib/stopwatch.rb @@ -0,0 +1,3 @@ +module Stopwatch + +end diff --git a/lib/stopwatch/hooks.rb b/lib/stopwatch/hooks.rb new file mode 100644 index 0000000..e69de29 diff --git a/lib/stopwatch/start_timer.rb b/lib/stopwatch/start_timer.rb new file mode 100644 index 0000000..621bb55 --- /dev/null +++ b/lib/stopwatch/start_timer.rb @@ -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 diff --git a/lib/stopwatch/timer.rb b/lib/stopwatch/timer.rb new file mode 100644 index 0000000..e33f61f --- /dev/null +++ b/lib/stopwatch/timer.rb @@ -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 diff --git a/lib/stopwatch/user_patch.rb b/lib/stopwatch/user_patch.rb new file mode 100644 index 0000000..07cb1b1 --- /dev/null +++ b/lib/stopwatch/user_patch.rb @@ -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 diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..6998204 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,5 @@ +require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper') + +class ActiveSupport::TestCase + +end diff --git a/test/unit/start_timer_test.rb b/test/unit/start_timer_test.rb new file mode 100644 index 0000000..0b09f18 --- /dev/null +++ b/test/unit/start_timer_test.rb @@ -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 + + diff --git a/test/unit/timer_test.rb b/test/unit/timer_test.rb new file mode 100644 index 0000000..f35a667 --- /dev/null +++ b/test/unit/timer_test.rb @@ -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 + diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb new file mode 100644 index 0000000..db86daa --- /dev/null +++ b/test/unit/user_test.rb @@ -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