central-support 0.9.0

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