be_taskable 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,87 @@
1
+ module BeTaskable
2
+ class TaskRunner
3
+
4
+ attr_reader :task
5
+
6
+ def initialize(task)
7
+ raise ArgumentError, "Invalid task" unless task.is_a?(BeTaskable::Task)
8
+ @task = task
9
+ end
10
+
11
+ def refresh
12
+
13
+ return if task.completed?
14
+
15
+ _mark_assignments_as_unconfirmed
16
+
17
+ if _relevant?
18
+ _make_task_relevant
19
+ _update_task_label
20
+ _update_assignments
21
+ else
22
+ _make_task_irrelevant
23
+ end
24
+
25
+ _delete_unconfirmed_assignments
26
+ end
27
+
28
+ def resolver
29
+ task.resolver
30
+ end
31
+
32
+ def _mark_assignments_as_unconfirmed
33
+ _assignments.uncompleted.update_all(confirmed: false)
34
+ end
35
+
36
+ def _delete_unconfirmed_assignments
37
+ _assignments.uncompleted.unconfirmed.delete_all
38
+ end
39
+
40
+ def _update_task_label
41
+ task.update_attribute(:label, resolver.label_for_task(task))
42
+ end
43
+
44
+ def _make_task_relevant
45
+ task.make_relevant if task.can_make_relevant?
46
+ end
47
+
48
+ def _make_task_irrelevant
49
+ # assignments will be just deleted
50
+ # no need to update them
51
+ task.make_irrelevant if task.can_make_irrelevant?
52
+ end
53
+
54
+ def _update_assignments
55
+ _assignees.each do |assignee|
56
+ # find the assignment
57
+ assignment = _assignments.where(assignee_id: assignee.id).last
58
+
59
+ unless assignment
60
+ assignment = _assignments.create(assignee: assignee)
61
+ end
62
+
63
+ assignment.complete_by = resolver.due_date_for_assignment(assignment)
64
+ assignment.visible_at = resolver.visible_date_for_assignment(assignment)
65
+ assignment.label = resolver.label_for_assignment(assignment)
66
+ assignment.url = resolver.url_for_assignment(assignment)
67
+ assignment.confirmed = true
68
+ assignment.save
69
+ end
70
+ end
71
+
72
+ def _relevant?
73
+ # if the taskable cannot be found then assume it is not relevant
74
+ return false unless task.taskable
75
+ resolver.is_task_relevant?(task)
76
+ end
77
+
78
+ def _assignees
79
+ resolver.assignees_for_task(task)
80
+ end
81
+
82
+ def _assignments
83
+ task.assignments
84
+ end
85
+
86
+ end
87
+ end
@@ -0,0 +1,157 @@
1
+ module BeTaskable
2
+ module Taskable
3
+
4
+ def be_taskable(*actions)
5
+ include InstanceMethods
6
+
7
+ has_many :tasks, class_name: '::BeTaskable::Task', dependent: :destroy, as: :taskable
8
+ end
9
+
10
+ def _task_resolver_name_for_action(action)
11
+ self.name + action.camelize + 'TaskResolver'
12
+ end
13
+
14
+ def _task_resolver_for_action(action)
15
+ _task_resolver_name_for_action(action).constantize.new
16
+ end
17
+
18
+ module InstanceMethods
19
+
20
+ # hook for testing
21
+ # e.g. expect(instance).to be_taskable
22
+ def taskable?
23
+ true
24
+ end
25
+
26
+ # @param {String} action Name of the action
27
+ # @return {Object} Resolver instance for the given action
28
+ def task_resolver_for_action(action)
29
+ self.class._task_resolver_for_action(action)
30
+ end
31
+
32
+ # Create a task and run it
33
+ # @param {String} action Name of the action
34
+ # @return {BeTaskable::Task} A task object
35
+ def create_task_for_action(action)
36
+ raise(ActiveRecord::RecordNotSaved, "Taskable must be persisted") unless self.persisted?
37
+
38
+ task = tasks.create(action: action)
39
+
40
+ if task.persisted?
41
+ task.on_creation
42
+ task.refresh
43
+ else
44
+ raise "Couldn't create task #{task.errors.full_messages}"
45
+ end
46
+
47
+ task
48
+ end
49
+
50
+ # Finds or Create a task and run it
51
+ # @param {String} action Name of the action
52
+ # @return {BeTaskable::Task} A task object
53
+ def create_or_refresh_task_for_action(action)
54
+ # if already created use that task
55
+ task = last_current_task_for_action(action)
56
+
57
+ if !task
58
+ task = create_task_for_action(action)
59
+ else
60
+ task.refresh
61
+ end
62
+
63
+ task
64
+ end
65
+
66
+ # @param {String} action Name of the action
67
+ # @return {ActiveRecord::Relation}
68
+ def tasks_for_action(action)
69
+ tasks.where(action: action)
70
+ end
71
+
72
+ def current_tasks_for_action(action)
73
+ tasks.where(action: action).current
74
+ end
75
+
76
+ # @param {String} action Name of the action
77
+ # @return {BeTaskable::Task} Last task for the given action
78
+ def last_task_for_action(action)
79
+ tasks_for_action(action).last
80
+ end
81
+
82
+ # @return {BeTaskable::Task} Last current task for the given action
83
+ def last_current_task_for_action(action)
84
+ current_tasks_for_action(action).last
85
+ end
86
+
87
+ # @return {Array} All current assignments for this taskable
88
+ def task_assignments
89
+ tasks.map(&:assignments).flatten
90
+ end
91
+
92
+ # @return {Array} All current assignments for this action
93
+ # returns an empty array if it cannot find the task
94
+ def task_assignments_for_action(action)
95
+ tasks_for_action(action).map(&:assignments).flatten
96
+ end
97
+
98
+
99
+ # @param {String} action
100
+ # @param {Object} assignee
101
+ # @return {TaskAssignment}
102
+ # Return nil if it cannot find the task
103
+ def task_assignment_for(action, assignee)
104
+ task = last_task_for_action(action)
105
+ task.assignment_for(assignee) if task
106
+ end
107
+
108
+ def current_task_assignment_for(action, assignee)
109
+ task = last_current_task_for_action(action)
110
+ task.assignment_for(assignee) if task
111
+ end
112
+
113
+ # @param {String} action Name of the action
114
+ # Completes the last task for an action (and all the assignments)
115
+ def complete_task_for_action(action)
116
+ task = last_task_for_action(action)
117
+ return false unless task
118
+ task.complete!
119
+ end
120
+
121
+ # Completes all task for this action
122
+ # Only for uncompleted tasks
123
+ def complete_tasks_for_action(action)
124
+ ts = current_tasks_for_action(action)
125
+
126
+ return false unless ts.any?
127
+ ts.each do |task|
128
+ task.complete!
129
+ end
130
+ true
131
+ end
132
+
133
+ # @param {String} action Name of the action
134
+ # @param {Object} assignee
135
+ # Completes a task assignment
136
+ def complete_task_for(action, assignee)
137
+ task = last_task_for_action(action)
138
+ return false unless task
139
+ task.complete_by(assignee)
140
+ end
141
+
142
+ # Expire all task for this action
143
+ # Only for uncompleted tasks
144
+ def expire_tasks_for_action(action)
145
+ ts = current_tasks_for_action(action)
146
+
147
+ return false unless ts.any?
148
+ ts.each do |task|
149
+ task.expire
150
+ end
151
+ true
152
+ end
153
+
154
+ end
155
+
156
+ end
157
+ end
@@ -0,0 +1,21 @@
1
+ module BeTaskable
2
+ module Tasker
3
+
4
+ def be_tasker
5
+ include InstanceMethods
6
+
7
+ has_many :task_assignments, class_name: '::BeTaskable::TaskAssignment', dependent: :destroy, foreign_key: :assignee_id
8
+ end
9
+
10
+ module InstanceMethods
11
+
12
+ # hook for testing
13
+ # e.g. expect(instance).to be_tasker
14
+ def tasker?
15
+ true
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,28 @@
1
+ require "active_record"
2
+ require "active_record/version"
3
+ # require "action_view"
4
+
5
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
6
+
7
+ module BeTaskable
8
+ def self.table_name_prefix
9
+ 'be_taskable_'
10
+ end
11
+ end
12
+
13
+ require "be_taskable/taskable"
14
+ require "be_taskable/tasker"
15
+ require "be_taskable/task"
16
+ require "be_taskable/task_assignment"
17
+ require "be_taskable/task_resolver"
18
+ require "be_taskable/task_runner"
19
+
20
+ if defined?(ActiveRecord::Base)
21
+ ActiveRecord::Base.extend BeTaskable::Taskable
22
+ ActiveRecord::Base.extend BeTaskable::Tasker
23
+ end
24
+
25
+ # view helpers
26
+ # if defined?(ActionView::Base)
27
+ # ActionView::Base.send :include, ActsAsTaggableOn::TagsHelper
28
+ # end
@@ -0,0 +1,44 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+
4
+ module BeTaskable
5
+ class MigrationGenerator < Rails::Generators::Base
6
+
7
+ include Rails::Generators::Migration
8
+
9
+ desc "Generates migration for BeTaskable Models"
10
+
11
+ def self.orm
12
+ Rails::Generators.options[:rails][:orm]
13
+ end
14
+
15
+ def self.source_root
16
+ File.join(File.dirname(__FILE__), 'templates', (orm.to_s unless orm.class.eql?(String)) )
17
+ end
18
+
19
+ def self.orm_has_migration?
20
+ [:active_record].include? orm
21
+ end
22
+
23
+ def self.next_migration_number(dirname)
24
+ if ActiveRecord::Base.timestamped_migrations
25
+ migration_number = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
26
+ migration_number += 1
27
+ migration_number.to_s
28
+ else
29
+ "%.3d" % (current_migration_number(dirname) + 1)
30
+ end
31
+ end
32
+
33
+ def create_migration_file
34
+ if self.class.orm_has_migration?
35
+ migration_template 'migration.rb', "db/migrate/#{migration_name}"
36
+ end
37
+ end
38
+
39
+ def migration_name
40
+ 'be_taskable_migration'
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,28 @@
1
+ require 'rails/generators'
2
+
3
+ module BeTaskable
4
+ class ResolverGenerator < Rails::Generators::Base
5
+ desc "Create a Task Resolver"
6
+
7
+ def self.source_root
8
+ @source_root ||= File.join(File.dirname(__FILE__), 'templates')
9
+ end
10
+
11
+ argument :taskable, type: :string
12
+ argument :action_name, type: :string
13
+
14
+ def copy_class_file
15
+ dest = "app/task_resolvers/#{taskable}_#{action_name}_task_resolver.rb"
16
+ copy_file "resolver.rb.tpl", dest
17
+ class_name = "#{taskable.camelize}#{action_name.camelize}TaskResolver"
18
+ gsub_file dest, '{{class_name}}', class_name
19
+ end
20
+
21
+ # def copy_spec_file
22
+ # dest = "spec/services/#{name}_service_spec.rb"
23
+ # copy_file "spec.rb.tpl", dest
24
+ # gsub_file dest, '{{class_name}}', "#{name.camelize}Service"
25
+ # end
26
+
27
+ end
28
+ end
@@ -0,0 +1,35 @@
1
+ class BeTaskableMigration < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :be_taskable_tasks do |t|
4
+ t.string :action
5
+ t.references :taskable, polymorphic: true
6
+ t.string :state
7
+ t.string :label
8
+ t.datetime :completed_at
9
+ t.datetime :expired_at
10
+ t.timestamps
11
+ end
12
+
13
+ create_table :be_taskable_task_assignments do |t|
14
+ t.integer :task_id
15
+ t.references :assignee, polymorphic: true
16
+ t.string :label
17
+ t.string :url
18
+ t.boolean :confirmed
19
+ t.boolean :enacted
20
+ t.datetime :visible_at
21
+ t.datetime :complete_by
22
+ t.datetime :completed_at
23
+ t.datetime :expired_at
24
+ t.timestamps
25
+ end
26
+
27
+ add_index :be_taskable_tasks, [:taskable_id, :taskable_type]
28
+ add_index :be_taskable_task_assignments, :task_id
29
+ end
30
+
31
+ def self.down
32
+ drop_table :be_taskable_tasks
33
+ drop_table :be_taskable_task_assignments
34
+ end
35
+ end
@@ -0,0 +1,67 @@
1
+ class {{class_name}} < BeTaskable::TaskResolver
2
+
3
+ def consensus?(task)
4
+ # task.any_assignment_done?
5
+ # if any assignment is completed then return true
6
+
7
+ # task.majority_of_assignments_done?
8
+ # if the majority of assignments are completed then return true
9
+
10
+ # task.all_assignments_done?
11
+ # if all assignments are completed then return true
12
+
13
+ # use task.assignments to calculate consensus manually
14
+ # false
15
+ end
16
+
17
+ def is_task_relevant?(task)
18
+ # get the taskable by calling task.taskable
19
+ # evaluate if a task is still relevant
20
+ # e.g. the taskable object is no longer valid
21
+ # if this method returns false then:
22
+ # - the task will be marked as irrelevant
23
+ # - the assignments will be deleted (except the already completed ones)
24
+ true
25
+ end
26
+
27
+ def assignees_for_task(task)
28
+ # get the taskable by calling task.taskable
29
+ []
30
+ end
31
+
32
+ def due_date_for_assignment(assignment)
33
+ # get the taskable by calling assignment.taskable
34
+ nil
35
+ end
36
+
37
+ def visible_date_for_assignment(assignment)
38
+ # get the taskable by calling assignment.taskable
39
+ nil
40
+ end
41
+
42
+ def label_for_task(task)
43
+ # get the taskable by calling task.taskable
44
+ ""
45
+ end
46
+
47
+ def label_for_assignment(assignment)
48
+ # get the taskable by calling assignment.taskable
49
+ ""
50
+ end
51
+
52
+ def url_for_assignment(assignment)
53
+ # get the taskable by calling assignment.taskable
54
+ ""
55
+ end
56
+
57
+ # hooks
58
+ def on_creation(task)
59
+ end
60
+
61
+ def on_completion(task)
62
+ end
63
+
64
+ def on_expiration(task)
65
+ end
66
+
67
+ end