task-manager 0.1.1

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.
Files changed (69) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +4 -0
  3. data/Rakefile +27 -0
  4. data/app/assets/images/task-manager/icons/add.png +0 -0
  5. data/app/assets/images/task-manager/icons/delete.png +0 -0
  6. data/app/assets/images/task-manager/icons/edit.png +0 -0
  7. data/app/assets/javascripts/task-manager/application.js +43 -0
  8. data/app/assets/javascripts/task-manager/extjs/app/controller/Plans.js +359 -0
  9. data/app/assets/javascripts/task-manager/extjs/app/controller/Tasks.js +56 -0
  10. data/app/assets/javascripts/task-manager/extjs/app/helper/application_helper.js +24 -0
  11. data/app/assets/javascripts/task-manager/extjs/app/model/Assignee.js +19 -0
  12. data/app/assets/javascripts/task-manager/extjs/app/model/Plan.js +195 -0
  13. data/app/assets/javascripts/task-manager/extjs/app/model/Task.js +23 -0
  14. data/app/assets/javascripts/task-manager/extjs/app/store/Assignees.js +53 -0
  15. data/app/assets/javascripts/task-manager/extjs/app/store/AssigneesTree.js +3 -0
  16. data/app/assets/javascripts/task-manager/extjs/app/store/Booleans.js +9 -0
  17. data/app/assets/javascripts/task-manager/extjs/app/store/Days.js +38 -0
  18. data/app/assets/javascripts/task-manager/extjs/app/store/Hours.js +31 -0
  19. data/app/assets/javascripts/task-manager/extjs/app/store/Minutes.js +67 -0
  20. data/app/assets/javascripts/task-manager/extjs/app/store/Months.js +19 -0
  21. data/app/assets/javascripts/task-manager/extjs/app/store/Plans.js +6 -0
  22. data/app/assets/javascripts/task-manager/extjs/app/store/QuarterlyMonths.js +10 -0
  23. data/app/assets/javascripts/task-manager/extjs/app/store/Statuses.js +11 -0
  24. data/app/assets/javascripts/task-manager/extjs/app/store/Tasks.js +6 -0
  25. data/app/assets/javascripts/task-manager/extjs/app/store/Types.js +12 -0
  26. data/app/assets/javascripts/task-manager/extjs/app/store/WeekDays.js +14 -0
  27. data/app/assets/javascripts/task-manager/extjs/app/view/plan/AssignablesWindow.js +23 -0
  28. data/app/assets/javascripts/task-manager/extjs/app/view/plan/Edit.js +189 -0
  29. data/app/assets/javascripts/task-manager/extjs/app/view/plan/EditWindow.js +21 -0
  30. data/app/assets/javascripts/task-manager/extjs/app/view/plan/Grid.js +92 -0
  31. data/app/assets/javascripts/task-manager/extjs/app/view/plan/Index.js +24 -0
  32. data/app/assets/javascripts/task-manager/extjs/app/view/plan/New.js +190 -0
  33. data/app/assets/javascripts/task-manager/extjs/app/view/plan/Search.js +132 -0
  34. data/app/assets/javascripts/task-manager/extjs/app/view/plan/SelectAssignables.js +22 -0
  35. data/app/assets/javascripts/task-manager/extjs/app/view/plan/SelectAssignablesGrid.js +22 -0
  36. data/app/assets/javascripts/task-manager/extjs/app/view/plan/SelectAssignablesTree.js +9 -0
  37. data/app/assets/javascripts/task-manager/extjs/app/view/plan/Window.js +21 -0
  38. data/app/assets/javascripts/task-manager/extjs/app/view/task/Grid.js +62 -0
  39. data/app/assets/javascripts/task-manager/extjs/app/view/task/Index.js +21 -0
  40. data/app/assets/javascripts/task-manager/extjs/app/view/task/Search.js +148 -0
  41. data/app/assets/javascripts/task-manager/extjs/lib/ux/MultiSelectablePagingGrid.js +20 -0
  42. data/app/assets/javascripts/task-manager/extjs/lib/ux/RowExpander.js +230 -0
  43. data/app/assets/stylesheets/task-manager/application.css.scss +13 -0
  44. data/app/assets/stylesheets/task-manager/extjs.css.scss +43 -0
  45. data/app/controllers/task_manager/api/v1/plans_controller.rb +251 -0
  46. data/app/controllers/task_manager/api/v1/tasks_controller.rb +82 -0
  47. data/app/controllers/task_manager/application_controller.rb +4 -0
  48. data/app/helpers/task_manager/application_helper.rb +4 -0
  49. data/app/models/task_manager/assignable.rb +10 -0
  50. data/app/models/task_manager/callable.rb +10 -0
  51. data/app/models/task_manager/plan.rb +114 -0
  52. data/app/models/task_manager/task.rb +81 -0
  53. data/app/serializers/task_manager/plan_serializer.rb +8 -0
  54. data/app/serializers/task_manager/task_serializer.rb +8 -0
  55. data/app/views/layouts/task-manager/application.html.erb +14 -0
  56. data/config/routes.rb +8 -0
  57. data/db/migrate/20121102054723_setup_hstore.rb +9 -0
  58. data/db/migrate/20121102055137_create_task_manager_plans.rb +16 -0
  59. data/db/migrate/20121102142657_create_task_manager_assignables.rb +13 -0
  60. data/db/migrate/20121102150720_create_task_manager_callables.rb +13 -0
  61. data/db/migrate/20121105170602_create_task_manager_tasks.rb +18 -0
  62. data/lib/task-manager.rb +37 -0
  63. data/lib/task-manager/api_constraints.rb +12 -0
  64. data/lib/task-manager/deadline_calculator.rb +22 -0
  65. data/lib/task-manager/deadline_validator.rb +27 -0
  66. data/lib/task-manager/engine.rb +5 -0
  67. data/lib/task-manager/version.rb +3 -0
  68. data/lib/tasks/task-manager_tasks.rake +4 -0
  69. metadata +327 -0
