task-manager 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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