be_taskable 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/readme.md ADDED
@@ -0,0 +1,292 @@
1
+ BeTaskable
2
+ ==========
3
+
4
+ BeTaskable is a small framework for creating and maintaining tasks / chores / assignments. Meaning something that someone has to do.
5
+
6
+ Concepts
7
+ --------
8
+
9
+ ### Taskable
10
+
11
+ Any object that needs action, e.g. a document. The taskable can have different actions e.g. publish, authorise
12
+
13
+ ### Assignee
14
+
15
+ Object that has to do the task. E.g. user
16
+
17
+ ### Task
18
+
19
+ An object representing an action that needs to be done for a particular taskable.
20
+ e.g. Publish document #29
21
+
22
+ A task can have many assignees (See Task Assignments).
23
+
24
+ ### Task Assignment
25
+
26
+ An object linking a task and a assignee.
27
+ e.g. User #80 can publish document #29
28
+
29
+ ### Resolver
30
+
31
+ An object linked to a taskable that has the business logic for your particular application.
32
+
33
+ Usage
34
+ -----
35
+
36
+ ### Taskable Model
37
+
38
+ Make a model taskable
39
+
40
+ class Taskable < ActiveRecord::Base
41
+ be_taskable
42
+ end
43
+
44
+ ### Task Resolver
45
+
46
+ A resolver is a class that should provide the business logic for each particular task.
47
+ The resolver name is composed of the following parts:
48
+
49
+ - Name of the taskable model e.g. Document
50
+ - + the name of the action e.g. Publish
51
+ - + 'TaskResolver'
52
+
53
+ class DocumentPublishTaskResolver < BeTaskable::TaskResolver
54
+
55
+ end
56
+
57
+ A resolver object should provide the following methods:
58
+
59
+ class DocumentPublishTaskResolver < BeTaskable::TaskResolver
60
+
61
+ def consensus?(task)
62
+ # This method should decide if the task is completed based on the assigments
63
+ # return true or false
64
+
65
+ # Some possible scenarios are:
66
+
67
+ # any assignment is completed then return true
68
+ # task.any_assignment_done?
69
+
70
+ # the majority of assignments are completed then return true
71
+ # task.majority_of_assignments_done?
72
+
73
+ # all task are completed then return true
74
+ # task.all_assignments_done?
75
+
76
+ # use task.assignments to calculate consensus manually
77
+ end
78
+
79
+ def is_task_relevant?(task)
80
+ true
81
+ end
82
+
83
+ def assignees_for_task(task)
84
+ # Return a list of assignees for this particular task
85
+ end
86
+
87
+ def due_date_for_assignment(assignment)
88
+ # called each time an assignment is created
89
+ # return here when the assignment should be completed by
90
+ # e.g. DateTime.now + two.weeks
91
+ # return nil = no due date
92
+ end
93
+
94
+ def visible_date_for_assignment(assignment)
95
+ # this sets the visible_at property on the assignment
96
+ # this is useful if you don't want an assignment to be visible until some time in the future
97
+ end
98
+
99
+ def label_for_task(task)
100
+ # return a label (name or description) for the task (if you need to show it on the ui)
101
+ # get the taskable by calling task.taskable
102
+ end
103
+
104
+ def label_for_assigment(assignment)
105
+ # return a label for the assignment
106
+ # get the taskable by calling assignment.taskable
107
+ end
108
+
109
+ def url_for_assignment(assignment)
110
+ # return a url where to go for the assigment
111
+ # get the taskable by calling assignment.taskable
112
+ end
113
+
114
+ # hooks
115
+ def on_creation(task)
116
+ # called when a task is created
117
+ end
118
+
119
+ def on_completion(task)
120
+ # will be called when a task is completed
121
+ end
122
+
123
+ def on_expiration(task)
124
+ # will be called when a task is expired
125
+ end
126
+
127
+ end
128
+
129
+ Create a resolver class for each taskable/action combination.
130
+
131
+ ### Creating a task
132
+
133
+ Given a taskable model, create a new task like this:
134
+
135
+ task = document.create_task_for_action('publish')
136
+
137
+ This creates the task and the assignments. You don't assign assignees to a task manually, they are assigned by the resolver.
138
+
139
+ Also there is a `create_or_refresh_task_for_action` method. This will reuse an existing task if present and is __not completed__ and __not expired__.
140
+
141
+ ### Completing a task
142
+
143
+ After completing an action you will usually have the taskable in hand. Using the taskable you can find the task like so:
144
+
145
+ task = taskable.last_task_for_action('publish') # will give you the last task
146
+ task.complete_by(assignee)
147
+
148
+ You may complete the task by using:
149
+
150
+ taskable.complete_task_for('publish', assignee)
151
+
152
+ When a task is completed several things will happen:
153
+
154
+ - Task will find the assignment for that particular assignee
155
+ - It will set the assignment as completed
156
+ - It will call the .consensus? method in the task resolver
157
+ - If consensus? returns true then it will set all the assignment to completed and the task as completed
158
+
159
+ You can check if a task is completed by doing:
160
+
161
+ task.completed?
162
+
163
+ Other options:
164
+
165
+ task.complete!
166
+ # completes the task regardless for all assignees. Marks all the assignments as completed.
167
+
168
+ taskable.complete_task_for_action('publish')
169
+ # same as task.complete!
170
+
171
+ Task.refresh
172
+ ------------
173
+
174
+ When task.refresh is called the following will happen:
175
+
176
+ - Mark all the current task assignments as 'unconfirmed'
177
+ - Find the list of assignees
178
+ - Find or create an assignment for each assignee
179
+ - Set those assignment to confirmed
180
+ - Delete all the assignments that are still left as 'unconfirmed'
181
+
182
+ This means that if the business rules change in your resolver or the assignees change (e.g. you have more users) then `task.refresh` will create and deleted assignments as needed.
183
+ `task.refresh` has no effect if the task is already completed. Also it won't delete assignments that are already completed.
184
+
185
+ Task.tally
186
+ ----------
187
+
188
+ This checks if the task can be considered done, it uses the `consensus?` method in the resolver to decide this. If the task is done then all assignments will be marked as completed.
189
+
190
+ Task.audit
191
+ --------
192
+
193
+ Calls `task.refresh` and `task.tally` immediatelly.
194
+
195
+ This is useful for an audit of process that runs everyday to check the validity of the assignments in your application, e.g.
196
+
197
+ BeTaskable::Task.find_each do |task|
198
+ task.audit
199
+ end
200
+
201
+ Task.expire
202
+ -----------
203
+
204
+ This sets the task as no longer valid, it expires all the assignments and calls the on_expiration method on the resolver
205
+
206
+ task.expire
207
+
208
+ Label and url
209
+ -------------
210
+
211
+ When task.run is called task.label, assignment.label and assignment.url are generated (using the resolver) and stored in the database. They are re-generated each time task.run is called.
212
+ When you call task.label, assignment.label and assignment.url they will be retrieved from the cached attribute stored in the database.
213
+ If you want to use the no-cached version use task.label!, assignment.label! and assignment.url! these will ask the resolver directly.
214
+
215
+ Who did the task?
216
+ -----------------
217
+
218
+ To find out who did a particular task do the following:
219
+
220
+ assignments = tasks.enacted_assignments # this are the assignments that were actually completed by their assignees
221
+ assignees = assignments.map(&:assignee)
222
+
223
+ Task Assignment Scopes
224
+ ---------------------
225
+
226
+ The following scopes are available for task assignments:
227
+
228
+ - completed
229
+ - uncompled
230
+ - visible
231
+ - expired
232
+ - unexpired
233
+ - overdue
234
+ - not_overdue
235
+ - current: which are uncompled + visible + unexpired + not_overdue
236
+
237
+ Assignee
238
+ ---------
239
+ This is the object doing a task. e.g. User
240
+
241
+ Mixin in `be_tasker` into your model to access the BeTaskable methods:
242
+
243
+ class User < ActiveRecord::Base
244
+ be_tasker
245
+ end
246
+
247
+ user.task_assignments #=> array with all assignments
248
+ user.task_assignments.current #=> array of current assignments
249
+
250
+ Testing (rspec)
251
+ -------
252
+
253
+ To stub a resolver do the following:
254
+
255
+ resolver = DocumentAuthorizeTaskResolver.new
256
+
257
+ # stub the taskable class
258
+ Document.stub(:_task_resolver_for_action).and_return(resolver)
259
+
260
+ # now you can stub the resolver
261
+ resolver.stub(:assignees_for_task).and_return([user1, user2])
262
+
263
+ document = Document.create
264
+ document.create_task_for_action('authorize') # this will use the stubbed resolver
265
+
266
+ Generators
267
+ ----------
268
+
269
+ A handy generator is provided for creating the necessary tables
270
+
271
+ rails g be_taskable:migration
272
+
273
+ Also a genator for task resolvers is provided:
274
+
275
+ rails g be_taskable:resolver document publish
276
+
277
+
278
+ Testing this Gem
279
+ ----------------
280
+
281
+ bundle
282
+ rspec
283
+
284
+ License
285
+ -------
286
+
287
+ MIT
288
+
289
+
290
+
291
+
292
+
@@ -0,0 +1,79 @@
1
+ require File.expand_path('../../../spec_helper', __FILE__)
2
+
3
+ describe 'End to End' do
4
+
5
+ let(:user1) { User.create }
6
+ let(:user2) { User.create }
7
+ let(:user3) { User.create }
8
+ let(:assignees) { [user1, user2] }
9
+ let(:taskable) { Taskable.create }
10
+ let(:resolver) { TaskableReviewTaskResolver.new }
11
+
12
+ before do
13
+ user1
14
+ user2
15
+ user3
16
+
17
+ resolver.stub(:assignees_for_task).and_return(assignees)
18
+
19
+ TaskableReviewTaskResolver.stub(:new).and_return(resolver)
20
+ end
21
+
22
+ # Create a task
23
+ # make sure assignees are assigned
24
+ # complete an assignment
25
+ # complete another assignment (that triggers consensus)
26
+ # task should be completed
27
+ it 'works' do
28
+ # resolver should receive on_creation when a task is created
29
+ resolver.should_receive(:on_creation)
30
+
31
+ task = taskable.create_task_for_action('review')
32
+
33
+ # there should be two assigments now
34
+ expect(BeTaskable::TaskAssignment.count).to eq(2)
35
+
36
+ # add more assignees
37
+ assignees.push(user3)
38
+
39
+ # task should have a label provided by the resolver
40
+ expect(task.label).to eq("Task label #{task.id}")
41
+
42
+ task.refresh
43
+
44
+ # there should be 3 assigments now
45
+ expect(BeTaskable::TaskAssignment.count).to eq(3)
46
+
47
+ # assignment should have a label provided by the resolver
48
+ assignment = BeTaskable::TaskAssignment.first
49
+ expect(assignment.label).to eq("Assignment label #{assignment.id}")
50
+
51
+ # assignment should have a url provided by the resolver
52
+ expect(assignment.url).to eq("Assignment url #{assignment.id}")
53
+
54
+ # assignment should have due date as provided by the resolver
55
+ expect(assignment.complete_by).to be_within(1.minute).of(DateTime.now + 10.days)
56
+
57
+ # complete the task for one of the assignees
58
+ taskable.complete_task_for('review', user1)
59
+
60
+ # the task should still be uncompleted
61
+ expect(task).not_to be_completed
62
+
63
+ # change the consensus
64
+ resolver.stub(:consensus?).and_return true
65
+
66
+ # the resolver shoud receive on_completion
67
+ resolver.should_receive(:on_completion)
68
+
69
+ # complete another task
70
+ taskable.complete_task_for('review', user2)
71
+
72
+ # task should be completed
73
+ task.reload
74
+ # puts task.state
75
+ # puts task.assignments.inspect
76
+ expect(task).to be_completed
77
+ end
78
+
79
+ end
@@ -0,0 +1,70 @@
1
+ require File.expand_path('../../../spec_helper', __FILE__)
2
+
3
+ describe "Irrelevance" do
4
+
5
+ let(:user1) { User.create }
6
+ let(:user2) { User.create }
7
+ let(:assignees) { [user1, user2] }
8
+ let(:taskable) { Taskable.create }
9
+ let(:resolver) { TaskableReviewTaskResolver.new }
10
+ let(:task) { taskable.create_task_for_action('review') }
11
+
12
+ before do
13
+ user1
14
+ user2
15
+
16
+ resolver.stub(:assignees_for_task).and_return(assignees)
17
+ TaskableReviewTaskResolver.stub(:new).and_return(resolver)
18
+
19
+ task
20
+ end
21
+
22
+ steps "irrelevant" do
23
+
24
+ it "creates the assigments when relevant" do
25
+ expect(task).to be_open
26
+
27
+ # there should be two assigments now
28
+ expect(BeTaskable::TaskAssignment.count).to eq(2)
29
+ end
30
+
31
+ it "deletes the assignments when irrelevant" do
32
+ # expect(BeTaskable::TaskAssignment.count).to eq(2)
33
+
34
+ # make the task irrelevant
35
+ resolver.stub(:is_task_relevant?).and_return(false)
36
+
37
+ task.refresh
38
+
39
+ expect(task).to be_irrelevant
40
+ expect(BeTaskable::TaskAssignment.count).to eq(0)
41
+ end
42
+
43
+ it "recreates the assignments when relevant again" do
44
+
45
+ # make task relevant again
46
+ resolver.unstub(:is_task_relevant?)
47
+
48
+ task.refresh
49
+
50
+ expect(task).to be_open
51
+ # there should be two assigments now
52
+ expect(BeTaskable::TaskAssignment.count).to eq(2)
53
+ end
54
+
55
+ it "doesnt delete already completed assignments" do
56
+ expect(BeTaskable::TaskAssignment.count).to eq(2)
57
+
58
+ resolver.stub(:is_task_relevant?).and_return(false)
59
+ resolver.stub(:consensus?).and_return(false)
60
+
61
+ BeTaskable::TaskAssignment.first.complete
62
+
63
+ task.refresh
64
+
65
+ expect(BeTaskable::TaskAssignment.count).to eq(1)
66
+ end
67
+
68
+ end
69
+
70
+ end
@@ -0,0 +1,17 @@
1
+ require File.expand_path('../../../spec_helper', __FILE__)
2
+
3
+ describe 'BeTaskable' do
4
+
5
+ describe "#be_taskable" do
6
+
7
+ it "should provide a class method be_taskable" do
8
+ expect(ActiveRecord::Base).to respond_to('be_taskable')
9
+ end
10
+
11
+ it "should provide a class method be_tasker" do
12
+ expect(ActiveRecord::Base).to respond_to('be_tasker')
13
+ end
14
+
15
+ end
16
+
17
+ end
@@ -0,0 +1,185 @@
1
+ require File.expand_path('../../../spec_helper', __FILE__)
2
+
3
+ describe 'BeTaskable::TaskAssignment' do
4
+
5
+ let(:task) { double.as_null_object }
6
+ let(:resolver) { double.as_null_object }
7
+ let(:assignment) { BeTaskable::TaskAssignment.new }
8
+
9
+ before do
10
+ assignment.stub(:task).and_return(task)
11
+ assignment.stub(:resolver).and_return(resolver)
12
+ end
13
+
14
+ describe "#completed" do
15
+ it "finds the completed" do
16
+ a1 = BeTaskable::TaskAssignment.create(completed_at: DateTime.now)
17
+ a2 = BeTaskable::TaskAssignment.create()
18
+ res = BeTaskable::TaskAssignment.completed.all
19
+ expect(res).to eq([a1])
20
+ end
21
+ end
22
+
23
+ describe "#uncompleted" do
24
+ it "finds the uncompleted" do
25
+ a1 = BeTaskable::TaskAssignment.create(completed_at: DateTime.now)
26
+ a2 = BeTaskable::TaskAssignment.create()
27
+ res = BeTaskable::TaskAssignment.uncompleted.all
28
+ expect(res).to eq([a2])
29
+ end
30
+ end
31
+
32
+ describe "#overdue" do
33
+ it "finds the overdue" do
34
+ a1 = BeTaskable::TaskAssignment.create(complete_by: DateTime.now - 1.day)
35
+ a2 = BeTaskable::TaskAssignment.create(complete_by: DateTime.now + 1.day)
36
+ a3 = BeTaskable::TaskAssignment.create()
37
+ res = BeTaskable::TaskAssignment.overdue.all
38
+ expect(res).to eq([a1])
39
+ end
40
+ end
41
+
42
+ describe "#not_overdue" do
43
+ it "finds the not_overdue" do
44
+ a1 = BeTaskable::TaskAssignment.create(complete_by: DateTime.now - 1.day)
45
+ a2 = BeTaskable::TaskAssignment.create(complete_by: DateTime.now + 1.day)
46
+ a3 = BeTaskable::TaskAssignment.create()
47
+ res = BeTaskable::TaskAssignment.not_overdue.all
48
+ expect(res).to eq([a2, a3])
49
+ end
50
+ end
51
+
52
+ describe "#visible" do
53
+ it "finds the visible" do
54
+ a1 = BeTaskable::TaskAssignment.create(visible_at: DateTime.now - 1.day)
55
+ a2 = BeTaskable::TaskAssignment.create()
56
+ a3 = BeTaskable::TaskAssignment.create(visible_at: DateTime.now + 1.day)
57
+ res = BeTaskable::TaskAssignment.visible.all
58
+ expect(res).to eq([a1, a2])
59
+ end
60
+ end
61
+
62
+ describe "#expired" do
63
+ it "finds the expired" do
64
+ a1 = BeTaskable::TaskAssignment.create(expired_at: DateTime.now - 1.day)
65
+ a2 = BeTaskable::TaskAssignment.create()
66
+ res = BeTaskable::TaskAssignment.expired.all
67
+ expect(res).to eq([a1])
68
+ end
69
+ end
70
+
71
+ describe "#unexpired" do
72
+ it "finds the unexpired" do
73
+ a1 = BeTaskable::TaskAssignment.create(expired_at: DateTime.now - 1.day)
74
+ a2 = BeTaskable::TaskAssignment.create()
75
+ res = BeTaskable::TaskAssignment.unexpired.all
76
+ expect(res).to eq([a2])
77
+ end
78
+ end
79
+
80
+ describe "#current" do
81
+
82
+ let(:a1) { BeTaskable::TaskAssignment.create() }
83
+ let(:a2) { BeTaskable::TaskAssignment.create(expired_at: DateTime.now - 1.day) }
84
+ let(:a3) { BeTaskable::TaskAssignment.create(complete_by: DateTime.now - 1.day) }
85
+ let(:a4) { BeTaskable::TaskAssignment.create(complete_by: DateTime.now + 1.day) }
86
+ let(:a5) { BeTaskable::TaskAssignment.create(completed_at: DateTime.now - 1.day) }
87
+ let(:a6) { BeTaskable::TaskAssignment.create(visible_at: DateTime.now + 1.day) }
88
+
89
+ before do
90
+ a1;a2;a3;a4;a5;a6
91
+ end
92
+
93
+ it "finds them" do
94
+ res = BeTaskable::TaskAssignment.current.all
95
+ expect(res).to eq([a1, a4])
96
+ end
97
+
98
+ it "doesnt find expired" do
99
+ res = BeTaskable::TaskAssignment.current.all
100
+ expect(res).not_to include(a2)
101
+ end
102
+
103
+ it "doesnt find overdue" do
104
+ res = BeTaskable::TaskAssignment.current.all
105
+ expect(res).not_to include(a3)
106
+ end
107
+
108
+ it "doesnt find completed" do
109
+ res = BeTaskable::TaskAssignment.current.all
110
+ expect(res).not_to include(a5)
111
+ end
112
+
113
+ it "doesnt find invisibles" do
114
+ res = BeTaskable::TaskAssignment.current.all
115
+ expect(res).not_to include(a6)
116
+ end
117
+
118
+ end
119
+
120
+ describe ".taskable" do
121
+ it "ask the task" do
122
+ task.should_receive(:taskable)
123
+ assignment.taskable
124
+ end
125
+ end
126
+
127
+ describe ".resolver" do
128
+ it 'asks the task' do
129
+ assignment.unstub(:resolver)
130
+ task.should_receive(:resolver)
131
+ assignment.resolver
132
+ end
133
+ end
134
+
135
+ describe ".complete" do
136
+ it 'sets the completed_at' do
137
+ assignment.complete
138
+ expect(assignment.completed_at).to be_within(1.minute).of(DateTime.now)
139
+ end
140
+
141
+ it "calls on_assignment_completed on task" do
142
+ task.should_receive(:on_assignment_completed).with(assignment)
143
+ assignment.complete
144
+ end
145
+
146
+ it "returns true" do
147
+ res = assignment.complete
148
+ expect(res).to be_true
149
+ end
150
+
151
+ it "returns false if assignment has already been completed" do
152
+ assignment.update_attribute(:completed_at, DateTime.now)
153
+ res = assignment.complete
154
+ expect(res).to be_false
155
+ end
156
+
157
+ it "sets an errors array if it returns false" do
158
+ assignment.update_attribute(:completed_at, DateTime.now)
159
+ res = assignment.complete
160
+ expect(res).to be_false
161
+ #expect(res.errors.size).not_to be_empty
162
+ end
163
+
164
+ it "sets enacted" do
165
+ res = assignment.complete
166
+ # assignment.reload
167
+ expect(assignment).to be_enacted
168
+ end
169
+ end
170
+
171
+ describe ".label!" do
172
+ it "asks the resolver" do
173
+ resolver.should_receive(:label_for_assignment)
174
+ assignment.label!
175
+ end
176
+ end
177
+
178
+ describe ".url!" do
179
+ it "asks the resolver" do
180
+ resolver.should_receive(:url_for_assignment)
181
+ assignment.url!
182
+ end
183
+ end
184
+
185
+ end