task-manager 0.1.2 → 0.1.3
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 +136 -1
- data/Rakefile +3 -17
- data/app/assets/javascripts/task-manager/{application.js → extjs.js} +3 -9
- data/app/assets/javascripts/task-manager/extjs/app/controller/Plans.js +29 -264
- data/app/assets/javascripts/task-manager/extjs/app/helper/{application_helper.js → ApplicationHelper.js} +0 -0
- data/app/assets/javascripts/task-manager/extjs/app/model/Assignee.js +16 -0
- data/app/assets/javascripts/task-manager/extjs/app/model/Plan.js +18 -156
- data/app/assets/javascripts/task-manager/extjs/app/store/Assignees.js +37 -30
- data/app/assets/javascripts/task-manager/extjs/app/view/assignee/TreeCombo.js +48 -0
- data/app/assets/javascripts/task-manager/extjs/app/view/plan/Form.js +290 -0
- data/app/assets/javascripts/task-manager/extjs/app/view/plan/{Window.js → FormWindow.js} +7 -5
- data/app/assets/javascripts/task-manager/extjs/lib/ux/Model.js +14 -0
- data/app/assets/javascripts/task-manager/extjs/lib/ux/TreeCombo.js +276 -0
- data/app/controllers/task_manager/api/v1/plans_controller.rb +3 -3
- data/app/controllers/task_manager/api/v1/tasks_controller.rb +1 -1
- data/app/models/task_manager/plan.rb +48 -37
- data/lib/task-manager/deadline_validator.rb +23 -14
- data/lib/task-manager/version.rb +1 -1
- metadata +27 -16
- data/app/assets/javascripts/task-manager/extjs/app/store/AssigneesTree.js +0 -3
- data/app/assets/javascripts/task-manager/extjs/app/view/plan/AssignablesWindow.js +0 -23
- data/app/assets/javascripts/task-manager/extjs/app/view/plan/Edit.js +0 -189
- data/app/assets/javascripts/task-manager/extjs/app/view/plan/EditWindow.js +0 -21
- data/app/assets/javascripts/task-manager/extjs/app/view/plan/New.js +0 -190
- data/app/assets/javascripts/task-manager/extjs/app/view/plan/SelectAssignables.js +0 -22
- data/app/assets/javascripts/task-manager/extjs/app/view/plan/SelectAssignablesGrid.js +0 -22
- data/app/assets/javascripts/task-manager/extjs/app/view/plan/SelectAssignablesTree.js +0 -9
- data/app/assets/stylesheets/task-manager/application.css.scss +0 -13
@@ -1,9 +1,11 @@
|
|
1
|
-
Ext.define('TM.view.plan.
|
1
|
+
Ext.define('TM.view.plan.FormWindow', {
|
2
2
|
extend: 'Ext.window.Window',
|
3
|
-
|
4
|
-
|
3
|
+
xtype: 'plan_formwindow',
|
4
|
+
|
5
|
+
requires: [
|
6
|
+
'TM.view.plan.Form'
|
7
|
+
],
|
5
8
|
|
6
|
-
title: '添加计划',
|
7
9
|
modal: true,
|
8
10
|
|
9
11
|
width: 600,
|
@@ -15,7 +17,7 @@ Ext.define('TM.view.plan.Window', {
|
|
15
17
|
},
|
16
18
|
|
17
19
|
items: [{
|
18
|
-
xtype: '
|
20
|
+
xtype: 'plan_form',
|
19
21
|
border: false
|
20
22
|
}]
|
21
23
|
});
|
@@ -0,0 +1,14 @@
|
|
1
|
+
Ext.define('Ext.ux.Model', {
|
2
|
+
override: 'Ext.data.Model',
|
3
|
+
|
4
|
+
getPersistData: function() {
|
5
|
+
var data = {};
|
6
|
+
Ext.Array.each(this.fields.items, function(f) {
|
7
|
+
if(f.persist) {
|
8
|
+
data[f.name] = this.get(f.name);
|
9
|
+
}
|
10
|
+
}, this);
|
11
|
+
|
12
|
+
return data;
|
13
|
+
}
|
14
|
+
});
|
@@ -0,0 +1,276 @@
|
|
1
|
+
/*
|
2
|
+
Tree combo
|
3
|
+
Use with 'Ext.data.TreeStore'
|
4
|
+
|
5
|
+
If store root note has 'checked' property tree combo becomes multiselect combo (tree store must have records with 'checked' property)
|
6
|
+
|
7
|
+
Has event 'itemclick' that can be used to capture click
|
8
|
+
|
9
|
+
Options:
|
10
|
+
selectChildren - if set true and if store isn't multiselect, clicking on an non-leaf node selects all it's children
|
11
|
+
canSelectFolders - if set true and store isn't multiselect clicking on a folder selects that folder also as a value
|
12
|
+
|
13
|
+
Use:
|
14
|
+
|
15
|
+
single leaf node selector:
|
16
|
+
selectChildren: false
|
17
|
+
canSelectFolders: false
|
18
|
+
- this will select only leaf nodes and will not allow selecting non-leaf nodes
|
19
|
+
|
20
|
+
single node selector (can select leaf and non-leaf nodes)
|
21
|
+
selectChildren: false
|
22
|
+
canSelectFolders: true
|
23
|
+
- this will select single value either leaf or non-leaf
|
24
|
+
|
25
|
+
children selector:
|
26
|
+
selectChildren: true
|
27
|
+
canSelectFolders: true
|
28
|
+
- clicking on a node will select it's children and node, clicking on a leaf node will select only that node
|
29
|
+
|
30
|
+
This config:
|
31
|
+
selectChildren: true
|
32
|
+
canSelectFolders: false
|
33
|
+
- is invalid, you cannot select children without node
|
34
|
+
|
35
|
+
*/
|
36
|
+
Ext.define('Ext.ux.TreeCombo',
|
37
|
+
{
|
38
|
+
extend: 'Ext.form.field.Picker',
|
39
|
+
alias: 'widget.treecombo',
|
40
|
+
tree: false,
|
41
|
+
constructor: function(config)
|
42
|
+
{
|
43
|
+
this.addEvents(
|
44
|
+
{
|
45
|
+
"itemclick" : true
|
46
|
+
});
|
47
|
+
|
48
|
+
this.listeners = config.listeners;
|
49
|
+
this.callParent(arguments);
|
50
|
+
},
|
51
|
+
records: [],
|
52
|
+
recursiveRecords: [],
|
53
|
+
selectChildren: true,
|
54
|
+
canSelectFolders: true,
|
55
|
+
multiselect: false,
|
56
|
+
displayField: 'text',
|
57
|
+
valueField: 'id',
|
58
|
+
recursivePush: function(node)
|
59
|
+
{
|
60
|
+
var me = this;
|
61
|
+
me.recursiveRecords.push(node);
|
62
|
+
|
63
|
+
node.eachChild(function(nodesingle)
|
64
|
+
{
|
65
|
+
if(nodesingle.hasChildNodes() == true)
|
66
|
+
{
|
67
|
+
me.recursivePush(nodesingle);
|
68
|
+
}
|
69
|
+
else me.recursiveRecords.push(nodesingle);
|
70
|
+
});
|
71
|
+
},
|
72
|
+
recursiveUnPush: function(node)
|
73
|
+
{
|
74
|
+
var me = this;
|
75
|
+
Ext.Array.remove(me.records, node);
|
76
|
+
|
77
|
+
node.eachChild(function(nodesingle)
|
78
|
+
{
|
79
|
+
if(nodesingle.hasChildNodes() == true)
|
80
|
+
{
|
81
|
+
me.recursiveUnPush(nodesingle);
|
82
|
+
}
|
83
|
+
else Ext.Array.remove(me.records, nodesingle);
|
84
|
+
});
|
85
|
+
},
|
86
|
+
afterLoadSetValue: false,
|
87
|
+
setValue: function(valueInit)
|
88
|
+
{
|
89
|
+
if(typeof valueInit == 'undefined') return;
|
90
|
+
|
91
|
+
var me = this,
|
92
|
+
tree = this.tree,
|
93
|
+
value = valueInit.split(',');
|
94
|
+
|
95
|
+
inputEl = me.inputEl;
|
96
|
+
|
97
|
+
if(tree.store.isLoading())
|
98
|
+
{
|
99
|
+
me.afterLoadSetValue = valueInit;
|
100
|
+
}
|
101
|
+
|
102
|
+
if(inputEl && me.emptyText && !Ext.isEmpty(value))
|
103
|
+
{
|
104
|
+
inputEl.removeCls(me.emptyCls);
|
105
|
+
}
|
106
|
+
|
107
|
+
if(tree == false) return false;
|
108
|
+
|
109
|
+
var node = tree.getRootNode();
|
110
|
+
if(node == null) return false;
|
111
|
+
|
112
|
+
me.recursiveRecords = [];
|
113
|
+
me.recursivePush(node);
|
114
|
+
|
115
|
+
var valueFin = [];
|
116
|
+
var idsFin = [];
|
117
|
+
|
118
|
+
if(me.multiselect == true)
|
119
|
+
{
|
120
|
+
Ext.each(me.recursiveRecords, function(record)
|
121
|
+
{
|
122
|
+
record.set('checked', false);
|
123
|
+
});
|
124
|
+
}
|
125
|
+
|
126
|
+
me.records = [];
|
127
|
+
Ext.each(me.recursiveRecords, function(record)
|
128
|
+
{
|
129
|
+
var data = record.get(me.valueField);
|
130
|
+
Ext.each(value, function(val)
|
131
|
+
{
|
132
|
+
if(data == val)
|
133
|
+
{
|
134
|
+
valueFin.push(record.get(me.displayField));
|
135
|
+
idsFin.push(data);
|
136
|
+
if(me.multiselect == true) record.set('checked', true);
|
137
|
+
me.records.push(record);
|
138
|
+
}
|
139
|
+
});
|
140
|
+
});
|
141
|
+
|
142
|
+
me.value = valueInit;
|
143
|
+
me.setRawValue(valueFin.join(', '));
|
144
|
+
|
145
|
+
me.checkChange();
|
146
|
+
me.applyEmptyText();
|
147
|
+
return me;
|
148
|
+
},
|
149
|
+
getValue: function()
|
150
|
+
{
|
151
|
+
return this.value;
|
152
|
+
},
|
153
|
+
getSubmitValue: function()
|
154
|
+
{
|
155
|
+
return this.value;
|
156
|
+
},
|
157
|
+
checkParentNodes: function(node)
|
158
|
+
{
|
159
|
+
if(node == null) return;
|
160
|
+
|
161
|
+
var me = this,
|
162
|
+
checkedAll = true,
|
163
|
+
ids = [];
|
164
|
+
|
165
|
+
Ext.each(me.records, function(value)
|
166
|
+
{
|
167
|
+
ids.push(value.get(me.valueField));
|
168
|
+
});
|
169
|
+
|
170
|
+
node.eachChild(function(nodesingle)
|
171
|
+
{
|
172
|
+
if(!Ext.Array.contains(ids, nodesingle.get(me.valueField))) checkedAll = false;
|
173
|
+
});
|
174
|
+
|
175
|
+
if(checkedAll == true)
|
176
|
+
{
|
177
|
+
me.records.push(node);
|
178
|
+
me.checkParentNodes(node.parentNode);
|
179
|
+
}
|
180
|
+
else
|
181
|
+
{
|
182
|
+
Ext.Array.remove(me.records, node);
|
183
|
+
me.checkParentNodes(node.parentNode);
|
184
|
+
}
|
185
|
+
},
|
186
|
+
initComponent: function()
|
187
|
+
{
|
188
|
+
var me = this;
|
189
|
+
|
190
|
+
me.tree = Ext.create('Ext.tree.Panel',
|
191
|
+
{
|
192
|
+
alias: 'widget.assetstree',
|
193
|
+
hidden: true,
|
194
|
+
minHeight: 300,
|
195
|
+
rootVisible: (typeof me.rootVisible != 'undefined') ? me.rootVisible : true,
|
196
|
+
floating: true,
|
197
|
+
useArrows: true,
|
198
|
+
store: me.store,
|
199
|
+
listeners:
|
200
|
+
{
|
201
|
+
load: function(store, records)
|
202
|
+
{
|
203
|
+
if(me.afterLoadSetValue != false)
|
204
|
+
{
|
205
|
+
me.setValue(me.afterLoadSetValue);
|
206
|
+
}
|
207
|
+
},
|
208
|
+
itemclick: function(view, record, item, index, e, eOpts)
|
209
|
+
{
|
210
|
+
var values = [];
|
211
|
+
|
212
|
+
var node = me.tree.getRootNode().findChild('id', record.get(me.valueField), true);
|
213
|
+
if(node == null)
|
214
|
+
{
|
215
|
+
if(me.tree.getRootNode().get(me.valueField) == record.get(me.valueField)) node = me.tree.getRootNode();
|
216
|
+
else return false;
|
217
|
+
}
|
218
|
+
|
219
|
+
if(me.multiselect == false) me.records = [];
|
220
|
+
|
221
|
+
if(me.canSelectFolders == false && record.get('leaf') == false) return false;
|
222
|
+
if(record.get('leaf') == true || me.selectChildren == false)
|
223
|
+
{
|
224
|
+
if(me.multiselect == false) me.records.push(record);
|
225
|
+
else
|
226
|
+
{
|
227
|
+
if(record.get('checked') == false) me.records.push(record);
|
228
|
+
else Ext.Array.remove(me.records, record);
|
229
|
+
}
|
230
|
+
}
|
231
|
+
else
|
232
|
+
{
|
233
|
+
me.recursiveRecords = [];
|
234
|
+
if(me.multiselect == false || record.get('checked') == false)
|
235
|
+
{
|
236
|
+
me.recursivePush(node);
|
237
|
+
Ext.each(me.recursiveRecords, function(value)
|
238
|
+
{
|
239
|
+
if(!Ext.Array.contains(me.records, value)) me.records.push(value);
|
240
|
+
});
|
241
|
+
}
|
242
|
+
else if(record.get('checked') == true)
|
243
|
+
{
|
244
|
+
me.recursiveUnPush(node);
|
245
|
+
}
|
246
|
+
}
|
247
|
+
|
248
|
+
if(me.canSelectFolders == true) me.checkParentNodes(node.parentNode);
|
249
|
+
|
250
|
+
Ext.each(me.records, function(record)
|
251
|
+
{
|
252
|
+
values.push(record.get(me.valueField));
|
253
|
+
});
|
254
|
+
|
255
|
+
me.setValue(values.join(','));
|
256
|
+
|
257
|
+
me.fireEvent('itemclick', me, record, item, index, e, eOpts, me.records, values);
|
258
|
+
|
259
|
+
if(me.multiselect == false) me.onTriggerClick();
|
260
|
+
}
|
261
|
+
}
|
262
|
+
});
|
263
|
+
|
264
|
+
if(me.tree.getRootNode().get('checked') != null) me.multiselect = true;
|
265
|
+
|
266
|
+
this.createPicker = function()
|
267
|
+
{
|
268
|
+
var me = this;
|
269
|
+
return me.tree;
|
270
|
+
};
|
271
|
+
|
272
|
+
this.callParent(arguments);
|
273
|
+
}
|
274
|
+
});
|
275
|
+
|
276
|
+
//@ sourceURL=/ux/TreeCombo.js
|
@@ -47,7 +47,7 @@ module TaskManager
|
|
47
47
|
# }, ...]
|
48
48
|
# }
|
49
49
|
def index
|
50
|
-
plans = TaskManager::Plan.search(params[:q]).result
|
50
|
+
plans = TaskManager::Plan.search(params[:q]).result.order('id DESC')
|
51
51
|
result = {
|
52
52
|
total: plans.count,
|
53
53
|
plans: ActiveModel::ArraySerializer.new(
|
@@ -84,7 +84,7 @@ module TaskManager
|
|
84
84
|
# "deadline_minute": ... # 必填
|
85
85
|
# },
|
86
86
|
# "enabled_at": ..., # 必填
|
87
|
-
# "begin_to_remind": ..., #
|
87
|
+
# "begin_to_remind": ..., # 必填且大于等于0
|
88
88
|
# "autocompletable": ..., # 必填,缺省值为false
|
89
89
|
# "assignables_attributes": [{ # 至少需要一个assignable
|
90
90
|
# "assignee_id": ...,
|
@@ -162,7 +162,7 @@ module TaskManager
|
|
162
162
|
# "deadline_minute": ... # 必填
|
163
163
|
# },
|
164
164
|
# "enabled_at": ..., # 必填
|
165
|
-
# "begin_to_remind": ..., #
|
165
|
+
# "begin_to_remind": ..., # 必填且大于等于0
|
166
166
|
# "autocompletable": ..., # 必填,缺省值为false
|
167
167
|
#
|
168
168
|
# "assignables_attributes": [{
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
2
|
module TaskManager
|
3
|
-
# TODO 验证data的合法性
|
4
3
|
class Plan < ActiveRecord::Base
|
5
4
|
include TaskManager::DeadlineCalculator
|
6
5
|
extend Enumerize
|
@@ -24,7 +23,7 @@ module TaskManager
|
|
24
23
|
validates_numericality_of :begin_to_remind, greater_than_or_equal_to: 0
|
25
24
|
|
26
25
|
# validate the deadline
|
27
|
-
validates :data, deadline: true
|
26
|
+
validates :data, presence: true, deadline: true
|
28
27
|
|
29
28
|
def assignees
|
30
29
|
assignables.collect(&:assignee)
|
@@ -38,39 +37,10 @@ module TaskManager
|
|
38
37
|
#
|
39
38
|
# 为每一个计划的执行者创建一个计划任务。
|
40
39
|
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
40
|
tasks = []
|
56
|
-
data.symbolize_keys!
|
57
|
-
|
58
41
|
Plan.transaction do
|
59
42
|
assignables.each do |a|
|
60
|
-
tasks <<
|
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
|
43
|
+
tasks << generate_task_for_assignable(a)
|
74
44
|
end
|
75
45
|
end
|
76
46
|
|
@@ -79,6 +49,43 @@ module TaskManager
|
|
79
49
|
tasks
|
80
50
|
end
|
81
51
|
|
52
|
+
def generate_task_for_assignable(a)
|
53
|
+
data.symbolize_keys!
|
54
|
+
|
55
|
+
reminding_at = default_deadline.ago(begin_to_remind * 60)
|
56
|
+
if autocompletable
|
57
|
+
status, finished_at = :finished, Time.now
|
58
|
+
else
|
59
|
+
status, finished_at = :new, nil
|
60
|
+
end
|
61
|
+
|
62
|
+
Task.create! do |t|
|
63
|
+
t.name = name
|
64
|
+
t.data = { x: data[:x], y: data[:y] }
|
65
|
+
t.task_type = plan_type
|
66
|
+
t.deadline = calculate_deadline(plan_type, data)
|
67
|
+
t.reminding_at = reminding_at
|
68
|
+
t.status = status
|
69
|
+
t.finished_at = finished_at
|
70
|
+
t.create_assignable(
|
71
|
+
assignee_id: a.assignee_id,
|
72
|
+
assignee_type: a.assignee_type,
|
73
|
+
)
|
74
|
+
t.callables = callables
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def default_deadline
|
79
|
+
now = Time.now
|
80
|
+
case plan_type.to_sym
|
81
|
+
when :daily then now.end_of_day
|
82
|
+
when :weekly then now.end_of_week
|
83
|
+
when :monthly then now.end_of_month
|
84
|
+
when :quarterly then now.end_of_quarter
|
85
|
+
when :yearly then now.end_of_year
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
82
89
|
class << self
|
83
90
|
# 返回所有需要创建新任务的计划
|
84
91
|
#
|
@@ -91,9 +98,14 @@ module TaskManager
|
|
91
98
|
# 2.4) 季计划:`Time.now.beginning_of_quarter <= last_task_created_at <= Time.now.end_of_quarter`
|
92
99
|
# 2.5) 年计划:`Time.now.beginning_of_year <= last_task_created_at <= Time.now.end_of_year`
|
93
100
|
def active
|
94
|
-
|
95
|
-
|
96
|
-
|
101
|
+
where{
|
102
|
+
sift(:active_by_type, 'daily') |
|
103
|
+
sift(:active_by_type, 'weekly') |
|
104
|
+
sift(:active_by_type, 'monthly') |
|
105
|
+
sift(:active_by_type, 'quarterly') |
|
106
|
+
sift(:active_by_type, 'yearly')
|
107
|
+
|
108
|
+
}
|
97
109
|
end
|
98
110
|
|
99
111
|
def active_by_type(type)
|
@@ -106,8 +118,7 @@ module TaskManager
|
|
106
118
|
when :yearly then now.beginning_of_year
|
107
119
|
end
|
108
120
|
|
109
|
-
|
110
|
-
type, beginning_at).where("enabled_at <= ?", now)
|
121
|
+
squeel{ (plan_type == type) & (last_task_created_at <= beginning_at) & (enabled_at <= now) }
|
111
122
|
end
|
112
123
|
end
|
113
124
|
end
|