be_taskable 0.5.0

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/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