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