diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb index b517ddaebd781bdd10b8f0cc886d54fea76e1e99..9f403d96ed548a6102a1ab9fe4f985d4c042183d 100644 --- a/app/models/concerns/time_trackable.rb +++ b/app/models/concerns/time_trackable.rb @@ -9,7 +9,7 @@ module TimeTrackable extend ActiveSupport::Concern included do - attr_reader :time_spent, :time_spent_user + attr_reader :time_spent, :time_spent_user, :spent_at alias_method :time_spent?, :time_spent @@ -24,6 +24,7 @@ module TimeTrackable def spend_time(options) @time_spent = options[:duration] @time_spent_user = options[:user] + @spent_at = options[:spent_at] @original_total_time_spent = nil return if @time_spent == 0 @@ -55,7 +56,11 @@ module TimeTrackable end def add_or_subtract_spent_time - timelogs.new(time_spent: time_spent, user: @time_spent_user) + timelogs.new( + time_spent: time_spent, + user: @time_spent_user, + spent_at: @spent_at + ) end def check_negative_time_spent diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 955d934838bc6a3776ea33662fb27dd29afc79c6..06ac86cd5a981cea05523846ce4be67b976015cc 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -381,7 +381,7 @@ module QuickActions end desc 'Add or substract spent time' - explanation do |time_spent| + explanation do |time_spent, time_spent_date| if time_spent if time_spent > 0 verb = 'Adds' @@ -394,16 +394,20 @@ module QuickActions "#{verb} #{Gitlab::TimeTrackingFormatter.output(value)} spent time." end end - params '<1h 30m | -1h 30m>' + params ' ' condition do current_user.can?(:"admin_#{issuable.to_ability_name}", issuable) end - parse_params do |raw_duration| - Gitlab::TimeTrackingFormatter.parse(raw_duration) + parse_params do |raw_time_date| + Gitlab::QuickActions::SpendTimeAndDateSeparator.new(raw_time_date).execute end - command :spend do |time_spent| + command :spend do |time_spent, time_spent_date| if time_spent - @updates[:spend_time] = { duration: time_spent, user: current_user } + @updates[:spend_time] = { + duration: time_spent, + user: current_user, + spent_at: time_spent_date + } end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index a52dce6cb4bc5db2f1678e620f0991c44c2d6a7e..0bce20ae5b77edfa8025d28614a72a1d5e513471 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -195,9 +195,11 @@ module SystemNoteService if time_spent == :reset body = "removed time spent" else + spent_at = noteable.spent_at parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs) action = time_spent > 0 ? 'added' : 'subtracted' body = "#{action} #{parsed_time} of time spent" + body << " at #{spent_at}" if spent_at end create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking')) diff --git a/changelogs/unreleased/1312-time-spent-at.yml b/changelogs/unreleased/1312-time-spent-at.yml new file mode 100644 index 0000000000000000000000000000000000000000..c029497e9abce7915d648c38c93372b12f637427 --- /dev/null +++ b/changelogs/unreleased/1312-time-spent-at.yml @@ -0,0 +1,5 @@ +--- +title: Added possibility to enter past date in /spend command to log time in the past +merge_request: 3044 +author: g3dinua, LockiStrike +type: changed diff --git a/db/migrate/20170909150936_add_spent_at_to_timelogs.rb b/db/migrate/20170909150936_add_spent_at_to_timelogs.rb new file mode 100644 index 0000000000000000000000000000000000000000..ffff719c2894602424ccb16a29bdc1b4b89247cb --- /dev/null +++ b/db/migrate/20170909150936_add_spent_at_to_timelogs.rb @@ -0,0 +1,11 @@ +class AddSpentAtToTimelogs < ActiveRecord::Migration + DOWNTIME = false + + def up + add_column :timelogs, :spent_at, :datetime_with_timezone + end + + def down + remove_column :timelogs, :spent_at + end +end diff --git a/db/schema.rb b/db/schema.rb index aac37b6b45572b36abd22b6673e64730268c3849..8aadcfeb7d1ee723ddc26e76a41cd337fd3435be 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1550,6 +1550,7 @@ ActiveRecord::Schema.define(version: 20171006091000) do t.datetime "updated_at", null: false t.integer "issue_id" t.integer "merge_request_id" + t.datetime_with_timezone "spent_at" end add_index "timelogs", ["issue_id"], name: "index_timelogs_on_issue_id", using: :btree diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md index 6a5d2d40927ccf98816f791b24c82e41ff95016b..e81e935e37d2e3e2fd6785b11f978c20c9c00b9a 100644 --- a/doc/user/project/quick_actions.md +++ b/doc/user/project/quick_actions.md @@ -32,7 +32,7 @@ do. | `/wip` | Toggle the Work In Progress status | | /estimate <1w 3d 2h 14m> | Set time estimate | | `/remove_estimate` | Remove estimated time | -| /spend <1h 30m | -1h 5m> | Add or subtract spent time | +| /spend <time(1h 30m | -1h 5m)> <date(YYYY-MM-DD)> | Add or subtract spent time; optionally, specify the date that time was spent on | | `/remove_time_spent` | Remove time spent | | `/target_branch ` | Set target branch for current merge request | | `/award :emoji:` | Toggle award for :emoji: | diff --git a/lib/gitlab/quick_actions/spend_time_and_date_separator.rb b/lib/gitlab/quick_actions/spend_time_and_date_separator.rb new file mode 100644 index 0000000000000000000000000000000000000000..3f52402b31fdbb39e1361badb34139b46df4dc7d --- /dev/null +++ b/lib/gitlab/quick_actions/spend_time_and_date_separator.rb @@ -0,0 +1,54 @@ +module Gitlab + module QuickActions + # This class takes spend command argument + # and separates date and time from spend command arguments if it present + # example: + # spend_command_time_and_date = "15m 2017-01-02" + # SpendTimeAndDateSeparator.new(spend_command_time_and_date).execute + # => [900, Mon, 02 Jan 2017] + # if date doesn't present return time with current date + # in other cases return nil + class SpendTimeAndDateSeparator + DATE_REGEX = /(\d{2,4}[\/\-.]\d{1,2}[\/\-.]\d{1,2})/ + + def initialize(spend_command_arg) + @spend_arg = spend_command_arg + end + + def execute + return if @spend_arg.blank? + return [get_time, DateTime.now.to_date] unless date_present? + return unless valid_date? + + [get_time, get_date] + end + + private + + def get_time + raw_time = @spend_arg.gsub(DATE_REGEX, '') + Gitlab::TimeTrackingFormatter.parse(raw_time) + end + + def get_date + string_date = @spend_arg.match(DATE_REGEX)[0] + Date.parse(string_date) + end + + def date_present? + DATE_REGEX =~ @spend_arg + end + + def valid_date? + string_date = @spend_arg.match(DATE_REGEX)[0] + date = Date.parse(string_date) rescue nil + + date_past_or_today?(date) + end + + def date_past_or_today?(date) + date&.past? || date&.today? + end + end + end +end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 80d92b2e6a364558077d3908f1d4603308e82e84..121c0ed04edfa3a905212bd9b25e3278c3d6ac45 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -496,6 +496,7 @@ Timelog: - merge_request_id - issue_id - user_id +- spent_at - created_at - updated_at ProjectAutoDevops: diff --git a/spec/lib/gitlab/quick_actions/spend_time_and_date_separator_spec.rb b/spec/lib/gitlab/quick_actions/spend_time_and_date_separator_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..8b58f0b372528c944a6eea44c1db82e4257c50cc --- /dev/null +++ b/spec/lib/gitlab/quick_actions/spend_time_and_date_separator_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' + +describe Gitlab::QuickActions::SpendTimeAndDateSeparator do + subject { described_class } + + shared_examples 'arg line with invalid parameters' do + it 'return nil' do + expect(subject.new(invalid_arg).execute).to eq(nil) + end + end + + shared_examples 'arg line with valid parameters' do + it 'return time and date array' do + expect(subject.new(valid_arg).execute).to eq(expected_response) + end + end + + describe '#execute' do + context 'invalid paramenter in arg line' do + context 'empty arg line' do + it_behaves_like 'arg line with invalid parameters' do + let(:invalid_arg) { '' } + end + end + + context 'future date in arg line' do + it_behaves_like 'arg line with invalid parameters' do + let(:invalid_arg) { '10m 6023-02-02' } + end + end + + context 'unparseable date(invalid mixes of delimiters)' do + it_behaves_like 'arg line with invalid parameters' do + let(:invalid_arg) { '10m 2017.02-02' } + end + end + + context 'trash in arg line' do + let(:invalid_arg) { 'dfjkghdskjfghdjskfgdfg' } + + it 'return nil as time value' do + time_date_response = subject.new(invalid_arg).execute + + expect(time_date_response).to be_an_instance_of(Array) + expect(time_date_response.first).to eq(nil) + end + end + end + + context 'only time present in arg line' do + it_behaves_like 'arg line with valid parameters' do + let(:valid_arg) { '2m 3m 5m 1h' } + let(:time) { Gitlab::TimeTrackingFormatter.parse(valid_arg) } + let(:date) { DateTime.now.to_date } + let(:expected_response) { [time, date] } + end + end + + context 'simple time with date in arg line' do + it_behaves_like 'arg line with valid parameters' do + let(:raw_time) { '10m' } + let(:raw_date) { '2016-02-02' } + let(:valid_arg) { "#{raw_time} #{raw_date}" } + let(:date) { Date.parse(raw_date) } + let(:time) { Gitlab::TimeTrackingFormatter.parse(raw_time) } + let(:expected_response) { [time, date] } + end + end + + context 'composite time with date in arg line' do + it_behaves_like 'arg line with valid parameters' do + let(:raw_time) { '2m 10m 1h 3d' } + let(:raw_date) { '2016/02/02' } + let(:valid_arg) { "#{raw_time} #{raw_date}" } + let(:date) { Date.parse(raw_date) } + let(:time) { Gitlab::TimeTrackingFormatter.parse(raw_time) } + let(:expected_response) { [time, date] } + end + end + end +end diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index 6926ac85de327fc1b5611e3ef3b4f71d2d053ce4..c35177f6ebc26c68e2a93e3bedd2c1ac4462bd9c 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -207,7 +207,11 @@ describe QuickActions::InterpretService do it 'populates spend_time: 3600 if content contains /spend 1h' do _, updates = service.execute(content, issuable) - expect(updates).to eq(spend_time: { duration: 3600, user: developer }) + expect(updates).to eq(spend_time: { + duration: 3600, + user: developer, + spent_at: DateTime.now.to_date + }) end end @@ -215,7 +219,39 @@ describe QuickActions::InterpretService do it 'populates spend_time: -1800 if content contains /spend -30m' do _, updates = service.execute(content, issuable) - expect(updates).to eq(spend_time: { duration: -1800, user: developer }) + expect(updates).to eq(spend_time: { + duration: -1800, + user: developer, + spent_at: DateTime.now.to_date + }) + end + end + + shared_examples 'spend command with valid date' do + it 'populates spend time: 1800 with date in date type format' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(spend_time: { + duration: 1800, + user: developer, + spent_at: Date.parse(date) + }) + end + end + + shared_examples 'spend command with invalid date' do + it 'will not create any note and timelog' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq({}) + end + end + + shared_examples 'spend command with future date' do + it 'will not create any note and timelog' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq({}) end end @@ -669,6 +705,22 @@ describe QuickActions::InterpretService do let(:issuable) { issue } end + it_behaves_like 'spend command with valid date' do + let(:date) { '2016-02-02' } + let(:content) { "/spend 30m #{date}" } + let(:issuable) { issue } + end + + it_behaves_like 'spend command with invalid date' do + let(:content) { '/spend 30m 17-99-99' } + let(:issuable) { issue } + end + + it_behaves_like 'spend command with future date' do + let(:content) { '/spend 30m 6017-10-10' } + let(:issuable) { issue } + end + it_behaves_like 'empty command' do let(:content) { '/spend' } let(:issuable) { issue }