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.
- data/lib/artisan/activity/activity_interactor.rb +18 -0
- data/lib/artisan/activity/activity_presenter.rb +35 -0
- data/lib/artisan/activity/formatters/diff_changes.rb +41 -0
- data/lib/artisan/activity/formatters/name.rb +22 -0
- data/lib/artisan/activity/formatters/source.rb +16 -0
- data/lib/artisan/activity/formatters/temporal.rb +20 -0
- data/lib/artisan/activity/iteration_auditor.rb +44 -0
- data/lib/artisan/activity/project_auditor.rb +28 -0
- data/lib/artisan/activity/story_auditor.rb +42 -0
- data/lib/artisan/callbacks.rb +27 -0
- data/lib/artisan/crud_strategy.rb +23 -0
- data/lib/artisan/event_mailer.rb +22 -0
- data/lib/artisan/inviter.rb +36 -0
- data/lib/artisan/iterations/iteration_point_calculator.rb +28 -0
- data/lib/artisan/iterations/iteration_presenter.rb +20 -0
- data/lib/artisan/iterations/iteration_workflow_interactor.rb +60 -0
- data/lib/artisan/iterations/iteration_workflow_presenter.rb +11 -0
- data/lib/artisan/iterations/iterations.rb +47 -0
- data/lib/artisan/iterations/iterations_interactor.rb +48 -0
- data/lib/artisan/iterations/move_to_backlog.rb +28 -0
- data/lib/artisan/iterations/story_tags.rb +15 -0
- data/lib/artisan/member.rb +52 -0
- data/lib/artisan/no_op_callbacks.rb +6 -0
- data/lib/artisan/projects/api_key_generator.rb +26 -0
- data/lib/artisan/projects/archive_interactor.rb +29 -0
- data/lib/artisan/projects/completed_stories_presenter.rb +20 -0
- data/lib/artisan/projects/iteration_numberer.rb +15 -0
- data/lib/artisan/projects/project_creator.rb +51 -0
- data/lib/artisan/projects/projects_interactor.rb +58 -0
- data/lib/artisan/projects/projects_presenter.rb +46 -0
- data/lib/artisan/projects/story_point_summer.rb +16 -0
- data/lib/artisan/reports/average_stat_per_iteration_data.rb +55 -0
- data/lib/artisan/reports/burn_up_chart.rb +46 -0
- data/lib/artisan/reports/high_charts_data_retriever.rb +77 -0
- data/lib/artisan/reports/high_charts_interactor.rb +31 -0
- data/lib/artisan/reports/percentage_of_commitments_met_data.rb +47 -0
- data/lib/artisan/reports/signoff/pdf.rb +161 -0
- data/lib/artisan/reports/signoff/report.rb +120 -0
- data/lib/artisan/reports/velocity_report.rb +43 -0
- data/lib/artisan/repository.rb +51 -0
- data/lib/artisan/stories/pert_calculator.rb +62 -0
- data/lib/artisan/stories/stories_interactor.rb +78 -0
- data/lib/artisan/stories/story_collection.rb +66 -0
- data/lib/artisan/stories/story_exporter.rb +45 -0
- data/lib/artisan/stories/story_numberer.rb +15 -0
- data/lib/artisan/stories/story_presenter.rb +23 -0
- data/lib/artisan/stories/story_sorter.rb +37 -0
- data/lib/artisan/story_board.rb +45 -0
- data/lib/artisan/story_column_changer.rb +71 -0
- data/lib/artisan/story_exporter.rb +14 -0
- data/lib/artisan/team.rb +50 -0
- data/spec/crud_strategy_spec.rb +100 -0
- data/spec/event_mailer_spec.rb +86 -0
- data/spec/inviter_spec.rb +58 -0
- data/spec/member_spec.rb +58 -0
- data/spec/repository_spec.rb +32 -0
- data/spec/story_board_spec.rb +36 -0
- data/spec/story_collection_spec.rb +61 -0
- data/spec/story_column_changer_spec.rb +222 -0
- data/spec/story_exporter_spec.rb +25 -0
- data/spec/story_sorter_spec.rb +67 -0
- data/spec/team_spec.rb +107 -0
- metadata +128 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
module Artisan
|
2
|
+
module Stories
|
3
|
+
class StoryNumberer
|
4
|
+
def initialize(project)
|
5
|
+
@project = project
|
6
|
+
end
|
7
|
+
|
8
|
+
def next_story_number
|
9
|
+
max = 0
|
10
|
+
Story.by_project(@project).each {|story| max = story.number if story.number && story.number > max}
|
11
|
+
return max + 1
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Artisan
|
2
|
+
module Stories
|
3
|
+
class StoryPresenter
|
4
|
+
attr_accessor :story
|
5
|
+
|
6
|
+
def initialize(story)
|
7
|
+
@story= story
|
8
|
+
end
|
9
|
+
|
10
|
+
def short_name
|
11
|
+
@story.name.length <= 30 ? @story.name : (@story.name[0..26] + "...")
|
12
|
+
end
|
13
|
+
|
14
|
+
def assigned_user_name
|
15
|
+
!@story.assigned_user.nil? ? @story.assigned_user.full_name : "Not Assigned"
|
16
|
+
end
|
17
|
+
|
18
|
+
def tags
|
19
|
+
@story.tags.map(&:name)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "artisan/stories/stories_interactor"
|
2
|
+
|
3
|
+
module Artisan
|
4
|
+
module Stories
|
5
|
+
class StorySorter
|
6
|
+
|
7
|
+
def initialize(project, state, story_ids, iteration)
|
8
|
+
@state = state
|
9
|
+
@story_ids = story_ids
|
10
|
+
@iteration = iteration
|
11
|
+
@project = project
|
12
|
+
end
|
13
|
+
|
14
|
+
def sort
|
15
|
+
get_stories.each do |story|
|
16
|
+
story.position = story_index(story) if has_story_id?(story)
|
17
|
+
Artisan::Repository.story.save(story)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def has_story_id?(story)
|
24
|
+
return @story_ids.index(story.id.to_s).present?
|
25
|
+
end
|
26
|
+
|
27
|
+
def story_index(story)
|
28
|
+
return @story_ids.index(story.id.to_s) + 1
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_stories
|
32
|
+
return Stories.for(@project.id, nil).backlog if @state == "backlog"
|
33
|
+
return @iteration.stories.send(@state)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'artisan/repository'
|
2
|
+
|
3
|
+
module Artisan
|
4
|
+
class StoryBoard
|
5
|
+
attr_reader :iteration
|
6
|
+
|
7
|
+
def initialize(iteration)
|
8
|
+
@iteration = iteration
|
9
|
+
end
|
10
|
+
|
11
|
+
def ready
|
12
|
+
repository.ready(iteration)
|
13
|
+
end
|
14
|
+
|
15
|
+
def working
|
16
|
+
repository.working(iteration)
|
17
|
+
end
|
18
|
+
|
19
|
+
def completed
|
20
|
+
repository.completed(iteration)
|
21
|
+
end
|
22
|
+
|
23
|
+
def backlog_total
|
24
|
+
Artisan::Stories::StoriesInteractor.new(iteration.project).backlog.points
|
25
|
+
end
|
26
|
+
|
27
|
+
def ready_total
|
28
|
+
ready.inject(0) { |sum, story| sum + story.estimate }
|
29
|
+
end
|
30
|
+
|
31
|
+
def working_total
|
32
|
+
working.inject(0) { |sum, story| sum + story.estimate }
|
33
|
+
end
|
34
|
+
|
35
|
+
def complete_total
|
36
|
+
completed.inject(0) { |sum, story| sum + story.estimate }
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def repository
|
42
|
+
Artisan::Repository.story
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require "artisan/activity/story_auditor"
|
2
|
+
require "artisan/repository"
|
3
|
+
|
4
|
+
module Artisan
|
5
|
+
class StoryColumnChanger
|
6
|
+
|
7
|
+
def initialize(story, user)
|
8
|
+
@story = story
|
9
|
+
@user = user
|
10
|
+
end
|
11
|
+
|
12
|
+
def move_to_backlog
|
13
|
+
clear_completed
|
14
|
+
clear_working
|
15
|
+
|
16
|
+
story_repository.remove_from_iteration(@story)
|
17
|
+
|
18
|
+
story_repository.save(@story)
|
19
|
+
Artisan::Activity::StoryAuditor::backlogged(@story.id, @story.project.id, @user.id)
|
20
|
+
end
|
21
|
+
|
22
|
+
def move_to_ready(iteration)
|
23
|
+
clear_completed
|
24
|
+
clear_working
|
25
|
+
|
26
|
+
story_repository.add_to_iteration(iteration, @story)
|
27
|
+
|
28
|
+
story_repository.save(@story)
|
29
|
+
Artisan::Activity::StoryAuditor::readied(@story.id, @story.project.id, @user.id)
|
30
|
+
end
|
31
|
+
|
32
|
+
def move_to_working(user, iteration)
|
33
|
+
clear_completed
|
34
|
+
|
35
|
+
story_repository.add_to_iteration(iteration, @story)
|
36
|
+
@story.assigned_user ||= user
|
37
|
+
|
38
|
+
story_repository.save(@story)
|
39
|
+
Artisan::Activity::StoryAuditor::worked(@story.id, @story.project.id, @user.id)
|
40
|
+
end
|
41
|
+
|
42
|
+
def move_to_completed(user, iteration, options = {})
|
43
|
+
story_repository.add_to_iteration(iteration, @story)
|
44
|
+
@story.assigned_user ||= user
|
45
|
+
|
46
|
+
@story.complete = true
|
47
|
+
@story.completed_at = DateTime.now
|
48
|
+
|
49
|
+
story_repository.save(@story)
|
50
|
+
|
51
|
+
options[:event_mailer].completed_story(@story) if !options[:event_mailer].nil?
|
52
|
+
Artisan::Activity::StoryAuditor::completed(@story.id, @story.project.id, @user.id)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
|
58
|
+
def clear_completed
|
59
|
+
@story.complete = false
|
60
|
+
@story.completed_at = nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def clear_working
|
64
|
+
@story.assigned_user = nil
|
65
|
+
end
|
66
|
+
|
67
|
+
def story_repository
|
68
|
+
Artisan::Repository.story
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class StoryExporter
|
2
|
+
|
3
|
+
UNNECESSARY_FIELDS = [:complete, :completed_at, :creator_id, :deleted, :foreign_id, :iteration_id, :split_from, :updated_at]
|
4
|
+
|
5
|
+
def initialize(story)
|
6
|
+
@story = story
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_json
|
10
|
+
return "{}" if @story.nil?
|
11
|
+
@story.to_json(:except => UNNECESSARY_FIELDS, :include => {:assigned_user => {:only => :full_name, :methods => [:gravatar_url]}})
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
data/lib/artisan/team.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'artisan/repository'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
class Team
|
5
|
+
|
6
|
+
attr_reader :project
|
7
|
+
|
8
|
+
def initialize(project, current_user)
|
9
|
+
@project = project
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_team_member(user)
|
13
|
+
add_member(user, false)
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_team_owner(user)
|
17
|
+
add_member(user, true)
|
18
|
+
end
|
19
|
+
|
20
|
+
def owner?(user)
|
21
|
+
return false if project.nil? || !user?(user)
|
22
|
+
project_repo.is_owner?(project, user)
|
23
|
+
end
|
24
|
+
|
25
|
+
def user?(user)
|
26
|
+
project_repo.is_member?(project, user)
|
27
|
+
end
|
28
|
+
|
29
|
+
def remove_user(user)
|
30
|
+
project_repo.remove_member(project, user)
|
31
|
+
if !user.nil?
|
32
|
+
Artisan::Member.new(OpenStruct.new(:user => user, :project => project)).remove_from_email_lists
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def owners
|
37
|
+
project_repo.owners(project)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def add_member(user, owner)
|
43
|
+
project_repo.add_member(project, user, owner) unless user?(user)
|
44
|
+
end
|
45
|
+
|
46
|
+
def project_repo
|
47
|
+
Artisan::Repository.project
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require "artisan/crud_strategy"
|
2
|
+
require "artisan-memory-repository/models/user"
|
3
|
+
require "artisan/callbacks"
|
4
|
+
|
5
|
+
class TestModel
|
6
|
+
attr_accessor :id, :attributes
|
7
|
+
def initialize
|
8
|
+
@id = 3
|
9
|
+
@attributes = {}
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class TestRepository
|
14
|
+
def save(model)
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
18
|
+
def new(*args)
|
19
|
+
TestModel.new
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe Artisan::CrudStrategy do
|
24
|
+
|
25
|
+
let(:model) { TestModel.new }
|
26
|
+
let(:model_attributes) { {:test => "test"}}
|
27
|
+
let(:user) { Artisan::Repository.user.new }
|
28
|
+
|
29
|
+
let(:callback) do
|
30
|
+
Artisan::Callbacks.build do |callback|
|
31
|
+
callback.on(:success) { @success_callback_triggered = true }
|
32
|
+
callback.on(:failure) { @failure_callback_triggered = true }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
let(:interactor) { Artisan::CrudStrategy }
|
37
|
+
let(:repository) { mock('repository') }
|
38
|
+
|
39
|
+
before do
|
40
|
+
TestModel.stub!(:find).and_return(model)
|
41
|
+
end
|
42
|
+
|
43
|
+
context "Create" do
|
44
|
+
|
45
|
+
before do
|
46
|
+
repository.stub!(:new).and_return(model)
|
47
|
+
repository.stub!(:save).and_return(true)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "creates a new model" do
|
51
|
+
new_model = interactor.create(model, callback, repository)
|
52
|
+
new_model.should == model
|
53
|
+
end
|
54
|
+
|
55
|
+
it "calls successful callbacks" do
|
56
|
+
interactor.create(model_attributes, callback, repository)
|
57
|
+
|
58
|
+
@success_callback_triggered.should be_true
|
59
|
+
end
|
60
|
+
|
61
|
+
it "calls failure callbacks on failed save" do
|
62
|
+
failure_callback_triggered = false
|
63
|
+
failure_callback = Proc.new { |model| failure_callback_triggered = true }
|
64
|
+
repository.should_receive(:save).and_return(false)
|
65
|
+
interactor.create(model_attributes, callback, repository)
|
66
|
+
|
67
|
+
@failure_callback_triggered.should be_true
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
context "Update" do
|
74
|
+
|
75
|
+
before do
|
76
|
+
model.stub(:update_attributes).and_return(true)
|
77
|
+
end
|
78
|
+
|
79
|
+
it "finds and updates the project" do
|
80
|
+
model.should_receive(:update_attributes).with(model_attributes).and_return(true)
|
81
|
+
|
82
|
+
interactor.update(model, model_attributes, user, callback).should == model
|
83
|
+
end
|
84
|
+
|
85
|
+
it "completes a successful callback" do
|
86
|
+
interactor.update(model, model_attributes, user, callback)
|
87
|
+
@success_callback_triggered.should be_true
|
88
|
+
end
|
89
|
+
|
90
|
+
it "completes a failure callback" do
|
91
|
+
model.stub(:update_attributes).and_return(false)
|
92
|
+
failure_callback_triggered = false
|
93
|
+
failure_callback = Proc.new { failure_callback_triggered = true }
|
94
|
+
interactor.update(model, model_attributes, user, callback)
|
95
|
+
@failure_callback_triggered.should be_true
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require "artisan/event_mailer"
|
2
|
+
require "artisan-memory-repository/models/project"
|
3
|
+
require "artisan-memory-repository/models/story"
|
4
|
+
require "artisan-memory-repository/models/user"
|
5
|
+
|
6
|
+
class Notifier; end # stubbed out for tests
|
7
|
+
|
8
|
+
describe EventMailer do
|
9
|
+
|
10
|
+
describe "mailing" do
|
11
|
+
|
12
|
+
before(:each) do
|
13
|
+
@project = Artisan::Repository.project.new(:name => "Test")
|
14
|
+
@project_configuration = double(:story_completed_email_list => ["hello@world.com", "dude@test.com"], :story_assigned_email_list => ["uhh@derp.com", "dude@test.com"])
|
15
|
+
@project.project_configuration = @project_configuration
|
16
|
+
@event_mailer = EventMailer.new(@project)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "takes a project and sets a configuration" do
|
20
|
+
@event_mailer.instance_variable_get(:@configuration).should == @project_configuration
|
21
|
+
@event_mailer.instance_variable_get(:@project_name).should == @project.name
|
22
|
+
end
|
23
|
+
|
24
|
+
it "alerts the notifier for a new assigned user to a story" do
|
25
|
+
user = Artisan::Repository.user.new(:full_name => "Mister McGoo")
|
26
|
+
story = Artisan::Repository.story.new(:assigned_user => user, :name => "Test")
|
27
|
+
|
28
|
+
email_list = "uhh@derp.com, dude@test.com"
|
29
|
+
Notifier.should_receive(:new_assigned_user).with(@project.name, user.full_name, story.name, email_list).and_return(double(:deliver => nil))
|
30
|
+
@event_mailer.new_assigned_user(story)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "alerts the notifier for a completed story on a project" do
|
34
|
+
story = Artisan::Repository.story.new(:name => "Test")
|
35
|
+
|
36
|
+
email_list = "hello@world.com, dude@test.com"
|
37
|
+
Notifier.should_receive(:completed_story).with(@project.name, story.name, email_list).and_return(double(:deliver => nil))
|
38
|
+
@event_mailer.completed_story(story)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
context "archived projects" do
|
44
|
+
before(:each) do
|
45
|
+
@project = Artisan::Repository.project.new(:archived => true)
|
46
|
+
@event_mailer = EventMailer.new(@project)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "does not alert the notifier of completed story if the project is archived" do
|
50
|
+
Notifier.should_not_receive(:completed_story)
|
51
|
+
@event_mailer.completed_story(nil)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "does not alert the notifier of a new assigned user if the project is archived" do
|
55
|
+
Notifier.should_not_receive(:new_assigned_user)
|
56
|
+
@event_mailer.new_assigned_user(nil)
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
describe "empty email lists" do
|
62
|
+
before(:each) do
|
63
|
+
@project = Artisan::Repository.project.new(:name => "Test")
|
64
|
+
@project_configuration = double(:story_completed_email_list => [], :story_assigned_email_list => [])
|
65
|
+
@project.project_configuration = @project_configuration
|
66
|
+
@event_mailer = EventMailer.new(@project)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "does not alert notififer if the assigned email list is empty" do
|
70
|
+
user = Artisan::Repository.user.new(:full_name => "Mister McGoo")
|
71
|
+
story = Artisan::Repository.story.new(:assigned_user => user, :name => "Test")
|
72
|
+
|
73
|
+
Notifier.should_not_receive(:new_assigned_user)
|
74
|
+
@event_mailer.new_assigned_user(story)
|
75
|
+
end
|
76
|
+
|
77
|
+
it "does not alert the notifier if the completed email list is empty" do
|
78
|
+
story = Artisan::Repository.story.new(:name => "Test")
|
79
|
+
|
80
|
+
Notifier.should_not_receive(:new_assigned_user)
|
81
|
+
@event_mailer.completed_story(story)
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|