artisan-core 0.0.1

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 (63) hide show
  1. data/lib/artisan/activity/activity_interactor.rb +18 -0
  2. data/lib/artisan/activity/activity_presenter.rb +35 -0
  3. data/lib/artisan/activity/formatters/diff_changes.rb +41 -0
  4. data/lib/artisan/activity/formatters/name.rb +22 -0
  5. data/lib/artisan/activity/formatters/source.rb +16 -0
  6. data/lib/artisan/activity/formatters/temporal.rb +20 -0
  7. data/lib/artisan/activity/iteration_auditor.rb +44 -0
  8. data/lib/artisan/activity/project_auditor.rb +28 -0
  9. data/lib/artisan/activity/story_auditor.rb +42 -0
  10. data/lib/artisan/callbacks.rb +27 -0
  11. data/lib/artisan/crud_strategy.rb +23 -0
  12. data/lib/artisan/event_mailer.rb +22 -0
  13. data/lib/artisan/inviter.rb +36 -0
  14. data/lib/artisan/iterations/iteration_point_calculator.rb +28 -0
  15. data/lib/artisan/iterations/iteration_presenter.rb +20 -0
  16. data/lib/artisan/iterations/iteration_workflow_interactor.rb +60 -0
  17. data/lib/artisan/iterations/iteration_workflow_presenter.rb +11 -0
  18. data/lib/artisan/iterations/iterations.rb +47 -0
  19. data/lib/artisan/iterations/iterations_interactor.rb +48 -0
  20. data/lib/artisan/iterations/move_to_backlog.rb +28 -0
  21. data/lib/artisan/iterations/story_tags.rb +15 -0
  22. data/lib/artisan/member.rb +52 -0
  23. data/lib/artisan/no_op_callbacks.rb +6 -0
  24. data/lib/artisan/projects/api_key_generator.rb +26 -0
  25. data/lib/artisan/projects/archive_interactor.rb +29 -0
  26. data/lib/artisan/projects/completed_stories_presenter.rb +20 -0
  27. data/lib/artisan/projects/iteration_numberer.rb +15 -0
  28. data/lib/artisan/projects/project_creator.rb +51 -0
  29. data/lib/artisan/projects/projects_interactor.rb +58 -0
  30. data/lib/artisan/projects/projects_presenter.rb +46 -0
  31. data/lib/artisan/projects/story_point_summer.rb +16 -0
  32. data/lib/artisan/reports/average_stat_per_iteration_data.rb +55 -0
  33. data/lib/artisan/reports/burn_up_chart.rb +46 -0
  34. data/lib/artisan/reports/high_charts_data_retriever.rb +77 -0
  35. data/lib/artisan/reports/high_charts_interactor.rb +31 -0
  36. data/lib/artisan/reports/percentage_of_commitments_met_data.rb +47 -0
  37. data/lib/artisan/reports/signoff/pdf.rb +161 -0
  38. data/lib/artisan/reports/signoff/report.rb +120 -0
  39. data/lib/artisan/reports/velocity_report.rb +43 -0
  40. data/lib/artisan/repository.rb +51 -0
  41. data/lib/artisan/stories/pert_calculator.rb +62 -0
  42. data/lib/artisan/stories/stories_interactor.rb +78 -0
  43. data/lib/artisan/stories/story_collection.rb +66 -0
  44. data/lib/artisan/stories/story_exporter.rb +45 -0
  45. data/lib/artisan/stories/story_numberer.rb +15 -0
  46. data/lib/artisan/stories/story_presenter.rb +23 -0
  47. data/lib/artisan/stories/story_sorter.rb +37 -0
  48. data/lib/artisan/story_board.rb +45 -0
  49. data/lib/artisan/story_column_changer.rb +71 -0
  50. data/lib/artisan/story_exporter.rb +14 -0
  51. data/lib/artisan/team.rb +50 -0
  52. data/spec/crud_strategy_spec.rb +100 -0
  53. data/spec/event_mailer_spec.rb +86 -0
  54. data/spec/inviter_spec.rb +58 -0
  55. data/spec/member_spec.rb +58 -0
  56. data/spec/repository_spec.rb +32 -0
  57. data/spec/story_board_spec.rb +36 -0
  58. data/spec/story_collection_spec.rb +61 -0
  59. data/spec/story_column_changer_spec.rb +222 -0
  60. data/spec/story_exporter_spec.rb +25 -0
  61. data/spec/story_sorter_spec.rb +67 -0
  62. data/spec/team_spec.rb +107 -0
  63. metadata +128 -0
