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.
@@ -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