central-support 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +2 -0
- data/.travis.yml +14 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +49 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/central-support.gemspec +30 -0
- data/lib/central/support.rb +27 -0
- data/lib/central/support/concerns/activity_concern.rb +4 -0
- data/lib/central/support/concerns/activity_concern/associations.rb +15 -0
- data/lib/central/support/concerns/activity_concern/callbacks.rb +25 -0
- data/lib/central/support/concerns/activity_concern/scopes.rb +55 -0
- data/lib/central/support/concerns/activity_concern/validations.rb +17 -0
- data/lib/central/support/concerns/project_concern.rb +5 -0
- data/lib/central/support/concerns/project_concern/associations.rb +27 -0
- data/lib/central/support/concerns/project_concern/attributes.rb +25 -0
- data/lib/central/support/concerns/project_concern/csv.rb +59 -0
- data/lib/central/support/concerns/project_concern/scopes.rb +26 -0
- data/lib/central/support/concerns/project_concern/validations.rb +43 -0
- data/lib/central/support/concerns/story_concern.rb +7 -0
- data/lib/central/support/concerns/story_concern/associations.rb +23 -0
- data/lib/central/support/concerns/story_concern/attributes.rb +29 -0
- data/lib/central/support/concerns/story_concern/callbacks.rb +50 -0
- data/lib/central/support/concerns/story_concern/csv.rb +81 -0
- data/lib/central/support/concerns/story_concern/scopes.rb +16 -0
- data/lib/central/support/concerns/story_concern/transitions.rb +71 -0
- data/lib/central/support/concerns/story_concern/validations.rb +45 -0
- data/lib/central/support/concerns/team_concern.rb +4 -0
- data/lib/central/support/concerns/team_concern/associations.rb +29 -0
- data/lib/central/support/concerns/team_concern/domain_validator.rb +19 -0
- data/lib/central/support/concerns/team_concern/scopes.rb +15 -0
- data/lib/central/support/concerns/team_concern/validations.rb +13 -0
- data/lib/central/support/concerns/user_concern.rb +3 -0
- data/lib/central/support/concerns/user_concern/associations.rb +21 -0
- data/lib/central/support/concerns/user_concern/callbacks.rb +39 -0
- data/lib/central/support/concerns/user_concern/validations.rb +14 -0
- data/lib/central/support/iteration.rb +51 -0
- data/lib/central/support/iteration_service.rb +213 -0
- data/lib/central/support/mattermost.rb +36 -0
- data/lib/central/support/validators/belongs_to_project_validator.rb +11 -0
- data/lib/central/support/validators/changed_validator.rb +7 -0
- data/lib/central/support/validators/estimate_validator.rb +11 -0
- data/lib/central/support/version.rb +5 -0
- metadata +146 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
module Central
|
2
|
+
module Support
|
3
|
+
module ProjectConcern
|
4
|
+
module Scopes
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
scope :not_archived, -> { where(archived_at: nil) }
|
9
|
+
scope :archived, -> { where.not(archived_at: nil) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def archived
|
13
|
+
!!(archived_at)
|
14
|
+
end
|
15
|
+
|
16
|
+
def archived=(value)
|
17
|
+
if !value || value == "0"
|
18
|
+
self.archived_at = nil
|
19
|
+
else
|
20
|
+
self.archived_at = Time.current
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Central
|
2
|
+
module Support
|
3
|
+
module ProjectConcern
|
4
|
+
module Validations
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
# These are the valid point scales for a project. These represent
|
9
|
+
# the set of valid points estimate values for a story in this project.
|
10
|
+
POINT_SCALES = {
|
11
|
+
'fibonacci' => [1,2,3,5,8].freeze,
|
12
|
+
'powers_of_two' => [1,2,4,8].freeze,
|
13
|
+
'linear' => [1,2,3,4,5].freeze,
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
validates_inclusion_of :point_scale, in: POINT_SCALES.keys,
|
17
|
+
message: "%{value} is not a valid estimation scheme"
|
18
|
+
|
19
|
+
ITERATION_LENGTH_RANGE = (1..4).freeze
|
20
|
+
|
21
|
+
validates_numericality_of :iteration_length,
|
22
|
+
greater_than_or_equal_to: ITERATION_LENGTH_RANGE.min,
|
23
|
+
less_than_or_equal_to: ITERATION_LENGTH_RANGE.max, only_integer: true,
|
24
|
+
message: "must be between 1 and 4 weeks"
|
25
|
+
|
26
|
+
validates_numericality_of :iteration_start_day,
|
27
|
+
greater_than_or_equal_to: 0, less_than_or_equal_to: 6,
|
28
|
+
only_integer: true, message: "must be an integer between 0 and 6"
|
29
|
+
|
30
|
+
validates :name, presence: true
|
31
|
+
|
32
|
+
validates_numericality_of :default_velocity, greater_than: 0,
|
33
|
+
only_integer: true
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns an array of the valid points values for this project
|
37
|
+
def point_values
|
38
|
+
POINT_SCALES[point_scale]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
require 'central/support/concerns/story_concern/attributes'
|
2
|
+
require 'central/support/concerns/story_concern/associations'
|
3
|
+
require 'central/support/concerns/story_concern/validations'
|
4
|
+
require 'central/support/concerns/story_concern/transitions'
|
5
|
+
require 'central/support/concerns/story_concern/scopes'
|
6
|
+
require 'central/support/concerns/story_concern/callbacks'
|
7
|
+
require 'central/support/concerns/story_concern/csv'
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Central
|
2
|
+
module Support
|
3
|
+
module StoryConcern
|
4
|
+
module Associations
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
included do
|
7
|
+
belongs_to :project, counter_cache: true
|
8
|
+
belongs_to :requested_by, class_name: 'User'
|
9
|
+
belongs_to :owned_by, class_name: 'User'
|
10
|
+
|
11
|
+
has_many :users, through: :project
|
12
|
+
end
|
13
|
+
|
14
|
+
# The list of users that should be notified when a new note is added to this
|
15
|
+
# story. Includes the requestor, the owner, and any other users who have
|
16
|
+
# added notes to the story.
|
17
|
+
def stakeholders_users
|
18
|
+
([requested_by, owned_by] + notes.map(&:user)).compact.uniq
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Central
|
2
|
+
module Support
|
3
|
+
module StoryConcern
|
4
|
+
module Attributes
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
delegate :suppress_notifications, to: :project
|
9
|
+
|
10
|
+
# This attribute is used to store the user who is acting on a story, for
|
11
|
+
# example delivering or modifying it. Usually set by the controller.
|
12
|
+
attr_accessor :acting_user, :base_uri
|
13
|
+
attr_accessor :iteration_number, :iteration_start_date # helper fields for IterationService
|
14
|
+
attr_accessor :iteration_service
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
title
|
19
|
+
end
|
20
|
+
|
21
|
+
def cycle_time_in(unit = :days)
|
22
|
+
raise 'wrong unit' unless %i[days weeks months years].include?(unit)
|
23
|
+
( cycle_time / 1.send(unit) ).round
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Central
|
2
|
+
module Support
|
3
|
+
module StoryConcern
|
4
|
+
module Callbacks
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
before_validation :set_position_to_last
|
9
|
+
before_save :set_started_at
|
10
|
+
before_save :set_accepted_at
|
11
|
+
before_save :cache_user_names
|
12
|
+
before_destroy { |record| raise ActiveRecord::ReadOnlyRecord if record.readonly? }
|
13
|
+
end
|
14
|
+
|
15
|
+
def set_position_to_last
|
16
|
+
return true if position
|
17
|
+
return true unless project
|
18
|
+
last = project.stories.order(position: :desc).first
|
19
|
+
self.position = last ? ( last.position + 1 ) : 1
|
20
|
+
end
|
21
|
+
|
22
|
+
def set_started_at
|
23
|
+
return unless state_changed?
|
24
|
+
return unless state == 'started'
|
25
|
+
self.started_at = Time.current if started_at.nil?
|
26
|
+
if owned_by.nil? && acting_user
|
27
|
+
self.owned_by = acting_user
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def set_accepted_at
|
32
|
+
return unless state_changed?
|
33
|
+
return unless state == 'accepted'
|
34
|
+
self.accepted_at = Time.current if accepted_at.nil?
|
35
|
+
if started_at
|
36
|
+
self.cycle_time = accepted_at - started_at
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def cache_user_names
|
41
|
+
self.requested_by_name = requested_by.name unless requested_by.nil?
|
42
|
+
if owned_by.present?
|
43
|
+
self.owned_by_name = owned_by.name
|
44
|
+
self.owned_by_initials = owned_by.initials
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Central
|
2
|
+
module Support
|
3
|
+
module StoryConcern
|
4
|
+
module CSV
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
CSV_HEADERS = [
|
9
|
+
"Id", "Story","Labels","Iteration","Iteration Start","Iteration End",
|
10
|
+
"Story Type","Estimate","Current State","Started At", "Created at","Accepted at",
|
11
|
+
"Deadline","Requested By","Owned By","Description","URL"
|
12
|
+
]
|
13
|
+
|
14
|
+
# Returns an array, in the correct order, of the headers to be added to
|
15
|
+
# a CSV render of a list of stories
|
16
|
+
def self.csv_headers
|
17
|
+
CSV_HEADERS
|
18
|
+
end
|
19
|
+
|
20
|
+
has_many :notes, -> { order(:created_at) }, dependent: :destroy do
|
21
|
+
|
22
|
+
# Creates a collection of rows on this story from a CSV::Row instance
|
23
|
+
# Each 'Note' field in the CSV will usually be in the following format:
|
24
|
+
#
|
25
|
+
# "This is the note body text (Note Author - Dec 25, 2011)"
|
26
|
+
#
|
27
|
+
# This method will attempt to set the user and created_at timestamps
|
28
|
+
# according to the values in the parens. If the parens are missing, or
|
29
|
+
# their contents cannot be matched or parsed, user and created_at will
|
30
|
+
# not be set.
|
31
|
+
def from_csv_row(row)
|
32
|
+
# Ensure no email notifications get sent during CSV import
|
33
|
+
project = proxy_association.owner.project
|
34
|
+
project.suppress_notifications
|
35
|
+
|
36
|
+
# Each row can have muliple Note headers. Extract any of them from
|
37
|
+
# this row.
|
38
|
+
notes = []
|
39
|
+
row.each do |header, value|
|
40
|
+
if %w{Note Comment}.include?(header) && value
|
41
|
+
next if value.blank? || value =~ /^Commit by/
|
42
|
+
value.gsub!("\n", "")
|
43
|
+
next unless matches = /(.*)\((.*) - (.*)\)$/.match(value)
|
44
|
+
next if matches[1].strip.blank?
|
45
|
+
note = build(note: matches[1].strip,
|
46
|
+
user: project.users.find_by_username(matches[2]),
|
47
|
+
user_name: matches[2],
|
48
|
+
created_at: matches[3])
|
49
|
+
notes << note
|
50
|
+
end
|
51
|
+
end
|
52
|
+
notes
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_csv
|
58
|
+
[
|
59
|
+
id, # Id
|
60
|
+
title, # Story
|
61
|
+
labels, # Labels
|
62
|
+
nil, # Iteration
|
63
|
+
nil, # Iteration Start
|
64
|
+
nil, # Iteration End
|
65
|
+
story_type, # Story Type
|
66
|
+
estimate, # Estimate
|
67
|
+
state, # Current State
|
68
|
+
started_at, # Started at
|
69
|
+
created_at, # Created at
|
70
|
+
accepted_at, # Accepted at
|
71
|
+
nil, # Deadline
|
72
|
+
requested_by_name, # Requested By
|
73
|
+
owned_by_name, # Owned By
|
74
|
+
description, # Description
|
75
|
+
nil # URL
|
76
|
+
].concat(notes.map(&:to_s))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Central
|
2
|
+
module Support
|
3
|
+
module StoryConcern
|
4
|
+
module Scopes
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
scope :done, -> { where(state: :accepted) }
|
9
|
+
scope :in_progress, -> { where(state: [:started, :finished, :delivered]) }
|
10
|
+
scope :backlog, -> { where(state: :unstarted) }
|
11
|
+
scope :chilly_bin, -> { where(state: :unscheduled) }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Central
|
2
|
+
module Support
|
3
|
+
module StoryConcern
|
4
|
+
module Transitions
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
include ActiveRecord::Transitions
|
7
|
+
|
8
|
+
included do
|
9
|
+
state_machine do
|
10
|
+
state :unscheduled
|
11
|
+
state :unstarted
|
12
|
+
state :started
|
13
|
+
state :finished
|
14
|
+
state :delivered
|
15
|
+
state :accepted
|
16
|
+
state :rejected
|
17
|
+
|
18
|
+
event :start do
|
19
|
+
transitions to: :started, from: [:unstarted, :unscheduled]
|
20
|
+
end
|
21
|
+
|
22
|
+
event :finish do
|
23
|
+
transitions to: :finished, from: :started
|
24
|
+
end
|
25
|
+
|
26
|
+
event :deliver do
|
27
|
+
transitions to: :delivered, from: :finished
|
28
|
+
end
|
29
|
+
|
30
|
+
event :accept do
|
31
|
+
transitions to: :accepted, from: :delivered
|
32
|
+
end
|
33
|
+
|
34
|
+
event :reject do
|
35
|
+
transitions to: :rejected, from: :delivered
|
36
|
+
end
|
37
|
+
|
38
|
+
event :restart do
|
39
|
+
transitions to: :started, from: :rejected
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns the list of state change events that can operate on this story,
|
45
|
+
# based on its current state
|
46
|
+
def events
|
47
|
+
self.class.state_machine.events_for(current_state)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the CSS id of the column this story belongs in
|
51
|
+
def column
|
52
|
+
case state
|
53
|
+
when 'unscheduled'
|
54
|
+
'#chilly_bin'
|
55
|
+
when 'unstarted'
|
56
|
+
'#backlog'
|
57
|
+
when 'accepted'
|
58
|
+
if iteration_service
|
59
|
+
if iteration_service.current_iteration_number == iteration_service.iteration_number_for_date(accepted_at)
|
60
|
+
return '#in_progress'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
'#done'
|
64
|
+
else
|
65
|
+
'#in_progress'
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Central
|
2
|
+
module Support
|
3
|
+
module StoryConcern
|
4
|
+
module Validations
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
validates :project, presence: true
|
9
|
+
validates :title, presence: true
|
10
|
+
|
11
|
+
validates :requested_by_id, belongs_to_project: true
|
12
|
+
validates :owned_by_id, belongs_to_project: true
|
13
|
+
|
14
|
+
ESTIMABLE_TYPES = %w[feature release]
|
15
|
+
STORY_TYPES = %i[feature chore bug release].freeze
|
16
|
+
|
17
|
+
extend Enumerize
|
18
|
+
enumerize :story_type, in: STORY_TYPES, predicates: true, scope: true
|
19
|
+
validates :story_type, presence: true
|
20
|
+
validates :estimate, estimate: true, allow_nil: true
|
21
|
+
|
22
|
+
validate :bug_chore_estimation
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns true or false based on whether the story has been estimated.
|
26
|
+
def estimated?
|
27
|
+
!estimate.nil?
|
28
|
+
end
|
29
|
+
alias :estimated :estimated?
|
30
|
+
|
31
|
+
# Returns true if this story can have an estimate made against it
|
32
|
+
def estimable?
|
33
|
+
feature? && !estimated?
|
34
|
+
end
|
35
|
+
alias :estimable :estimable?
|
36
|
+
|
37
|
+
def bug_chore_estimation
|
38
|
+
if !ESTIMABLE_TYPES.include?(story_type) && estimated?
|
39
|
+
errors.add(:estimate, :cant_estimate)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Central
|
2
|
+
module Support
|
3
|
+
module TeamConcern
|
4
|
+
module Associations
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
has_many :enrollments
|
9
|
+
has_many :users, through: :enrollments
|
10
|
+
|
11
|
+
has_many :ownerships
|
12
|
+
has_many :projects, through: :ownerships do
|
13
|
+
def not_archived
|
14
|
+
where(archived_at: nil)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def is_admin?(user)
|
20
|
+
enrollments.find_by_user_id(user.id)&.is_admin?
|
21
|
+
end
|
22
|
+
|
23
|
+
def owns?(project)
|
24
|
+
ownerships.find_by_project_id(project.id)&.is_owner
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Central
|
2
|
+
module Support
|
3
|
+
module TeamConcern
|
4
|
+
module DomainValidator
|
5
|
+
DOMAIN_SEPARATORS_REGEX = /[,;\|\n]/
|
6
|
+
|
7
|
+
def allowed_domain?(email)
|
8
|
+
whitelist = ( registration_domain_whitelist || "" ).split(DOMAIN_SEPARATORS_REGEX).map(&:strip)
|
9
|
+
blacklist = ( registration_domain_blacklist || "" ).split(DOMAIN_SEPARATORS_REGEX).map(&:strip)
|
10
|
+
has_whitelist = true
|
11
|
+
has_whitelist = whitelist.any? { |domain| email.include?(domain) } unless whitelist.empty?
|
12
|
+
has_blacklist = false
|
13
|
+
has_blacklist = blacklist.any? { |domain| email.include?(domain) } unless blacklist.empty?
|
14
|
+
has_whitelist && !has_blacklist
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|