@@ -0,0 +1,251 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module TaskManager
3
+ module Api
4
+ module V1
5
+ class PlansController < TaskManager::ApplicationController
6
+ respond_to :json
7
+
8
+ # 查询计划
9
+ #
10
+ # 支持的查询属性有:
11
+ # name 计划名
12
+ # plan_type 计划类型
13
+ # autocompletable 是否自动完成
14
+ # last_task_created_at 最后任务生成时间
15
+ # enabled_at 生效时间
16
+ #
17
+ # 支持的查询操作参见 https://github.com/ernie/ransack/wiki/Basic-Searching
18
+ #
19
+ # 分页查询参数:
20
+ # page 请求的页码,缺省值1
21
+ # limit 每页记录数,缺省值25
22
+ #
23
+ # @example
24
+ # # 请求
25
+ # GET /api/plans?q[name_cont]=... HTTP/1.1
26
+ # Accept: application/vnd.menglifang.com.cn; version=1
27
+ #
28
+ # # 响应
29
+ # HTTP/1.1 200 OK
30
+ # {
31
+ # "total": ...,
32
+ # "plans": [{
33
+ # "id": ...,
34
+ # "name": ...,
35
+ # "data": ...,
36
+ # "plan_type": ...,
37
+ # "enabled_at": ...,
38
+ # "last_task_created_at": ...,
39
+ # "begin_to_remind": ...,
40
+ # "autocompletable": ...,
41
+ # "created_at": ...,
42
+ # "updated_at": ...,
43
+ # "assignees": [{
44
+ # "id": ...,
45
+ # "name": ...
46
+ # }, ...]
47
+ # }, ...]
48
+ # }
49
+ def index
50
+ plans = TaskManager::Plan.search(params[:q]).result
51
+ result = {
52
+ total: plans.count,
53
+ plans: ActiveModel::ArraySerializer.new(
54
+ plans.page(params[:page]).per(params[:limit])
55
+ ).as_json
56
+ }
57
+
58
+ render json: result, status: :ok
59
+ end
60
+
61
+ # 创建计划
62
+ #
63
+ # @example
64
+ # # 请求
65
+ # POST /api/plans HTTP/1.1
66
+ # Accept: application/vnd.menglifang.com.cn; version=1
67
+ #
68
+ # {
69
+ # "plan": {
70
+ # "name": ..., # 必填且唯一
71
+ # "plan_type": ..., # 必填,有效取值:'daily',
72
+ # # 'weekly', 'monthly',
73
+ # # 'quarterly', 'yearly'
74
+ # "data": {
75
+ # "x": ..., # 可选
76
+ # "y": ..., # 可选
77
+ # "deadline_month": ..., # 当"plan_type"为'yearly'或
78
+ # # 'quarterly'时,为必填项;
79
+ # # 否则,为null
80
+ # "deadline_day": ..., # 当"plan_type"为'monthly'或
81
+ # # 'weekly'时,为必填项;
82
+ # # 否则,为null
83
+ # "deadline_hour": ..., # 必填
84
+ # "deadline_minute": ... # 必填
85
+ # },
86
+ # "enabled_at": ..., # 必填
87
+ # "begin_to_remind": ..., # 必填且小于等于0
88
+ # "autocompletable": ..., # 必填,缺省值为false
89
+ # "assignables_attributes": [{ # 至少需要一个assignable
90
+ # "assignee_id": ...,
91
+ # "assignee_type": ...
92
+ # }, ...],
93
+ #
94
+ # "callables_attributes": [{ # 可选
95
+ # "callback_id": ...,
96
+ # "callback_type": ...
97
+ # }]
98
+ # }
99
+ # }
100
+ #
101
+ # # 响应
102
+ # ## 成功
103
+ # HTTP/1.1 201 Created
104
+ # {
105
+ # "plan": {
106
+ # "id": ...,
107
+ # "name": ...,
108
+ # "plan_type": ...,
109
+ # "data": ...,
110
+ # "enabled_at": ...,
111
+ # "begin_to_remind": ...,
112
+ # "autocompletable": ...,
113
+ # "created_at": ...,
114
+ # "updated_at": ...,
115
+ # "assignees": [{
116
+ # "id": ...,
117
+ # "name": ...
118
+ # }, ...]
119
+ # }
120
+ # }
121
+ # ## 失败
122
+ # HTTP/1.1 422 Unprocessable Entity
123
+ # {
124
+ # "errors": {
125
+ # "name": [...],
126
+ # ...
127
+ # }
128
+ # }
129
+ def create
130
+ plan = TaskManager::Plan.new(params[:plan])
131
+
132
+ if plan.save
133
+ render json: plan, status: :created
134
+ else
135
+ render json: { errors: plan.errors }, status: :unprocessable_entity
136
+ end
137
+ end
138
+
139
+ # 更新计划
140
+ #
141
+ # @example
142
+ # # 请求
143
+ # PUT /api/plans/... HTTP/1.1
144
+ # Accept: application/vnd.menglifang.com.cn; version=1
145
+ #
146
+ # {
147
+ # "plan": {
148
+ # "name": ..., # 必填且唯一
149
+ # "plan_type": ..., # 必填,有效取值:'daily',
150
+ # # 'weekly', 'monthly',
151
+ # # 'quarterly', 'yearly'
152
+ # "data": {
153
+ # "x": ..., # 可选
154
+ # "y": ..., # 可选
155
+ # "deadline_month": ..., # 当"plan_type"为'yearly'或
156
+ # # 'quarterly'时,为必填项;
157
+ # # 否则,为null
158
+ # "deadline_day": ..., # 当"plan_type"为'monthly'或
159
+ # # 'weekly'时,为必填项;
160
+ # # 否则,为null
161
+ # "deadline_hour": ..., # 必填
162
+ # "deadline_minute": ... # 必填
163
+ # },
164
+ # "enabled_at": ..., # 必填
165
+ # "begin_to_remind": ..., # 必填且小于等于0
166
+ # "autocompletable": ..., # 必填,缺省值为false
167
+ #
168
+ # "assignables_attributes": [{
169
+ # "id": ..., ## 可选,
170
+ # ## 如果有,则更新,否则创建
171
+ # "assignee_id": ..., ## 必填
172
+ # "assignee_type": ..., ## 必填
173
+ # "_destroy": "1" ## 可选,
174
+ # ## 如果设置,存在则删除,
175
+ # ## 不存在则忽略
176
+ # }, ...],
177
+ #
178
+ # "callables_attributes": [{ # 可选
179
+ # "id": ..., ## 可选,
180
+ # ## 如果有,则更新,否则创建
181
+ # "callback_id": ..., ## 必填
182
+ # "callback_type": ..., ## 必填
183
+ # "_destroy": "1" ## 可选,
184
+ # ## 如果设置,存在则删除,
185
+ # ## 不存在则忽略
186
+ # }]
187
+ # }
188
+ # }
189
+ #
190
+ # # 响应
191
+ # ## 成功
192
+ # HTTP/1.1 202 Accepted
193
+ # {
194
+ # "plan": {
195
+ # "id": ...,
196
+ # "name": ...,
197
+ # "plan_type": ...,
198
+ # "data": ...,
199
+ # "enabled_at": ...,
200
+ # "begin_to_remind": ...,
201
+ # "autocompletable": ...,
202
+ # "created_at": ...,
203
+ # "updated_at": ...,
204
+ # "assignees": [{
205
+ # "id": ...,
206
+ # "name": ...
207
+ # }, ...]
208
+ # }
209
+ # }
210
+ # ## 失败
211
+ # HTTP/1.1 422 Unprocessable Entity
212
+ # {
213
+ # "errors": {
214
+ # "name": [...],
215
+ # ...
216
+ # }
217
+ # }
218
+ def update
219
+ plan.assignables.destroy_all
220
+
221
+ if plan.update_attributes(params[:plan])
222
+ render json: plan, status: :ok
223
+ else
224
+ render json: { errors: plan.errors }, status: :unprocessable_entity
225
+ end
226
+ end
227
+
228
+ # 删除计划
229
+ #
230
+ # @example
231
+ # # 请求
232
+ # DELETE /api/plans/... HTTP/1.1
233
+ # Accept: application/vnd.menglifang.com.cn; version=1
234
+ #
235
+ # # 响应
236
+ # ## 成功
237
+ # HTTP/1.1 204 No Content
238
+ def destroy
239
+ plan.destroy
240
+
241
+ head :no_content
242
+ end
243
+
244
+ private
245
+ def plan
246
+ TaskManager::Plan.find(params[:id])
247
+ end
248
+ end
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,82 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module TaskManager
3
+ module Api
4
+ module V1
5
+ class TasksController < TaskManager::ApplicationController
6
+ respond_to :json
7
+
8
+ # 查询任务
9
+ #
10
+ # 支持的查询属性有:
11
+ # name 任务名
12
+ # task_type 任务类型
13
+ # deadline 截止时间
14
+ # status 状态
15
+ # updated_at 完成时间
16
+ #
17
+ # 支持的查询操作参见 https://github.com/ernie/ransack/wiki/Basic-Searching
18
+ #
19
+ # 分页查询参数:
20
+ # page 请求的页码,缺省值1
21
+ # limit 每页记录数,缺省值25
22
+ #
23
+ # @example
24
+ # # 请求
25
+ # GET /api/tasks?q[name_cont]=... HTTP/1.1
26
+ # Accept: application/vnd.menglifang.com.cn; version=1
27
+ #
28
+ # # 响应
29
+ # HTTP/1.1 200 OK
30
+ # {
31
+ # "total": ...,
32
+ # "tasks": [{
33
+ # "id": ...,
34
+ # "name": ...,
35
+ # "data": ...,
36
+ # "task_type": ...,
37
+ # "deadline": ...,
38
+ # "status": ...,
39
+ # "created_at": ...,
40
+ # "updated_at": ...,
41
+ # "assignee": {
42
+ # "id": ...,
43
+ # "name": ...
44
+ # }
45
+ # }, ...]
46
+ # }
47
+ def index
48
+ tasks = TaskManager::Task.search(params[:q]).result
49
+ result = {
50
+ total: tasks.count,
51
+ tasks: ActiveModel::ArraySerializer.new(
52
+ tasks.page(params[:page]).per(params[:limit])
53
+ ).as_json
54
+ }
55
+
56
+ render json: result, status: :ok
57
+ end
58
+
59
+ # 删除任务
60
+ #
61
+ # @example
62
+ # # 请求
63
+ # DELETE /api/tasks/... HTTP/1.1
64
+ # Accept: application/vnd.menglifang.com.cn; version=1
65
+ #
66
+ # # 响应
67
+ # ## 成功
68
+ # HTTP/1.1 204 No Content
69
+ def destroy
70
+ task.destroy
71
+
72
+ head :no_content
73
+ end
74
+
75
+ private
76
+ def task
77
+ TaskManager::Task.find(params[:id])
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,4 @@
1
+ module TaskManager
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module TaskManager
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,10 @@
1
+ module TaskManager
2
+ class Assignable < ActiveRecord::Base
3
+ belongs_to :target, polymorphic: true
4
+ belongs_to :assignee, polymorphic: true
5
+
6
+ validates_presence_of :assignee_id, :assignee_type
7
+
8
+ attr_accessible :assignee_id, :assignee_type
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module TaskManager
2
+ class Callable < ActiveRecord::Base
3
+ belongs_to :callback, polymorphic: true
4
+ belongs_to :target, polymorphic: true
5
+
6
+ validates_presence_of :callback_id, :callback_type
7
+
8
+ attr_accessible :callback_id, :callback_type
9
+ end
10
+ end
@@ -0,0 +1,114 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module TaskManager
3
+ # TODO 验证data的合法性
4
+ class Plan < ActiveRecord::Base
5
+ include TaskManager::DeadlineCalculator
6
+ extend Enumerize
7
+
8
+ has_many :assignables, as: :target
9
+ has_many :callables, as: :target
10
+
11
+ enumerize :plan_type, in: [:daily, :weekly, :monthly, :quarterly, :yearly]
12
+
13
+ serialize :data, ActiveRecord::Coders::Hstore
14
+
15
+ accepts_nested_attributes_for :assignables, :callables
16
+
17
+ attr_accessible :autocompletable, :data, :last_task_created_at,
18
+ :name, :plan_type, :begin_to_remind, :enabled_at,
19
+ :assignables_attributes, :callables_attributes
20
+
21
+ validates_presence_of :name, :plan_type, :begin_to_remind,
22
+ :enabled_at, :assignables
23
+ validates_uniqueness_of :name
24
+ validates_numericality_of :begin_to_remind, greater_than_or_equal_to: 0
25
+
26
+ # validate the deadline
27
+ validates :data, deadline: true
28
+
29
+ def assignees
30
+ assignables.collect(&:assignee)
31
+ end
32
+
33
+ def callbacks
34
+ callables.collect(&:callback)
35
+ end
36
+
37
+ # 生成计划任务
38
+ #
39
+ # 为每一个计划的执行者创建一个计划任务。
40
+ def generate_tasks
41
+ now = Time.now
42
+ default_deadline = case plan_type.to_sym
43
+ when :daily then now.end_of_day
44
+ when :weekly then now.end_of_week
45
+ when :monthly then now.end_of_month
46
+ when :quarterly then now.end_of_quarter
47
+ when :yearly then now.end_of_year
48
+ end
49
+
50
+ #reminding_at = default_deadline.ago(-(begin_to_remind * 60))
51
+ reminding_at = default_deadline.ago(begin_to_remind * 60)
52
+ status = autocompletable ? :finished : :new
53
+ finished_at = autocompletable ? Time.now : nil
54
+
55
+ tasks = []
56
+ data.symbolize_keys!
57
+
58
+ Plan.transaction do
59
+ assignables.each do |a|
60
+ tasks << Task.create! do |t|
61
+ t.name = name
62
+ t.data = { x: data[:x], y: data[:y] }
63
+ t.task_type = plan_type
64
+ t.deadline = calculate_deadline(plan_type, data)
65
+ t.reminding_at = reminding_at
66
+ t.status = status
67
+ t.finished_at = finished_at
68
+ t.create_assignable(
69
+ assignee_id: a.assignee_id,
70
+ assignee_type: a.assignee_type,
71
+ )
72
+ t.callables = callables
73
+ end
74
+ end
75
+ end
76
+
77
+ update_attributes(last_task_created_at: Time.now)
78
+
79
+ tasks
80
+ end
81
+
82
+ class << self
83
+ # 返回所有需要创建新任务的计划
84
+ #
85
+ # 判断条件:
86
+ # 1) `enabled_at`在当前时间之前;
87
+ # 2) `last_task_created_at`在周期内。
88
+ # 2.1) 日计划:`Time.now.beginning_of_day <= last_task_created_at <= Time.now.end_of_day`
89
+ # 2.2) 周计划:`Time.now.beginning_of_week <= last_task_created_at <= Time.now.end_of_week`
90
+ # 2.3) 月计划:`Time.now.beginning_of_month <= last_task_created_at <= Time.now.end_of_month`
91
+ # 2.4) 季计划:`Time.now.beginning_of_quarter <= last_task_created_at <= Time.now.end_of_quarter`
92
+ # 2.5) 年计划:`Time.now.beginning_of_year <= last_task_created_at <= Time.now.end_of_year`
93
+ def active
94
+ active_by_type(:daily) | active_by_type(:weekly) |
95
+ active_by_type(:monthly) | active_by_type(:quarterly) |
96
+ active_by_type(:yearly)
97
+ end
98
+
99
+ def active_by_type(type)
100
+ now = Time.now
101
+ beginning_at = case type.to_sym
102
+ when :daily then now.beginning_of_day
103
+ when :weekly then now.beginning_of_week
104
+ when :monthly then now.beginning_of_month
105
+ when :quarterly then now.beginning_of_quarter
106
+ when :yearly then now.beginning_of_year
107
+ end
108
+
109
+ where("plan_type = ? AND last_task_created_at <= ?",
110
+ type, beginning_at).where("enabled_at <= ?", now)
111
+ end
112
+ end
113
+ end
114
+ end