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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +14 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +49 -0
  8. data/Rakefile +6 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +8 -0
  11. data/central-support.gemspec +30 -0
  12. data/lib/central/support.rb +27 -0
  13. data/lib/central/support/concerns/activity_concern.rb +4 -0
  14. data/lib/central/support/concerns/activity_concern/associations.rb +15 -0
  15. data/lib/central/support/concerns/activity_concern/callbacks.rb +25 -0
  16. data/lib/central/support/concerns/activity_concern/scopes.rb +55 -0
  17. data/lib/central/support/concerns/activity_concern/validations.rb +17 -0
  18. data/lib/central/support/concerns/project_concern.rb +5 -0
  19. data/lib/central/support/concerns/project_concern/associations.rb +27 -0
  20. data/lib/central/support/concerns/project_concern/attributes.rb +25 -0
  21. data/lib/central/support/concerns/project_concern/csv.rb +59 -0
  22. data/lib/central/support/concerns/project_concern/scopes.rb +26 -0
  23. data/lib/central/support/concerns/project_concern/validations.rb +43 -0
  24. data/lib/central/support/concerns/story_concern.rb +7 -0
  25. data/lib/central/support/concerns/story_concern/associations.rb +23 -0
  26. data/lib/central/support/concerns/story_concern/attributes.rb +29 -0
  27. data/lib/central/support/concerns/story_concern/callbacks.rb +50 -0
  28. data/lib/central/support/concerns/story_concern/csv.rb +81 -0
  29. data/lib/central/support/concerns/story_concern/scopes.rb +16 -0
  30. data/lib/central/support/concerns/story_concern/transitions.rb +71 -0
  31. data/lib/central/support/concerns/story_concern/validations.rb +45 -0
  32. data/lib/central/support/concerns/team_concern.rb +4 -0
  33. data/lib/central/support/concerns/team_concern/associations.rb +29 -0
  34. data/lib/central/support/concerns/team_concern/domain_validator.rb +19 -0
  35. data/lib/central/support/concerns/team_concern/scopes.rb +15 -0
  36. data/lib/central/support/concerns/team_concern/validations.rb +13 -0
  37. data/lib/central/support/concerns/user_concern.rb +3 -0
  38. data/lib/central/support/concerns/user_concern/associations.rb +21 -0
  39. data/lib/central/support/concerns/user_concern/callbacks.rb +39 -0
  40. data/lib/central/support/concerns/user_concern/validations.rb +14 -0
  41. data/lib/central/support/iteration.rb +51 -0
  42. data/lib/central/support/iteration_service.rb +213 -0
  43. data/lib/central/support/mattermost.rb +36 -0
  44. data/lib/central/support/validators/belongs_to_project_validator.rb +11 -0
  45. data/lib/central/support/validators/changed_validator.rb +7 -0
  46. data/lib/central/support/validators/estimate_validator.rb +11 -0
  47. data/lib/central/support/version.rb +5 -0
  48. 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,4 @@
1
+ require 'central/support/concerns/team_concern/associations'
2
+ require 'central/support/concerns/team_concern/validations'
3
+ require 'central/support/concerns/team_concern/scopes'
4
+ require 'central/support/concerns/team_concern/domain_validator'
@@ -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