From 20ccbba2c1666bfc07b095b1606f575f1670b833 Mon Sep 17 00:00:00 2001 From: Jens Kraemer Date: Fri, 24 Apr 2020 17:20:18 +0800 Subject: [PATCH] works, basically --- .../stopwatch_timers_controller.rb | 62 +++++++++++++++- .../hooks/_layouts_base_body_bottom.html.erb | 3 + .../hooks/_layouts_base_html_head.html.erb | 2 + .../_time_entries_context_menu_start.html.erb | 10 +++ app/views/stopwatch_timers/_edit.html.erb | 15 ++-- .../stopwatch_timers/_entries_list.html.erb | 31 ++++++++ .../stopwatch_timers/_entry_form.html.erb | 74 +++++++++++++++++++ app/views/stopwatch_timers/_new.html.erb | 10 ++- app/views/stopwatch_timers/create.js.erb | 1 + app/views/stopwatch_timers/edit.js.erb | 4 + app/views/stopwatch_timers/new.js.erb | 2 +- app/views/stopwatch_timers/start.js.erb | 9 +++ app/views/stopwatch_timers/update.js.erb | 5 ++ assets/javascripts/stopwatch.js | 24 ++++++ assets/stylesheets/stopwatch.css | 3 + config/locales/en.yml | 11 +++ config/routes.rb | 10 ++- lib/stopwatch/hooks.rb | 11 +++ lib/stopwatch/start_timer.rb | 17 ++--- lib/stopwatch/stop_timer.rb | 20 +++++ lib/stopwatch/timer.rb | 13 +++- lib/stopwatch/user_patch.rb | 5 ++ test/unit/start_timer_test.rb | 6 +- test/unit/stop_timer_test.rb | 34 +++++++++ 24 files changed, 355 insertions(+), 27 deletions(-) create mode 100644 app/views/stopwatch/hooks/_layouts_base_body_bottom.html.erb create mode 100644 app/views/stopwatch/hooks/_layouts_base_html_head.html.erb create mode 100644 app/views/stopwatch/hooks/_time_entries_context_menu_start.html.erb create mode 100644 app/views/stopwatch_timers/_entries_list.html.erb create mode 100644 app/views/stopwatch_timers/_entry_form.html.erb create mode 100644 app/views/stopwatch_timers/create.js.erb create mode 100644 app/views/stopwatch_timers/edit.js.erb create mode 100644 app/views/stopwatch_timers/start.js.erb create mode 100644 app/views/stopwatch_timers/update.js.erb create mode 100644 assets/javascripts/stopwatch.js create mode 100644 assets/stylesheets/stopwatch.css create mode 100644 lib/stopwatch/stop_timer.rb create mode 100644 test/unit/stop_timer_test.rb diff --git a/app/controllers/stopwatch_timers_controller.rb b/app/controllers/stopwatch_timers_controller.rb index 104e439..1cd8c37 100644 --- a/app/controllers/stopwatch_timers_controller.rb +++ b/app/controllers/stopwatch_timers_controller.rb @@ -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 diff --git a/app/views/stopwatch/hooks/_layouts_base_body_bottom.html.erb b/app/views/stopwatch/hooks/_layouts_base_body_bottom.html.erb new file mode 100644 index 0000000..43e4dfe --- /dev/null +++ b/app/views/stopwatch/hooks/_layouts_base_body_bottom.html.erb @@ -0,0 +1,3 @@ +<%= javascript_tag do %> + Stopwatch.currentTimerUrl = '<%= j current_stopwatch_timers_url %>'; +<% end %> diff --git a/app/views/stopwatch/hooks/_layouts_base_html_head.html.erb b/app/views/stopwatch/hooks/_layouts_base_html_head.html.erb new file mode 100644 index 0000000..5674479 --- /dev/null +++ b/app/views/stopwatch/hooks/_layouts_base_html_head.html.erb @@ -0,0 +1,2 @@ +<%= javascript_include_tag "stopwatch", plugin: "stopwatch" %> +<%= stylesheet_link_tag "stopwatch", plugin: "stopwatch" %> diff --git a/app/views/stopwatch/hooks/_time_entries_context_menu_start.html.erb b/app/views/stopwatch/hooks/_time_entries_context_menu_start.html.erb new file mode 100644 index 0000000..2080f94 --- /dev/null +++ b/app/views/stopwatch/hooks/_time_entries_context_menu_start.html.erb @@ -0,0 +1,10 @@ +<% if @time_entries.one? %> + <% time_entry = @time_entries.first %> +
  • + <% 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 %> +
  • +<% end %> diff --git a/app/views/stopwatch_timers/_edit.html.erb b/app/views/stopwatch_timers/_edit.html.erb index 31638b0..11edf50 100644 --- a/app/views/stopwatch_timers/_edit.html.erb +++ b/app/views/stopwatch_timers/_edit.html.erb @@ -1,11 +1,14 @@ -

    <%= l(:label_spent_time) %>

    +

    <%= l(:button_log_time) %> - <%= format_date User.current.today %>

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

    - <%= 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' %>

    <% end %> diff --git a/app/views/stopwatch_timers/_entries_list.html.erb b/app/views/stopwatch_timers/_entries_list.html.erb new file mode 100644 index 0000000..8e94269 --- /dev/null +++ b/app/views/stopwatch_timers/_entries_list.html.erb @@ -0,0 +1,31 @@ +<% if entries.any? %> + + + + + + + + + +<% for entry in entries %> + "> + + + + + + +<% end -%> + +
    <%= l(:label_activity) %><%= l(:label_project) %><%= l(:field_comments) %><%= l(:field_hours) %>
    <%= entry.activity %><%= entry.project %> <%= h(' - ') + link_to_issue(entry.issue, truncate: 50) if entry.issue %><%= entry.comments %><%= html_hours(format_hours(entry.hours)) %> + <% 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 %> +
    +<% end %> + + diff --git a/app/views/stopwatch_timers/_entry_form.html.erb b/app/views/stopwatch_timers/_entry_form.html.erb new file mode 100644 index 0000000..653fd0c --- /dev/null +++ b/app/views/stopwatch_timers/_entry_form.html.erb @@ -0,0 +1,74 @@ +<%= error_messages_for 'time_entry' %> + +
    + <%= t @time_entry.new_record? ? '.legend_new' : '.legend_edit' %> +

    <%= f.select :activity_id, activity_collection_for_select_options(@time_entry), :required => true %>

    + +

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

    + +

    + <%= f.text_field :issue_id, :size => 6, :required => Setting.timelog_required_fields.include?('issue_id') %> + + <%= link_to_issue(@time_entry.issue) if @time_entry.issue.try(:visible?) %> + +

    + +

    <%= f.text_field :comments, :size => 100, :maxlength => 1024, :required => Setting.timelog_required_fields.include?('comments') %>

    +

    <%= f.hours_field :hours, :size => 6, :required => true %>

    + <% @time_entry.custom_field_values.each do |value| %> +

    <%= custom_field_tag_with_label :time_entry, value %>

    + <% end %> + <%= call_hook(:view_timelog_edit_form_bottom, { :time_entry => @time_entry, :form => f }) %> +
    + +<%= 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 %> + diff --git a/app/views/stopwatch_timers/_new.html.erb b/app/views/stopwatch_timers/_new.html.erb index 4888886..7fc3853 100644 --- a/app/views/stopwatch_timers/_new.html.erb +++ b/app/views/stopwatch_timers/_new.html.erb @@ -1,12 +1,14 @@ -

    <%= l(:button_log_time) %>

    +

    <%= l(:button_log_time) %> - <%= format_date User.current.today %>

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

    <%= 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' %>

    <% end %> diff --git a/app/views/stopwatch_timers/create.js.erb b/app/views/stopwatch_timers/create.js.erb new file mode 100644 index 0000000..f66581c --- /dev/null +++ b/app/views/stopwatch_timers/create.js.erb @@ -0,0 +1 @@ +hideModal(); diff --git a/app/views/stopwatch_timers/edit.js.erb b/app/views/stopwatch_timers/edit.js.erb new file mode 100644 index 0000000..09e5caf --- /dev/null +++ b/app/views/stopwatch_timers/edit.js.erb @@ -0,0 +1,4 @@ +hideModal(); +$('#ajax-modal').html('<%= j render partial: 'stopwatch_timers/edit' %>'); +showModal('ajax-modal', '700px'); + diff --git a/app/views/stopwatch_timers/new.js.erb b/app/views/stopwatch_timers/new.js.erb index 1ffb4da..e232151 100644 --- a/app/views/stopwatch_timers/new.js.erb +++ b/app/views/stopwatch_timers/new.js.erb @@ -1,2 +1,2 @@ $('#ajax-modal').html('<%= j render partial: 'stopwatch_timers/new' %>'); -showModal('ajax-modal', '600px'); +showModal('ajax-modal', '700px'); diff --git a/app/views/stopwatch_timers/start.js.erb b/app/views/stopwatch_timers/start.js.erb new file mode 100644 index 0000000..e58756a --- /dev/null +++ b/app/views/stopwatch_timers/start.js.erb @@ -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 %>'); + diff --git a/app/views/stopwatch_timers/update.js.erb b/app/views/stopwatch_timers/update.js.erb new file mode 100644 index 0000000..38dcbc6 --- /dev/null +++ b/app/views/stopwatch_timers/update.js.erb @@ -0,0 +1,5 @@ +hideModal(); +<% if params[:continue] %> + $('#ajax-modal').html('<%= j render partial: 'stopwatch_timers/new' %>'); + showModal('ajax-modal', '600px'); +<% end %> diff --git a/assets/javascripts/stopwatch.js b/assets/javascripts/stopwatch.js new file mode 100644 index 0000000..470f34c --- /dev/null +++ b/assets/javascripts/stopwatch.js @@ -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); diff --git a/assets/stylesheets/stopwatch.css b/assets/stylesheets/stopwatch.css new file mode 100644 index 0000000..53cc4f2 --- /dev/null +++ b/assets/stylesheets/stopwatch.css @@ -0,0 +1,3 @@ +tr.time-entry.running td.hours { + color: red; +} diff --git a/config/locales/en.yml b/config/locales/en.yml index 63f1c3e..773d28a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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 diff --git a/config/routes.rb b/config/routes.rb index 4b3347f..814cb6f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/lib/stopwatch/hooks.rb b/lib/stopwatch/hooks.rb index e69de29..e035f2e 100644 --- a/lib/stopwatch/hooks.rb +++ b/lib/stopwatch/hooks.rb @@ -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 diff --git a/lib/stopwatch/start_timer.rb b/lib/stopwatch/start_timer.rb index 621bb55..852f05f 100644 --- a/lib/stopwatch/start_timer.rb +++ b/lib/stopwatch/start_timer.rb @@ -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 diff --git a/lib/stopwatch/stop_timer.rb b/lib/stopwatch/stop_timer.rb new file mode 100644 index 0000000..2afc4bf --- /dev/null +++ b/lib/stopwatch/stop_timer.rb @@ -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 + diff --git a/lib/stopwatch/timer.rb b/lib/stopwatch/timer.rb index e33f61f..e5eacc1 100644 --- a/lib/stopwatch/timer.rb +++ b/lib/stopwatch/timer.rb @@ -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 diff --git a/lib/stopwatch/user_patch.rb b/lib/stopwatch/user_patch.rb index 07cb1b1..c0a5d36 100644 --- a/lib/stopwatch/user_patch.rb +++ b/lib/stopwatch/user_patch.rb @@ -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 diff --git a/test/unit/start_timer_test.rb b/test/unit/start_timer_test.rb index 0b09f18..1ed2c07 100644 --- a/test/unit/start_timer_test.rb +++ b/test/unit/start_timer_test.rb @@ -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 diff --git a/test/unit/stop_timer_test.rb b/test/unit/stop_timer_test.rb new file mode 100644 index 0000000..05aad3f --- /dev/null +++ b/test/unit/stop_timer_test.rb @@ -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 + + +