@@ -0,0 +1,120 @@
1
+ require "forwardable"
2
+ require "artisan/stories/story_presenter"
3
+ require "artisan/iterations/iteration_presenter"
4
+ require "artisan/iterations/story_tags"
5
+
6
+ module Signoff
7
+ class Report
8
+ extend Forwardable
9
+
10
+ attr_reader :project_name
11
+ attr_reader :table_data
12
+
13
+ def initialize(iteration, project_name, options = {})
14
+ @iteration = iteration
15
+ @project_name = project_name
16
+ @options = options
17
+ @table_data = {}
18
+ generate_reporting_variables
19
+ end
20
+
21
+ def_delegators :@iteration, :start_date, :finish_date, :committed_points, :committed_points_at_completion
22
+
23
+ def iteration_name
24
+ return "Iteration #{@iteration.number}"
25
+ end
26
+
27
+ def tables
28
+ tables = []
29
+ @options["sections"].each do |key, value|
30
+ tables << key if value == "1"
31
+ end
32
+ return tables
33
+ end
34
+
35
+ def total_billed_points
36
+ Artisan::Iterations::IterationPresenter.new(@iteration).total_billed_points
37
+ end
38
+
39
+ def team_member_names
40
+ Artisan::Iterations::IterationPresenter.new(@iteration).team_names
41
+ end
42
+
43
+ def completed
44
+ report_on(@iteration.stories.select { |story| story.complete? })
45
+ end
46
+
47
+ def features
48
+ report_on(@iteration.stories.select { |story| !story.nonbillable? })
49
+ end
50
+
51
+ def tasks
52
+ report_on(@iteration.stories.select { |story| story.nonbillable? })
53
+ end
54
+
55
+ def tag attr
56
+ report_on(@iteration.stories.select { |story| story.tag_list.include?(attr) })
57
+ end
58
+
59
+ def untagged
60
+ report_on(@iteration.stories.select { |story| story.tag_list.empty? })
61
+ end
62
+
63
+ def column_widths
64
+ widths = [60, 280]
65
+ widths << 100 if show_owner?
66
+ return widths
67
+ end
68
+
69
+ def column_titles
70
+ titles = ["Points", "Description"]
71
+ titles << "Owner" if show_owner?
72
+ return titles
73
+ end
74
+
75
+ private
76
+
77
+ def report_on(stories)
78
+ data = []
79
+ total_points = 0
80
+ stories.each do |story|
81
+ presenter = Artisan::Stories::StoryPresenter.new(story)
82
+ row = [story.estimate.to_s, story.name]
83
+ row << presenter.assigned_user_name if show_owner?
84
+
85
+ data << row
86
+ total_points += story.estimate
87
+ end
88
+
89
+ total_points = total_points.round(2)
90
+ final_row = [total_points.to_s, ""]
91
+ final_row << "" if show_owner?
92
+ data << final_row
93
+ end
94
+
95
+ def show_owner?
96
+ if !@options["show_owner"].nil?
97
+ return @options["show_owner"] == "1"
98
+ end
99
+ return false
100
+ end
101
+
102
+ def generate_reporting_variables
103
+ if !@options["sections"].nil?
104
+ @table_data['completed'] = self.completed if @options["sections"]["Completed"] == "1"
105
+
106
+ @table_data['features'] = self.features if @options["sections"]["Features"] == "1"
107
+
108
+ @table_data['tasks'] = self.tasks if @options["sections"]["Tasks"] == "1"
109
+
110
+ @table_data['untagged'] = self.untagged if @options["sections"]["Untagged"] == "1"
111
+ end
112
+
113
+ Artisan::Iterations::StoryTags.new(@iteration).tags.each { |tag| make_tag_callback(tag) }
114
+ end
115
+
116
+ def make_tag_callback(attr)
117
+ @table_data[attr.to_s.downcase] = self.tag(attr)
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,43 @@
1
+ require "artisan/iterations/iteration_point_calculator"
2
+ module Reports
3
+ class VelocityReport
4
+ attr_accessor :results, :project, :limit
5
+
6
+ def initialize(project)
7
+ @project = project
8
+ end
9
+
10
+ def generate(criteria = {})
11
+ @limit = !criteria[:limit].nil? ? criteria[:limit] : @project.iterations.count
12
+ @results = limited_iterations_by_finish_date
13
+ end
14
+
15
+ def collect_iteration(action)
16
+ return @results.collect {|iteration| iteration.send(action.to_sym) }.reverse
17
+ end
18
+
19
+ def max_value
20
+ list = array_to_f(collect_iteration(:committed_points)).concat(completed_points)
21
+ return list.sort[-1]
22
+ end
23
+
24
+ def collect_dates
25
+ return @results.collect{|iteration| iteration.finish_date.strftime('%b %d')}.reverse
26
+ end
27
+
28
+ def completed_points
29
+ return @results.collect {|iteration| Artisan::Iterations::IterationPointCalculator.new(iteration).completed_points }.reverse
30
+ end
31
+
32
+ private
33
+
34
+ def array_to_f(array)
35
+ return array.map(&:to_f)
36
+ end
37
+
38
+ def limited_iterations_by_finish_date
39
+ @project.iterations.by_finish_date[0, @limit.to_i]
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,51 @@
1
+ module Artisan
2
+ class RecordNotFound < RuntimeError
3
+ attr_reader :base
4
+
5
+ def initialize(base = nil)
6
+ @base = base
7
+ end
8
+ end
9
+
10
+ class Repository
11
+ def self.register_repo(repo)
12
+ @repo = repo
13
+ end
14
+
15
+ def self.story
16
+ repo.story
17
+ end
18
+
19
+ def self.project
20
+ repo.project
21
+ end
22
+
23
+ def self.iteration
24
+ repo.iteration
25
+ end
26
+
27
+ def self.user
28
+ repo.user
29
+ end
30
+
31
+ def self.future_user
32
+ repo.future_user
33
+ end
34
+
35
+ def self.project_configuration
36
+ repo.project_configuration
37
+ end
38
+
39
+ def self.change
40
+ repo.change
41
+ end
42
+
43
+ def self.member
44
+ repo.member
45
+ end
46
+
47
+ def self.repo
48
+ @repo
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,62 @@
1
+ class Float
2
+ def round_to(x)
3
+ (self/x).round * x
4
+ end
5
+ end
6
+
7
+ module Artisan
8
+ module Stories
9
+ class PertCalculator
10
+ attr_reader :story, :estimate_mode
11
+
12
+ def initialize(story, estimate_mode = "stddev")
13
+ @story = story
14
+ @estimate_mode = estimate_mode
15
+ end
16
+
17
+ def weighted_mean
18
+ unrounded_weighted_mean.round_to(0.25)
19
+ end
20
+
21
+ def standard_deviation
22
+ unrounded_standard_deviation.round(2)
23
+ end
24
+
25
+ def variance
26
+ unrounded_variance.round(2)
27
+ end
28
+
29
+ def estimate
30
+ mean = unrounded_weighted_mean
31
+ mean += unrounded_standard_deviation * 2 if @estimate_mode.to_s != "mean"
32
+ return mean.round_to(0.25)
33
+ end
34
+
35
+ private
36
+
37
+ def unrounded_weighted_mean
38
+ (optimistic.to_f + 4 * realistic.to_f + pessimistic.to_f) / 6
39
+ end
40
+
41
+ def unrounded_standard_deviation
42
+ (pessimistic.to_f - optimistic.to_f)/6
43
+ end
44
+
45
+ def unrounded_variance
46
+ unrounded_standard_deviation ** 2
47
+ end
48
+
49
+ def optimistic
50
+ @story.optimistic
51
+ end
52
+
53
+ def realistic
54
+ @story.realistic
55
+ end
56
+
57
+ def pessimistic
58
+ @story.pessimistic
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,78 @@
1
+ require "artisan/crud_strategy"
2
+ require 'artisan/activity/story_auditor'
3
+ require "artisan/stories/story_numberer"
4
+
5
+ module Artisan
6
+ module Stories
7
+
8
+ def self.for(project_id, user = nil)
9
+ StoriesInteractor.new(project_id, user)
10
+ end
11
+
12
+ class StoriesInteractor
13
+ def initialize(project_id, user = nil)
14
+ @project = project_repository.find(project_id)
15
+ @user = user
16
+ end
17
+
18
+ def create(story_attributes, callbacks)
19
+ model = repository.create_for_project(@project, story_attributes)
20
+ set_story_number_for model
21
+ story = CrudStrategy.create(model, callbacks, repository)
22
+ Activity::StoryAuditor.created(story.id, @project.id, @user.id, story_attributes)
23
+ return story
24
+ end
25
+
26
+ def update(number, story_attributes, callbacks)
27
+ story = find(number)
28
+ original_attributes = story.attributes
29
+ story = CrudStrategy.update(story, story_attributes, @user, callbacks)
30
+
31
+ Activity::StoryAuditor.updated(story.id, @project.id, @user.id, original_attributes, story_attributes)
32
+ end
33
+
34
+ def delete(number)
35
+ story = find(number)
36
+ repository.delete(story)
37
+ Activity::StoryAuditor.deleted(story.id, @project.id, @user.id)
38
+ end
39
+
40
+ def backlog(filter_tag = nil)
41
+ backlog_stories = filtered(filter_tag, @project.stories.backlog)
42
+ return StoryCollection.new(backlog_stories)
43
+ end
44
+
45
+ def completed
46
+ @project.stories.complete
47
+ end
48
+
49
+ def find_by_iteration(iteration_number)
50
+ iteration = @project.iterations.find_by_number iteration_number
51
+ iteration.stories(:include => 'assigned_user')
52
+ end
53
+
54
+ def find(number)
55
+ repository.find(number, @project)
56
+ end
57
+
58
+ private
59
+
60
+ def filtered(filter_tag, stories)
61
+ return stories if filter_tag.nil?
62
+ return stories.tagged_with(filter_tag, :on => :tags)
63
+ end
64
+
65
+ def set_story_number_for story
66
+ story.update_attribute(:number, StoryNumberer.new(@project).next_story_number)
67
+ end
68
+
69
+ def repository
70
+ Repository.story
71
+ end
72
+
73
+ def project_repository
74
+ Repository.project
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,66 @@
1
+ module Artisan
2
+ module Stories
3
+ class StoryCollection
4
+ include Enumerable
5
+
6
+ attr_reader :stories
7
+ alias :all :stories
8
+
9
+ def initialize(stories = [])
10
+ @stories = stories
11
+ end
12
+
13
+ def billable
14
+ select {|story| !story.nonbillable?}
15
+ end
16
+
17
+ def billed
18
+ select{|s| !s.nonbillable? && s.complete?}
19
+ end
20
+
21
+ def completed
22
+ select(&:complete?)
23
+ end
24
+
25
+ def incomplete
26
+ reject(&:complete?)
27
+ end
28
+
29
+ def billable_points
30
+ sum_estimates(billable)
31
+ end
32
+
33
+ def billed_points
34
+ sum_estimates(billed)
35
+ end
36
+
37
+ def estimated_points
38
+ sum_estimates(@stories)
39
+ end
40
+
41
+ alias :points :estimated_points
42
+ alias :total_points :estimated_points
43
+
44
+ def completed_points
45
+ sum_estimates(completed)
46
+ end
47
+
48
+ def all_complete?
49
+ all?(&:complete?)
50
+ end
51
+
52
+ def each(&block)
53
+ stories.each { |story| block.call(story) }
54
+ end
55
+
56
+ private
57
+
58
+ def sum_estimates(collection)
59
+ collection.inject(0.0) do |sum, story|
60
+ sum += story.estimate if story.estimate
61
+ sum
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,45 @@
1
+ module Artisan
2
+ module Stories
3
+ class StoryExporter
4
+
5
+ def initialize(story)
6
+ @story = story
7
+ end
8
+
9
+ def as_json(story_json = {}, options={})
10
+ options = {} unless options
11
+ options = neglect_created_at(options)
12
+
13
+ story_json = story_json
14
+
15
+ if @story.assigned_user
16
+ story_json["assigned_user"] = {
17
+ "full_name" => @story.assigned_user.full_name,
18
+ "gravatar_url" => @story.assigned_user.gravatar_url
19
+ }
20
+ end
21
+
22
+ story_json["assigned_user_options"] = @story.project.users.inject({}) do |collection, user|
23
+ collection[user.full_name] = user.id
24
+ collection
25
+ end if @story.project
26
+
27
+ story_json["created_at"] = @story.created_at.strftime("%m/%d/%Y") if !@story.created_at.nil?
28
+ story_json["creator"] = @story.creator.full_name if @story.creator
29
+ story_json["tag_list"] = @story.tag_list.join(",")
30
+ story_json
31
+ end
32
+
33
+ private
34
+
35
+ def neglect_created_at(options)
36
+ if options[:except]
37
+ options[:except] += [:created_at]
38
+ else
39
+ options[:except] = [:created_at]
40
+ end
41
+ options
42
+ end
43
+ end
44
+ end
45
+ end