Class: TrakFlow::Models::Task

Inherits:
Object
  • Object
show all
Defined in:
lib/trak_flow/models/task.rb

Overview

Represents a task in the TrakFlow system

Tasks serve multiple roles determined by flags:

  • Plan (blueprint): task.plan? == true
  • Workflow: task.source_plan_id.present? && !task.plan?
  • Step (conceptual): child Task of a Plan
  • Work item: child Task of a Workflow

Tasks can be bugs, features, tasks, epics, or chores

Constant Summary collapse

VALID_STATUSES =
TrakFlow::STATUSES
VALID_PRIORITIES =
TrakFlow::PRIORITIES
VALID_TYPES =
TrakFlow::TYPES
NILABLE_ATTRIBUTES =

Attributes that default to nil when not given.

%i[id title assignee parent_id closed_at content_hash source_plan_id].freeze
ATTRIBUTE_DEFAULTS =

Attributes with a non-nil default.

{
  description: "",
  status: "open",
  priority: 2,
  type: "task",
  plan: false,
  ephemeral: false,
  notes: ""
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(attrs = {}) ⇒ Task

Returns a new instance of Task.



37
38
39
40
41
42
43
44
# File 'lib/trak_flow/models/task.rb', line 37

def initialize(attrs = {})
  NILABLE_ATTRIBUTES.each { |key| instance_variable_set("@#{key}", attrs[key]) }
  ATTRIBUTE_DEFAULTS.each { |key, default| instance_variable_set("@#{key}", attrs[key] || default) }

  now = Time.now.utc
  @created_at = attrs[:created_at] || now
  @updated_at = attrs[:updated_at] || now
end

Instance Attribute Details

#assigneeObject

Returns the value of attribute assignee.



15
16
17
# File 'lib/trak_flow/models/task.rb', line 15

def assignee
  @assignee
end

#closed_atObject

Returns the value of attribute closed_at.



15
16
17
# File 'lib/trak_flow/models/task.rb', line 15

def closed_at
  @closed_at
end

#content_hashObject

Returns the value of attribute content_hash.



15
16
17
# File 'lib/trak_flow/models/task.rb', line 15

def content_hash
  @content_hash
end

#created_atObject

Returns the value of attribute created_at.



15
16
17
# File 'lib/trak_flow/models/task.rb', line 15

def created_at
  @created_at
end

#descriptionObject

Returns the value of attribute description.



15
16
17
# File 'lib/trak_flow/models/task.rb', line 15

def description
  @description
end

#ephemeralObject

Returns the value of attribute ephemeral.



15
16
17
# File 'lib/trak_flow/models/task.rb', line 15

def ephemeral
  @ephemeral
end

#idObject

Returns the value of attribute id.



15
16
17
# File 'lib/trak_flow/models/task.rb', line 15

def id
  @id
end

#notesObject

Returns the value of attribute notes.



15
16
17
# File 'lib/trak_flow/models/task.rb', line 15

def notes
  @notes
end

#parent_idObject

Returns the value of attribute parent_id.



15
16
17
# File 'lib/trak_flow/models/task.rb', line 15

def parent_id
  @parent_id
end

#planObject

Returns the value of attribute plan.



15
16
17
# File 'lib/trak_flow/models/task.rb', line 15

def plan
  @plan
end

#priorityObject

Returns the value of attribute priority.



15
16
17
# File 'lib/trak_flow/models/task.rb', line 15

def priority
  @priority
end

#source_plan_idObject

Returns the value of attribute source_plan_id.



15
16
17
# File 'lib/trak_flow/models/task.rb', line 15

def source_plan_id
  @source_plan_id
end

#statusObject

Returns the value of attribute status.



15
16
17
# File 'lib/trak_flow/models/task.rb', line 15

def status
  @status
end

#titleObject

Returns the value of attribute title.



15
16
17
# File 'lib/trak_flow/models/task.rb', line 15

def title
  @title
end

#typeObject

Returns the value of attribute type.



15
16
17
# File 'lib/trak_flow/models/task.rb', line 15

def type
  @type
end

#updated_atObject

Returns the value of attribute updated_at.



15
16
17
# File 'lib/trak_flow/models/task.rb', line 15

def updated_at
  @updated_at
end

Class Method Details

.from_hash(hash) ⇒ Object



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/trak_flow/models/task.rb', line 164

def from_hash(hash)
  hash = hash.transform_keys(&:to_sym)
  new(
    id: hash[:id],
    title: hash[:title],
    description: hash[:description],
    status: hash[:status],
    priority: hash[:priority]&.to_i,
    type: hash[:type],
    assignee: hash[:assignee],
    parent_id: hash[:parent_id],
    created_at: TimeParser.parse(hash[:created_at]),
    updated_at: TimeParser.parse(hash[:updated_at]),
    closed_at: TimeParser.parse(hash[:closed_at]),
    content_hash: hash[:content_hash],
    plan: hash[:plan],
    source_plan_id: hash[:source_plan_id],
    ephemeral: hash[:ephemeral],
    notes: hash[:notes]
  )
end

.from_json(json_string) ⇒ Object



186
187
188
# File 'lib/trak_flow/models/task.rb', line 186

def from_json(json_string)
  from_hash(Oj.load(json_string, mode: :compat, symbol_keys: true))
end

Instance Method Details

#append_trace(action, message) ⇒ Object



127
128
129
130
131
132
# File 'lib/trak_flow/models/task.rb', line 127

def append_trace(action, message)
  timestamp = Time.now.utc.iso8601
  entry = "[#{timestamp}] [#{action}] #{message}"
  self.notes = "#{notes}\n#{entry}".strip
  touch!
end

#blocked?Boolean

Returns:

  • (Boolean)


78
79
80
# File 'lib/trak_flow/models/task.rb', line 78

def blocked?
  status == "blocked"
end

#close!(reason: nil) ⇒ Object



108
109
110
111
112
113
# File 'lib/trak_flow/models/task.rb', line 108

def close!(reason: nil)
  self.status = "closed"
  self.closed_at = Time.now.utc
  self.notes = "#{notes}\n[Closed] #{reason}".strip if reason
  touch!
end

#closed?Boolean

Returns:

  • (Boolean)


70
71
72
# File 'lib/trak_flow/models/task.rb', line 70

def closed?
  %w[closed tombstone].include?(status)
end

#discardable?Boolean

Returns:

  • (Boolean)


104
105
106
# File 'lib/trak_flow/models/task.rb', line 104

def discardable?
  ephemeral?
end

#ephemeral?Boolean

Returns:

  • (Boolean)


96
97
98
# File 'lib/trak_flow/models/task.rb', line 96

def ephemeral?
  !!ephemeral
end

#epic?Boolean

Returns:

  • (Boolean)


82
83
84
# File 'lib/trak_flow/models/task.rb', line 82

def epic?
  type == "epic"
end

#errorsObject



50
51
52
53
54
55
56
57
58
59
60
# File 'lib/trak_flow/models/task.rb', line 50

def errors
  errs = []
  errs << "Title is required" if title.nil? || title.strip.empty?
  errs << "Invalid status: #{status}" unless VALID_STATUSES.include?(status)
  errs << "Invalid priority: #{priority}" unless VALID_PRIORITIES.include?(priority)
  errs << "Invalid type: #{type}" unless VALID_TYPES.include?(type)
  errs << "Plans cannot be ephemeral" if plan && ephemeral
  errs << "Plans cannot change status" if plan && status != "open"
  errs << "Plans cannot be derived from other Plans" if plan && source_plan_id
  errs
end

#executable?Boolean

Returns:

  • (Boolean)


100
101
102
# File 'lib/trak_flow/models/task.rb', line 100

def executable?
  !plan?
end

#in_progress?Boolean

Returns:

  • (Boolean)


74
75
76
# File 'lib/trak_flow/models/task.rb', line 74

def in_progress?
  status == "in_progress"
end

#open?Boolean

Returns:

  • (Boolean)


66
67
68
# File 'lib/trak_flow/models/task.rb', line 66

def open?
  status == "open"
end

#plan?Boolean

Plan/Workflow role predicates

Returns:

  • (Boolean)


88
89
90
# File 'lib/trak_flow/models/task.rb', line 88

def plan?
  !!plan
end

#reopen!(reason: nil) ⇒ Object



115
116
117
118
119
120
# File 'lib/trak_flow/models/task.rb', line 115

def reopen!(reason: nil)
  self.status = "open"
  self.closed_at = nil
  self.notes = "#{notes}\n[Reopened] #{reason}".strip if reason
  touch!
end

#to_hObject



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/trak_flow/models/task.rb', line 138

def to_h
  {
    id: id,
    title: title,
    description: description,
    status: status,
    priority: priority,
    type: type,
    assignee: assignee,
    parent_id: parent_id,
    created_at: created_at&.iso8601,
    updated_at: updated_at&.iso8601,
    closed_at: closed_at&.iso8601,
    content_hash: content_hash,
    plan: plan,
    source_plan_id: source_plan_id,
    ephemeral: ephemeral,
    notes: notes
  }.compact
end

#to_json(*args) ⇒ Object



159
160
161
# File 'lib/trak_flow/models/task.rb', line 159

def to_json(*args)
  Oj.dump(to_h, mode: :compat)
end

#touch!Object



122
123
124
125
# File 'lib/trak_flow/models/task.rb', line 122

def touch!
  self.updated_at = Time.now.utc
  update_content_hash!
end

#update_content_hash!Object



134
135
136
# File 'lib/trak_flow/models/task.rb', line 134

def update_content_hash!
  self.content_hash = IdGenerator.content_hash(to_h.except(:content_hash, :updated_at))
end

#valid?Boolean

Returns:

  • (Boolean)


46
47
48
# File 'lib/trak_flow/models/task.rb', line 46

def valid?
  errors.empty?
end

#validate!Object

Raises:



62
63
64
# File 'lib/trak_flow/models/task.rb', line 62

def validate!
  raise ValidationError, errors.join(", ") unless valid?
end

#workflow?Boolean

Returns:

  • (Boolean)


92
93
94
# File 'lib/trak_flow/models/task.rb', line 92

def workflow?
  source_plan_id && !source_plan_id.to_s.empty? && !plan?
end