Welcome to mirror list, hosted at ThFree Co, Russian Federation.

timebox.rb « concerns « models « app - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 5b74e88429c645f59de0e03b21bb5d9912499bbc (plain)
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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# frozen_string_literal: true

module Timebox
  extend ActiveSupport::Concern

  include CacheMarkdownField
  include Gitlab::SQL::Pattern
  include Referable
  include StripAttribute

  TimeboxStruct = Struct.new(:title, :name, :id, :class_name) do
    # Ensure these models match the interface required for exporting
    def serializable_hash(_opts = {})
      { title: title, name: name, id: id }
    end

    def self.declarative_policy_class
      "TimeboxPolicy"
    end

    def to_global_id
      ::Gitlab::GlobalId.build(self, model_name: class_name, id: id)
    end
  end

  # Represents a "No Timebox" state used for filtering Issues and Merge
  # Requests that have no timeboxes assigned.
  None = TimeboxStruct.new('No Timebox', 'No Timebox', 0)
  Any = TimeboxStruct.new('Any Timebox', '', -1)
  Upcoming = TimeboxStruct.new('Upcoming', '#upcoming', -2)
  Started = TimeboxStruct.new('Started', '#started', -3)

  included do
    # Defines the same constants above, but inside the including class.
    const_set :None, TimeboxStruct.new("No #{self.name}", "No #{self.name}", 0, self.name)
    const_set :Any, TimeboxStruct.new("Any #{self.name}", '', -1, self.name)
    const_set :Upcoming, TimeboxStruct.new('Upcoming', '#upcoming', -2, self.name)
    const_set :Started, TimeboxStruct.new('Started', '#started', -3, self.name)

    alias_method :timebox_id, :id

    validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? }
    validate :dates_within_4_digits

    cache_markdown_field :title, pipeline: :single_line
    cache_markdown_field :description, issuable_reference_expansion_enabled: true

    has_many :issues
    has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
    has_many :merge_requests

    scope :closed, -> { with_state(:closed) }
    scope :with_title, -> (title) { where(title: title) }

    # A timebox is within the timeframe (start_date, end_date) if it overlaps
    # with that timeframe:
    #
    #        [  timeframe   ]
    #  ----| ................     # Not overlapping
    #   |--| ................     # Not overlapping
    #  ------|...............     # Overlapping
    #  -----------------------|   # Overlapping
    #  ---------|............     # Overlapping
    #     |-----|............     # Overlapping
    #        |--------------|     # Overlapping
    #     |--------------------|  # Overlapping
    #        ...|-----|......     # Overlapping
    #        .........|-----|     # Overlapping
    #        .........|---------  # Overlapping
    #      |--------------------  # Overlapping
    #        .........|--------|  # Overlapping
    #        ...............|--|  # Overlapping
    #        ............... |-|  # Not Overlapping
    #        ............... |--  # Not Overlapping
    #
    # where: . = in timeframe
    #        ---| no start
    #        |--- no end
    #        |--| defined start and end
    #
    scope :within_timeframe, -> (start_date, end_date) do
      where('start_date is not NULL or due_date is not NULL')
        .where('start_date is NULL or start_date <= ?', end_date)
        .where('due_date is NULL or due_date >= ?', start_date)
    end

    strip_attributes! :title

    alias_attribute :name, :title
  end

  class_methods do
    # Searches for timeboxes with a matching title or description.
    #
    # This method uses ILIKE on PostgreSQL
    #
    # query - The search query as a String
    #
    # Returns an ActiveRecord::Relation.
    def search(query)
      fuzzy_search(query, [:title, :description])
    end

    def filter_by_state(timeboxes, state)
      case state
      when 'closed' then timeboxes.closed
      when 'all' then timeboxes
      else timeboxes.active
      end
    end

    def predefined_id?(id)
      [Any.id, None.id, Upcoming.id, Started.id].include?(id)
    end

    def predefined?(timebox)
      predefined_id?(timebox&.id)
    end
  end

  def to_reference
    raise NotImplementedError
  end

  def reference_link_text(from = nil)
    self.class.reference_prefix + self.title
  end

  def title=(value)
    write_attribute(:title, sanitize_title(value)) if value.present?
  end

  def timebox_name
    model_name.singular
  end

  def safe_title
    title.to_slug.normalize.to_s
  end

  def resource_parent
    raise NotImplementedError
  end

  def to_ability_name
    model_name.singular
  end

  def merge_requests_enabled?
    raise NotImplementedError
  end

  def weight_available?
    resource_parent&.feature_available?(:issue_weights)
  end

  private

  def start_date_should_be_less_than_due_date
    if due_date <= start_date
      errors.add(:due_date, _("must be greater than start date"))
    end
  end

  def dates_within_4_digits
    if start_date && start_date > Date.new(9999, 12, 31)
      errors.add(:start_date, _("date must not be after 9999-12-31"))
    end

    if due_date && due_date > Date.new(9999, 12, 31)
      errors.add(:due_date, _("date must not be after 9999-12-31"))
    end
  end

  def sanitize_title(value)
    CGI.unescape_html(Sanitize.clean(value.to_s))
  end
end