central-support 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|