File: time_trackable.rb

package info (click to toggle)
gitlab 17.6.5-19
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 629,368 kB
  • sloc: ruby: 1,915,304; javascript: 557,307; sql: 60,639; xml: 6,509; sh: 4,567; makefile: 1,239; python: 406
file content (154 lines) | stat: -rw-r--r-- 4,664 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# frozen_string_literal: true

# == TimeTrackable concern
#
# Contains functionality related to objects that support time tracking.
#
# Used by Issue and MergeRequest.
#

module TimeTrackable
  extend ActiveSupport::Concern

  include Gitlab::Utils::StrongMemoize

  included do
    attr_reader :time_spent, :time_spent_user, :spent_at, :summary

    alias_method :time_spent?, :time_spent

    validate :check_time_estimate
    validate :check_negative_time_spent

    has_many :timelogs, dependent: :destroy, autosave: true # rubocop:disable Cop/ActiveRecordDependent
    before_save :set_time_estimate_default_value
    after_save :clear_memoized_total_time_spent
  end

  def clear_memoized_total_time_spent
    clear_memoization(:total_time_spent)
  end

  def reset
    clear_memoized_total_time_spent

    super
  end

  def reload(*args)
    clear_memoized_total_time_spent

    super(*args)
  end

  # rubocop:disable Gitlab/ModuleWithInstanceVariables
  def spend_time(options)
    @time_spent = options[:duration]
    @time_spent_note_id = options[:note_id]
    @time_spent_user = User.find(options[:user_id])
    @spent_at = options[:spent_at]
    @summary = options[:summary]
    @original_total_time_spent = nil
    @category_id = category_id(options[:category])

    return if @time_spent == 0

    @timelog = if @time_spent == :reset
                 reset_spent_time
               else
                 add_or_subtract_spent_time
               end
  end
  alias_method :spend_time=, :spend_time
  # rubocop:enable Gitlab/ModuleWithInstanceVariables

  def total_time_spent
    sum = timelogs.sum(:time_spent)

    # A new restriction has been introduced to limit total time spent to -
    # Timelog::MAX_TOTAL_TIME_SPENT or 3.154e+7 seconds (approximately a year, a generous limit)
    # Since there could be existing records that breach the limit, check and return the maximum/minimum allowed value.
    # (some issuable might have total time spent that's negative because a validation was missing.)
    sum.clamp(-Timelog::MAX_TOTAL_TIME_SPENT, Timelog::MAX_TOTAL_TIME_SPENT)
  end
  strong_memoize_attr :total_time_spent

  def human_total_time_spent
    Gitlab::TimeTrackingFormatter.output(total_time_spent)
  end

  def time_change
    @timelog&.time_spent.to_i # rubocop:disable Gitlab/ModuleWithInstanceVariables
  end

  def human_time_change
    Gitlab::TimeTrackingFormatter.output(time_change)
  end

  def human_time_estimate
    Gitlab::TimeTrackingFormatter.output(time_estimate)
  end

  def time_estimate=(val)
    val.is_a?(Integer) ? super([val, Gitlab::Database::MAX_INT_VALUE].min) : super(val)
  end

  def time_estimate
    super || self.class.column_defaults['time_estimate']
  end

  def set_time_estimate_default_value
    return if new_record?
    return unless has_attribute?(:time_estimate)
    # time estimate can be set to nil, in case of an invalid value, e.g. a String instead of a number, in which case
    # we should not be overwriting it to default value, but rather have the validation catch the error
    return if time_estimate_changed?

    self.time_estimate = self.class.column_defaults['time_estimate'] if read_attribute(:time_estimate).nil?
  end

  private

  def reset_spent_time
    timelogs.new(time_spent: total_time_spent * -1, user: @time_spent_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables
  end

  # rubocop:disable Gitlab/ModuleWithInstanceVariables
  def add_or_subtract_spent_time
    timelogs.new(
      time_spent: time_spent,
      note_id: @time_spent_note_id,
      user: @time_spent_user,
      spent_at: @spent_at,
      summary: @summary,
      timelog_category_id: @category_id
    )
  end
  # rubocop:enable Gitlab/ModuleWithInstanceVariables

  def check_negative_time_spent
    return if time_spent.nil? || time_spent == :reset

    if time_spent < 0 && (time_spent.abs > original_total_time_spent)
      errors.add(:base, _('Time to subtract exceeds the total time spent'))
    end
  end

  # we need to cache the total time spent so multiple calls to #valid?
  # doesn't give a false error
  def original_total_time_spent
    @original_total_time_spent ||= total_time_spent
  end

  def check_time_estimate
    # we'll set the time_tracking to zero at DB level through default value
    return unless time_estimate_changed?
    return if read_attribute(:time_estimate).is_a?(Numeric) && read_attribute(:time_estimate) >= 0

    errors.add(:time_estimate, _('must have a valid format and be greater than or equal to zero.'))
  end

  def category_id(category)
    TimeTracking::TimelogCategory.find_by_name(project&.root_namespace, category).first&.id
  end
end