cellect-server 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/cellect/server/adapters/default.rb +15 -15
- data/lib/cellect/server/adapters/postgres.rb +6 -6
- data/lib/cellect/server/api/helpers.rb +2 -2
- data/lib/cellect/server/api/sets.rb +3 -3
- data/lib/cellect/server/api/users.rb +2 -2
- data/lib/cellect/server/api.rb +6 -6
- data/lib/cellect/server/{grouped_project.rb → grouped_workflow.rb} +1 -1
- data/lib/cellect/server/node_set.rb +3 -2
- data/lib/cellect/server/user.rb +8 -8
- data/lib/cellect/server/{project.rb → workflow.rb} +6 -6
- data/lib/cellect/server.rb +4 -4
- data/lib/cellect/version.rb +1 -1
- data/spec/fixtures/{project_data → workflow_data}/grouped_pairwise_priority.json +0 -0
- data/spec/fixtures/{project_data → workflow_data}/grouped_pairwise_random.json +0 -0
- data/spec/fixtures/{project_data → workflow_data}/grouped_priority.json +0 -0
- data/spec/fixtures/{project_data → workflow_data}/grouped_random.json +0 -0
- data/spec/fixtures/{project_data → workflow_data}/pairwise_priority.json +0 -0
- data/spec/fixtures/{project_data → workflow_data}/pairwise_random.json +0 -0
- data/spec/fixtures/{project_data → workflow_data}/priority.json +0 -0
- data/spec/fixtures/{project_data → workflow_data}/random.json +0 -0
- data/spec/server/api/add_seen_spec.rb +8 -8
- data/spec/server/api/add_spec.rb +14 -14
- data/spec/server/api/remove_spec.rb +9 -9
- data/spec/server/api/sample_spec.rb +8 -8
- data/spec/server/api/user_load_spec.rb +7 -7
- data/spec/server/grouped_workflow_spec.rb +76 -0
- data/spec/server/server_spec.rb +9 -9
- data/spec/server/user_spec.rb +5 -5
- data/spec/server/workflow_spec.rb +62 -0
- data/spec/support/{shared_examples_for_project.rb → shared_examples_for_workflow.rb} +3 -3
- data/spec/support/spec_adapter.rb +6 -6
- metadata +26 -26
- data/spec/server/grouped_project_spec.rb +0 -76
- data/spec/server/project_spec.rb +0 -62
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cd1ff2b4c25231cf18b03a8a558077a22915881c
|
4
|
+
data.tar.gz: 926b9cc3da11fcb74cf25994377d566ef2a9c1d2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 80c13d8c484811d77c62afbe567b966df465a4aac0f0f7e885711a2ed21e1e4afbdfab51d654b0242048c4b01ee70bded2b2de39da3846f61c160f93c21b3171
|
7
|
+
data.tar.gz: 9506e44c1991d879f7282fd9129222e49e5dca854619b4dbf06123c17fa2679a219e28ed9ff74cca3cabe88ea2d3942df62b0cdbf13117c8e580dd5ebe97448e
|
@@ -2,7 +2,7 @@ module Cellect
|
|
2
2
|
module Server
|
3
3
|
module Adapters
|
4
4
|
class Default
|
5
|
-
# Return a list of
|
5
|
+
# Return a list of workflows to load in the form:
|
6
6
|
# [{
|
7
7
|
# 'id' => 123,
|
8
8
|
# 'name' => 'foo',
|
@@ -10,49 +10,49 @@ module Cellect
|
|
10
10
|
# 'pairwise' => false,
|
11
11
|
# 'grouped' => false
|
12
12
|
# }, ...]
|
13
|
-
def
|
13
|
+
def workflow_list
|
14
14
|
raise NotImplementedError
|
15
15
|
end
|
16
16
|
|
17
|
-
# Load the data for a
|
18
|
-
# Accepts a
|
17
|
+
# Load the data for a workflow, this method:
|
18
|
+
# Accepts a workflow
|
19
19
|
# Returns an array of hashes in the form:
|
20
20
|
# {
|
21
21
|
# 'id' => 123,
|
22
22
|
# 'priority' => 0.123,
|
23
23
|
# 'group_id' => 456
|
24
24
|
# }
|
25
|
-
def load_data_for(
|
25
|
+
def load_data_for(workflow_name)
|
26
26
|
raise NotImplementedError
|
27
27
|
end
|
28
28
|
|
29
29
|
# Load seen ids for a user, this method:
|
30
|
-
# Accepts a
|
30
|
+
# Accepts a workflow_name, and a user id
|
31
31
|
# Returns an array in the form:
|
32
32
|
# [1, 2, 3]
|
33
|
-
def load_user(
|
33
|
+
def load_user(workflow_name, id)
|
34
34
|
raise NotImplementedError
|
35
35
|
end
|
36
36
|
|
37
|
-
def
|
38
|
-
|
37
|
+
def load_workflows
|
38
|
+
workflow_list.each{ |workflow_info| load_workflow workflow_info }
|
39
39
|
end
|
40
40
|
|
41
|
-
def
|
41
|
+
def load_workflow(args)
|
42
42
|
info = if args.is_a?(Hash)
|
43
43
|
args
|
44
44
|
elsif args.is_a?(String)
|
45
|
-
|
45
|
+
workflow_list.select{ |h| h['name'] == args }.first
|
46
46
|
else
|
47
47
|
raise ArgumentError
|
48
48
|
end
|
49
49
|
|
50
|
-
|
50
|
+
workflow_for info
|
51
51
|
end
|
52
52
|
|
53
|
-
def
|
54
|
-
|
55
|
-
|
53
|
+
def workflow_for(opts = { })
|
54
|
+
workflow_klass = opts.fetch('grouped', false) ? GroupedWorkflow : Workflow
|
55
|
+
workflow_klass[opts['name'], pairwise: opts['pairwise'], prioritized: opts['prioritized']]
|
56
56
|
end
|
57
57
|
end
|
58
58
|
end
|
@@ -11,9 +11,9 @@ module Cellect
|
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
|
-
def
|
14
|
+
def workflow_list
|
15
15
|
with_pg do |pg|
|
16
|
-
pg.exec('select * from
|
16
|
+
pg.exec('select * from workflows').collect do |row|
|
17
17
|
{
|
18
18
|
'id' => row['id'].to_i,
|
19
19
|
'name' => row['id'],
|
@@ -25,9 +25,9 @@ module Cellect
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
|
-
def load_data_for(
|
28
|
+
def load_data_for(workflow_name)
|
29
29
|
with_pg do |pg|
|
30
|
-
pg.exec("select id, priority, group_id from
|
30
|
+
pg.exec("select id, priority, group_id from workflow_#{ workflow_name }_subjects").collect do |row|
|
31
31
|
{
|
32
32
|
'id' => row['id'].to_i,
|
33
33
|
'priority' => row['priority'].to_f,
|
@@ -37,9 +37,9 @@ module Cellect
|
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
40
|
-
def load_user(
|
40
|
+
def load_user(workflow_name, id)
|
41
41
|
with_pg do |pg|
|
42
|
-
pg.exec("select subject_id from
|
42
|
+
pg.exec("select subject_id from workflow_#{ workflow_name }_classifications where user_id=#{ id }").collect do |row|
|
43
43
|
row['subject_id'].to_i
|
44
44
|
end
|
45
45
|
end
|
@@ -3,16 +3,16 @@ module Cellect
|
|
3
3
|
class API
|
4
4
|
class Sets < Grape::API
|
5
5
|
get do
|
6
|
-
|
6
|
+
workflow.sample selector_params
|
7
7
|
end
|
8
8
|
|
9
9
|
put :add do
|
10
|
-
|
10
|
+
workflow.add update_params
|
11
11
|
nil
|
12
12
|
end
|
13
13
|
|
14
14
|
put :remove do
|
15
|
-
|
15
|
+
workflow.remove update_params
|
16
16
|
nil
|
17
17
|
end
|
18
18
|
end
|
@@ -9,7 +9,7 @@ module Cellect
|
|
9
9
|
subject_id = param_to_int :subject_id
|
10
10
|
|
11
11
|
if user_id && user_id > 0 && subject_id && subject_id > 0
|
12
|
-
|
12
|
+
workflow.async.add_seen_for user_id, subject_id
|
13
13
|
end
|
14
14
|
|
15
15
|
nil
|
@@ -19,7 +19,7 @@ module Cellect
|
|
19
19
|
user_id = param_to_int :user_id
|
20
20
|
|
21
21
|
if user_id && user_id > 0
|
22
|
-
|
22
|
+
workflow.async.user user_id
|
23
23
|
end
|
24
24
|
|
25
25
|
nil
|
data/lib/cellect/server/api.rb
CHANGED
@@ -20,26 +20,26 @@ module Cellect
|
|
20
20
|
}
|
21
21
|
end
|
22
22
|
|
23
|
-
resources :
|
23
|
+
resources :workflows do
|
24
24
|
get do
|
25
|
-
Cellect::Server.adapter.
|
25
|
+
Cellect::Server.adapter.workflow_list
|
26
26
|
end
|
27
27
|
|
28
|
-
segment '/:
|
28
|
+
segment '/:workflow_id' do
|
29
29
|
helpers Helpers
|
30
30
|
mount Sets
|
31
31
|
mount Users
|
32
32
|
|
33
33
|
get :status do
|
34
|
-
|
34
|
+
workflow.status
|
35
35
|
end
|
36
36
|
|
37
37
|
post :reload do
|
38
|
-
|
38
|
+
workflow.async.load_data
|
39
39
|
end
|
40
40
|
|
41
41
|
delete do
|
42
|
-
# delete a
|
42
|
+
# delete a workflow (maybe?)
|
43
43
|
end
|
44
44
|
end
|
45
45
|
end
|
@@ -10,8 +10,9 @@ module Cellect
|
|
10
10
|
|
11
11
|
def setup
|
12
12
|
zk.mkdir_p '/nodes'
|
13
|
-
|
14
|
-
|
13
|
+
address = Socket.ip_address_list.find{ |address| address.ipv4? && !address.ipv4_loopback? }
|
14
|
+
raise "Cannot identify IP address" unless address
|
15
|
+
path = zk.create '/nodes/node', data: address.ip_address, mode: :ephemeral_sequential
|
15
16
|
self.id = path.sub /^\/nodes\//, ''
|
16
17
|
end
|
17
18
|
end
|
data/lib/cellect/server/user.rb
CHANGED
@@ -4,23 +4,23 @@ module Cellect
|
|
4
4
|
include Celluloid
|
5
5
|
include Celluloid::Logger
|
6
6
|
|
7
|
-
trap_exit :
|
7
|
+
trap_exit :workflow_crashed
|
8
8
|
finalizer :cancel_ttl_timer
|
9
9
|
|
10
|
-
attr_accessor :id, :
|
10
|
+
attr_accessor :id, :workflow_name, :seen, :state
|
11
11
|
attr_accessor :ttl, :ttl_timer
|
12
12
|
|
13
|
-
def initialize(id,
|
13
|
+
def initialize(id, workflow_name: nil, ttl: nil)
|
14
14
|
self.id = id
|
15
|
-
self.
|
15
|
+
self.workflow_name = workflow_name
|
16
16
|
self.seen = DiffSet::RandomSet.new
|
17
|
-
monitor
|
17
|
+
monitor Workflow[workflow_name]
|
18
18
|
@ttl = ttl
|
19
19
|
load_data
|
20
20
|
end
|
21
21
|
|
22
22
|
def load_data
|
23
|
-
data = Cellect::Server.adapter.load_user(
|
23
|
+
data = Cellect::Server.adapter.load_user(workflow_name, id) || []
|
24
24
|
data.each do |subject_id|
|
25
25
|
@seen.add subject_id
|
26
26
|
end
|
@@ -50,14 +50,14 @@ module Cellect
|
|
50
50
|
def ttl_expired!
|
51
51
|
debug "User #{ id } TTL expired"
|
52
52
|
cancel_ttl_timer
|
53
|
-
|
53
|
+
Workflow[workflow_name].async.remove_user(id)
|
54
54
|
end
|
55
55
|
|
56
56
|
def ttl
|
57
57
|
@ttl || 60 * 15 # 15 minutes
|
58
58
|
end
|
59
59
|
|
60
|
-
def
|
60
|
+
def workflow_crashed(actor, reason)
|
61
61
|
cancel_ttl_timer
|
62
62
|
terminate
|
63
63
|
end
|
@@ -1,25 +1,25 @@
|
|
1
1
|
module Cellect
|
2
2
|
module Server
|
3
|
-
class
|
3
|
+
class Workflow
|
4
4
|
include Celluloid
|
5
5
|
|
6
6
|
attr_accessor :name, :users, :subjects, :state
|
7
7
|
attr_accessor :pairwise, :prioritized
|
8
8
|
|
9
9
|
def self.[](name, pairwise: false, prioritized: false)
|
10
|
-
key = "
|
10
|
+
key = "workflow_#{ name }".to_sym
|
11
11
|
Actor[key] ||= supervise name, pairwise: pairwise, prioritized: prioritized
|
12
12
|
Actor[key].actors.first
|
13
13
|
end
|
14
14
|
|
15
15
|
def self.names
|
16
16
|
actor_names = Celluloid.actor_system.registry.names.collect &:to_s
|
17
|
-
|
18
|
-
|
17
|
+
workflow_actors = actor_names.select{ |key| key =~ /^workflow_/ }
|
18
|
+
workflow_actors.collect{ |name| name.sub(/^workflow_/, '').to_sym }
|
19
19
|
end
|
20
20
|
|
21
21
|
def self.all
|
22
|
-
names.collect{ |name|
|
22
|
+
names.collect{ |name| Workflow[name] }
|
23
23
|
end
|
24
24
|
|
25
25
|
def initialize(name, pairwise: false, prioritized: false)
|
@@ -41,7 +41,7 @@ module Cellect
|
|
41
41
|
end
|
42
42
|
|
43
43
|
def user(id)
|
44
|
-
self.users[id] ||= User.supervise id,
|
44
|
+
self.users[id] ||= User.supervise id, workflow_name: name
|
45
45
|
users[id].actors.first
|
46
46
|
end
|
47
47
|
|
data/lib/cellect/server.rb
CHANGED
@@ -6,8 +6,8 @@ module Cellect
|
|
6
6
|
module Server
|
7
7
|
require 'cellect/server/node_set'
|
8
8
|
require 'cellect/server/adapters'
|
9
|
-
require 'cellect/server/
|
10
|
-
require 'cellect/server/
|
9
|
+
require 'cellect/server/workflow'
|
10
|
+
require 'cellect/server/grouped_workflow'
|
11
11
|
require 'cellect/server/user'
|
12
12
|
require 'cellect/server/api'
|
13
13
|
|
@@ -16,8 +16,8 @@ module Cellect
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def self.ready?
|
19
|
-
|
20
|
-
return false unless
|
19
|
+
Workflow.all.each do |workflow|
|
20
|
+
return false unless workflow.ready?
|
21
21
|
end
|
22
22
|
|
23
23
|
true
|
data/lib/cellect/version.rb
CHANGED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -7,16 +7,16 @@ module Cellect::Server
|
|
7
7
|
{ 'Ungrouped' => nil, 'Grouped' => 'grouped' }.each_pair do |grouping_type, grouping|
|
8
8
|
SET_TYPES.shuffle.each do |set_type|
|
9
9
|
context "#{ grouping_type } #{ set_type }" do
|
10
|
-
let(:
|
11
|
-
let(:
|
12
|
-
let(:user){
|
13
|
-
before(:each){ pass_until
|
10
|
+
let(:workflow_type){ [grouping, set_type].compact.join '_' }
|
11
|
+
let(:workflow){ Workflow[workflow_type] }
|
12
|
+
let(:user){ workflow.user 123 }
|
13
|
+
before(:each){ pass_until workflow, is: :ready }
|
14
14
|
|
15
15
|
it 'should add seen subjects' do
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
put "/
|
16
|
+
async_workflow = double
|
17
|
+
workflow.should_receive(:async).and_return async_workflow
|
18
|
+
async_workflow.should_receive(:add_seen_for).with 123, 123
|
19
|
+
put "/workflows/#{ workflow_type }/users/123/add_seen", subject_id: 123
|
20
20
|
last_response.status.should == 200
|
21
21
|
end
|
22
22
|
end
|
data/spec/server/api/add_spec.rb
CHANGED
@@ -7,30 +7,30 @@ module Cellect::Server
|
|
7
7
|
{ 'Ungrouped' => nil, 'Grouped' => 'grouped' }.each_pair do |grouping_type, grouping|
|
8
8
|
SET_TYPES.shuffle.each do |set_type|
|
9
9
|
context "#{ grouping_type } #{ set_type }" do
|
10
|
-
let(:
|
11
|
-
let(:
|
12
|
-
let(:user){
|
13
|
-
before(:each){ pass_until
|
10
|
+
let(:workflow_type){ [grouping, set_type].compact.join '_' }
|
11
|
+
let(:workflow){ Workflow[workflow_type] }
|
12
|
+
let(:user){ workflow.user 123 }
|
13
|
+
before(:each){ pass_until workflow, is: :ready }
|
14
14
|
|
15
15
|
let(:opts) do
|
16
16
|
{ subject_id: 123 }.tap do |h|
|
17
|
-
h[:priority] = 456.0 if
|
18
|
-
h[:group_id] = 1 if
|
17
|
+
h[:priority] = 456.0 if workflow.prioritized?
|
18
|
+
h[:group_id] = 1 if workflow.grouped?
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
22
|
it 'should add subjects' do
|
23
|
-
if
|
24
|
-
|
25
|
-
elsif
|
26
|
-
|
27
|
-
elsif
|
28
|
-
|
23
|
+
if workflow.grouped? && workflow.prioritized?
|
24
|
+
workflow.should_receive(:add).with subject_id: 123, group_id: 1, priority: 456.0
|
25
|
+
elsif workflow.grouped?
|
26
|
+
workflow.should_receive(:add).with subject_id: 123, group_id: 1, priority: nil
|
27
|
+
elsif workflow.prioritized?
|
28
|
+
workflow.should_receive(:add).with subject_id: 123, group_id: nil, priority: 456.0
|
29
29
|
else
|
30
|
-
|
30
|
+
workflow.should_receive(:add).with subject_id: 123, group_id: nil, priority: nil
|
31
31
|
end
|
32
32
|
|
33
|
-
put "/
|
33
|
+
put "/workflows/#{ workflow_type }/add", opts
|
34
34
|
last_response.status.should == 200
|
35
35
|
end
|
36
36
|
end
|
@@ -7,25 +7,25 @@ module Cellect::Server
|
|
7
7
|
{ 'Ungrouped' => nil, 'Grouped' => 'grouped' }.each_pair do |grouping_type, grouping|
|
8
8
|
SET_TYPES.shuffle.each do |set_type|
|
9
9
|
context "#{ grouping_type } #{ set_type }" do
|
10
|
-
let(:
|
11
|
-
let(:
|
12
|
-
let(:user){
|
13
|
-
before(:each){ pass_until
|
10
|
+
let(:workflow_type){ [grouping, set_type].compact.join '_' }
|
11
|
+
let(:workflow){ Workflow[workflow_type] }
|
12
|
+
let(:user){ workflow.user 123 }
|
13
|
+
before(:each){ pass_until workflow, is: :ready }
|
14
14
|
|
15
15
|
let(:opts) do
|
16
16
|
{ subject_id: 123 }.tap do |h|
|
17
|
-
h[:group_id] = 1 if
|
17
|
+
h[:group_id] = 1 if workflow.grouped?
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
21
|
it 'should remove subjects' do
|
22
|
-
if
|
23
|
-
|
22
|
+
if workflow.grouped?
|
23
|
+
workflow.should_receive(:remove).with subject_id: 123, group_id: 1, priority: nil
|
24
24
|
else
|
25
|
-
|
25
|
+
workflow.should_receive(:remove).with subject_id: 123, group_id: nil, priority: nil
|
26
26
|
end
|
27
27
|
|
28
|
-
put "/
|
28
|
+
put "/workflows/#{ workflow_type }/remove", opts
|
29
29
|
last_response.status.should == 200
|
30
30
|
end
|
31
31
|
end
|
@@ -7,14 +7,14 @@ module Cellect::Server
|
|
7
7
|
{ 'Ungrouped' => nil, 'Grouped' => 'grouped' }.each_pair do |grouping_type, grouping|
|
8
8
|
SET_TYPES.shuffle.each do |set_type|
|
9
9
|
context "#{ grouping_type } #{ set_type }" do
|
10
|
-
let(:
|
11
|
-
let(:
|
12
|
-
let(:user){
|
13
|
-
before(:each){ pass_until
|
10
|
+
let(:workflow_type){ [grouping, set_type].compact.join '_' }
|
11
|
+
let(:workflow){ Workflow[workflow_type] }
|
12
|
+
let(:user){ workflow.user 123 }
|
13
|
+
before(:each){ pass_until workflow, is: :ready }
|
14
14
|
|
15
15
|
it 'should sample without a user, limit, or group' do
|
16
|
-
|
17
|
-
get "/
|
16
|
+
workflow.should_receive(:sample).with(limit: 5, user_id: nil, group_id: nil).and_call_original
|
17
|
+
get "/workflows/#{ workflow_type }"
|
18
18
|
last_response.status.should == 200
|
19
19
|
json.should be_a Array
|
20
20
|
end
|
@@ -22,8 +22,8 @@ module Cellect::Server
|
|
22
22
|
shoulda = grouping ? 'limit, group, and user' : 'limit and user'
|
23
23
|
it "should sample with a #{ shoulda }" do
|
24
24
|
group_id = grouping ? 1 : nil
|
25
|
-
|
26
|
-
get "/
|
25
|
+
workflow.should_receive(:sample).with(limit: 3, user_id: 123, group_id: group_id).and_call_original
|
26
|
+
get "/workflows/#{ workflow_type }?limit=3&user_id=123#{ grouping ? '&group_id=1' : '' }"
|
27
27
|
last_response.status.should == 200
|
28
28
|
json.should be_a Array
|
29
29
|
end
|
@@ -7,15 +7,15 @@ module Cellect::Server
|
|
7
7
|
{ 'Ungrouped' => nil, 'Grouped' => 'grouped' }.each_pair do |grouping_type, grouping|
|
8
8
|
SET_TYPES.shuffle.each do |set_type|
|
9
9
|
context "#{ grouping_type } #{ set_type }" do
|
10
|
-
let(:
|
11
|
-
let(:
|
12
|
-
before(:each){ pass_until
|
10
|
+
let(:workflow_type){ [grouping, set_type].compact.join '_' }
|
11
|
+
let(:workflow){ Workflow[workflow_type] }
|
12
|
+
before(:each){ pass_until workflow, is: :ready }
|
13
13
|
|
14
14
|
it 'should load users' do
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
post "/
|
15
|
+
async_workflow = double
|
16
|
+
workflow.should_receive(:async).and_return async_workflow
|
17
|
+
async_workflow.should_receive(:user).with 123
|
18
|
+
post "/workflows/#{ workflow_type }/users/123/load"
|
19
19
|
last_response.status.should == 201
|
20
20
|
end
|
21
21
|
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Cellect::Server
|
4
|
+
describe GroupedWorkflow do
|
5
|
+
SET_TYPES.collect{ |type| "grouped_#{ type }" }.each do |workflow_type|
|
6
|
+
context workflow_type do
|
7
|
+
it_behaves_like 'workflow', :workflow
|
8
|
+
let(:workflow){ GroupedWorkflow[workflow_type] }
|
9
|
+
let(:user){ workflow.user 123 }
|
10
|
+
let(:set_klass){ workflow.prioritized? ? DiffSet::PrioritySet : DiffSet::RandomSet }
|
11
|
+
before(:each){ pass_until workflow, is: :ready }
|
12
|
+
|
13
|
+
it 'should provide unseen from a random group for users' do
|
14
|
+
workflow.groups = { }
|
15
|
+
workflow.groups[1] = set_klass.new
|
16
|
+
workflow.groups[1].should_receive(:subtract).with user.seen, 3
|
17
|
+
workflow.unseen_for 123, limit: 3
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should provide unseen from a specific group for users' do
|
21
|
+
3.times{ |i| workflow.groups[i] = set_klass.new }
|
22
|
+
workflow.group(1).should_receive(:subtract).with user.seen, 3
|
23
|
+
workflow.unseen_for 123, group_id: 1, limit: 3
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should sample subjects from a random group without a user' do
|
27
|
+
workflow.groups = { }
|
28
|
+
workflow.groups[1] = set_klass.new
|
29
|
+
workflow.group(1).should_receive(:sample).with 3
|
30
|
+
workflow.sample limit: 3
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should sample subjects from a specific group without a user' do
|
34
|
+
3.times{ |i| workflow.groups[i] = set_klass.new }
|
35
|
+
workflow.group(1).should_receive(:sample).with 3
|
36
|
+
workflow.sample group_id: 1, limit: 3
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should sample subjects from a random group for a user' do
|
40
|
+
workflow.groups = { }
|
41
|
+
workflow.groups[1] = set_klass.new
|
42
|
+
workflow.groups[1].should_receive(:subtract).with user.seen, 3
|
43
|
+
workflow.sample user_id: 123, limit: 3
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should sample subjects from a specific group for a user' do
|
47
|
+
3.times{ |i| workflow.groups[i] = set_klass.new }
|
48
|
+
workflow.group(1).should_receive(:subtract).with user.seen, 3
|
49
|
+
workflow.sample user_id: 123, group_id: 1, limit: 3
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should add subjects' do
|
53
|
+
workflow.groups[1] = set_klass.new
|
54
|
+
|
55
|
+
if workflow.prioritized?
|
56
|
+
workflow.groups[1].should_receive(:add).with 123, 456
|
57
|
+
workflow.add subject_id: 123, group_id: 1, priority: 456
|
58
|
+
else
|
59
|
+
workflow.groups[1].should_receive(:add).with 123
|
60
|
+
workflow.add subject_id: 123, group_id: 1
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should remove subjects' do
|
65
|
+
workflow.groups[1] = set_klass.new
|
66
|
+
workflow.groups[1].should_receive(:remove).with 123
|
67
|
+
workflow.remove subject_id: 123, group_id: 1
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should be grouped' do
|
71
|
+
workflow.should be_grouped
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
data/spec/server/server_spec.rb
CHANGED
@@ -6,18 +6,18 @@ module Cellect::Server
|
|
6
6
|
let(:default){ Cellect::Server::Adapters::Default.new }
|
7
7
|
|
8
8
|
it 'should raise a NotImplementedError when using the default adapter' do
|
9
|
-
expect{ default.
|
10
|
-
expect{ default.load_data_for(
|
9
|
+
expect{ default.workflow_list }.to raise_error NotImplementedError
|
10
|
+
expect{ default.load_data_for(Workflow.new('test')) }.to raise_error NotImplementedError
|
11
11
|
expect{ default.load_user 'random', 123 }.to raise_error NotImplementedError
|
12
12
|
end
|
13
13
|
|
14
|
-
it 'should return a
|
15
|
-
default.
|
16
|
-
default.
|
17
|
-
default.
|
18
|
-
default.
|
19
|
-
default.
|
20
|
-
default.
|
14
|
+
it 'should return a workflow given a set of options' do
|
15
|
+
default.workflow_for('name' => 'a').should be_an_instance_of Workflow
|
16
|
+
default.workflow_for('name' => 'b', 'grouped' => true).should be_an_instance_of GroupedWorkflow
|
17
|
+
default.workflow_for('name' => 'c', 'pairwise' => true).should be_pairwise
|
18
|
+
default.workflow_for('name' => 'd', 'prioritized' => true).should be_prioritized
|
19
|
+
default.workflow_for('name' => 'e', 'pairwise' => true, 'prioritized' => true).should be_pairwise
|
20
|
+
default.workflow_for('name' => 'e', 'pairwise' => true, 'prioritized' => true).should be_prioritized
|
21
21
|
end
|
22
22
|
end
|
23
23
|
end
|
data/spec/server/user_spec.rb
CHANGED
@@ -2,7 +2,7 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
module Cellect::Server
|
4
4
|
describe User do
|
5
|
-
let(:user){ User.new 1,
|
5
|
+
let(:user){ User.new 1, workflow_name: 'random' }
|
6
6
|
|
7
7
|
it 'should store seen ids' do
|
8
8
|
user.seen.should be_a DiffSet::RandomSet
|
@@ -13,7 +13,7 @@ module Cellect::Server
|
|
13
13
|
end
|
14
14
|
|
15
15
|
it 'should allow custom ttl' do
|
16
|
-
User.new(2,
|
16
|
+
User.new(2, workflow_name: 'random', ttl: 123).ttl.should == 123
|
17
17
|
end
|
18
18
|
|
19
19
|
it 'should reset the ttl timer on activity' do
|
@@ -22,9 +22,9 @@ module Cellect::Server
|
|
22
22
|
end
|
23
23
|
|
24
24
|
it 'should terminate on ttl expiry' do
|
25
|
-
|
26
|
-
|
27
|
-
|
25
|
+
async_workflow = double
|
26
|
+
Workflow[user.workflow_name].should_receive(:async).and_return async_workflow
|
27
|
+
async_workflow.should_receive(:remove_user).with user.id
|
28
28
|
user.ttl_expired!
|
29
29
|
user.ttl_timer.should be_nil
|
30
30
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Cellect::Server
|
4
|
+
describe Workflow do
|
5
|
+
SET_TYPES.each do |workflow_type|
|
6
|
+
context workflow_type do
|
7
|
+
it_behaves_like 'workflow', :workflow
|
8
|
+
let(:workflow){ Workflow[workflow_type] }
|
9
|
+
let(:user){ workflow.user 123 }
|
10
|
+
before(:each){ pass_until workflow, is: :ready }
|
11
|
+
|
12
|
+
it 'should provide unseen for users' do
|
13
|
+
workflow.subjects.should_receive(:subtract).with user.seen, 3
|
14
|
+
workflow.unseen_for 123, limit: 3
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should sample subjects without a user' do
|
18
|
+
workflow.subjects.should_receive(:sample).with 3
|
19
|
+
workflow.sample limit: 3
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should sample subjects with a user' do
|
23
|
+
workflow.subjects.should_receive(:subtract).with user.seen, 3
|
24
|
+
workflow.sample user_id: 123, limit: 3
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should add subjects' do
|
28
|
+
if workflow.prioritized?
|
29
|
+
workflow.subjects.should_receive(:add).with 123, 456
|
30
|
+
workflow.add subject_id: 123, priority: 456
|
31
|
+
else
|
32
|
+
workflow.subjects.should_receive(:add).with 123
|
33
|
+
workflow.add subject_id: 123
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should remove subjects' do
|
38
|
+
workflow.subjects.should_receive(:add).with 123
|
39
|
+
workflow.add subject_id: 123
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'should be notified of a user ttl expiry' do
|
43
|
+
async_workflow = double
|
44
|
+
workflow.should_receive(:async).and_return async_workflow
|
45
|
+
async_workflow.should_receive(:remove_user).with user.id
|
46
|
+
user.ttl_expired!
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should remove users when their ttl expires' do
|
50
|
+
id = user.id
|
51
|
+
workflow.remove_user id
|
52
|
+
workflow.users.should_not have_key id
|
53
|
+
expect{ user.id }.to raise_error
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should not be grouped' do
|
57
|
+
workflow.should_not be_grouped
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -1,12 +1,12 @@
|
|
1
|
-
shared_examples_for '
|
1
|
+
shared_examples_for 'workflow' do |name|
|
2
2
|
let(:obj){ send name }
|
3
3
|
|
4
4
|
before(:each) do
|
5
|
-
Cellect::Server.adapter.
|
5
|
+
Cellect::Server.adapter.load_workflow obj.name
|
6
6
|
end
|
7
7
|
|
8
8
|
it 'should add singleton instances to the registry' do
|
9
|
-
obj.class[:foo].should be_a_kind_of Cellect::Server::
|
9
|
+
obj.class[:foo].should be_a_kind_of Cellect::Server::Workflow
|
10
10
|
obj.class[:foo].object_id.should == obj.class[:foo].object_id
|
11
11
|
end
|
12
12
|
|
@@ -1,17 +1,17 @@
|
|
1
1
|
require 'oj'
|
2
2
|
|
3
3
|
class SpecAdapter < Cellect::Server::Adapters::Default
|
4
|
-
def
|
4
|
+
def workflow_list
|
5
5
|
fixtures.values
|
6
6
|
end
|
7
7
|
|
8
|
-
def load_data_for(
|
9
|
-
fixtures.fetch(
|
8
|
+
def load_data_for(workflow_name)
|
9
|
+
fixtures.fetch(workflow_name, { }).fetch 'entries', []
|
10
10
|
end
|
11
11
|
|
12
12
|
def fixtures
|
13
13
|
@fixtures ||= { }.tap do |fixtures|
|
14
|
-
Dir["#{ _fixture_path }/
|
14
|
+
Dir["#{ _fixture_path }/workflow_data/*.json"].collect do |f|
|
15
15
|
name = File.basename(f).sub /\.json$/, ''
|
16
16
|
data = Oj.strict_load File.read f
|
17
17
|
fixtures[name] = data
|
@@ -30,9 +30,9 @@ class SpecAdapter < Cellect::Server::Adapters::Default
|
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
33
|
-
def load_user(
|
33
|
+
def load_user(workflow_name, id)
|
34
34
|
user = user_fixtures[id]
|
35
|
-
user ? user[
|
35
|
+
user ? user[workflow_name] : user_fixtures['new_user'][workflow_name]
|
36
36
|
end
|
37
37
|
|
38
38
|
protected
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cellect-server
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Parrish
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-06-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -245,39 +245,39 @@ files:
|
|
245
245
|
- lib/cellect/server/api/helpers.rb
|
246
246
|
- lib/cellect/server/api/sets.rb
|
247
247
|
- lib/cellect/server/api/users.rb
|
248
|
-
- lib/cellect/server/
|
248
|
+
- lib/cellect/server/grouped_workflow.rb
|
249
249
|
- lib/cellect/server/node_set.rb
|
250
|
-
- lib/cellect/server/project.rb
|
251
250
|
- lib/cellect/server/user.rb
|
251
|
+
- lib/cellect/server/workflow.rb
|
252
252
|
- lib/cellect/version.rb
|
253
253
|
- log/.gitkeep
|
254
|
-
- spec/fixtures/project_data/grouped_pairwise_priority.json
|
255
|
-
- spec/fixtures/project_data/grouped_pairwise_random.json
|
256
|
-
- spec/fixtures/project_data/grouped_priority.json
|
257
|
-
- spec/fixtures/project_data/grouped_random.json
|
258
|
-
- spec/fixtures/project_data/pairwise_priority.json
|
259
|
-
- spec/fixtures/project_data/pairwise_random.json
|
260
|
-
- spec/fixtures/project_data/priority.json
|
261
|
-
- spec/fixtures/project_data/random.json
|
262
254
|
- spec/fixtures/user_data/complete_user.json
|
263
255
|
- spec/fixtures/user_data/new_user.json
|
264
256
|
- spec/fixtures/user_data/partial_user.json
|
257
|
+
- spec/fixtures/workflow_data/grouped_pairwise_priority.json
|
258
|
+
- spec/fixtures/workflow_data/grouped_pairwise_random.json
|
259
|
+
- spec/fixtures/workflow_data/grouped_priority.json
|
260
|
+
- spec/fixtures/workflow_data/grouped_random.json
|
261
|
+
- spec/fixtures/workflow_data/pairwise_priority.json
|
262
|
+
- spec/fixtures/workflow_data/pairwise_random.json
|
263
|
+
- spec/fixtures/workflow_data/priority.json
|
264
|
+
- spec/fixtures/workflow_data/random.json
|
265
265
|
- spec/server/api/add_seen_spec.rb
|
266
266
|
- spec/server/api/add_spec.rb
|
267
267
|
- spec/server/api/remove_spec.rb
|
268
268
|
- spec/server/api/sample_spec.rb
|
269
269
|
- spec/server/api/user_load_spec.rb
|
270
|
-
- spec/server/
|
270
|
+
- spec/server/grouped_workflow_spec.rb
|
271
271
|
- spec/server/node_set_spec.rb
|
272
|
-
- spec/server/project_spec.rb
|
273
272
|
- spec/server/server_spec.rb
|
274
273
|
- spec/server/user_spec.rb
|
274
|
+
- spec/server/workflow_spec.rb
|
275
275
|
- spec/spec_helper.rb
|
276
276
|
- spec/support/cellect_helper.rb
|
277
277
|
- spec/support/shared_api_context.rb
|
278
278
|
- spec/support/shared_examples_for_node_set.rb
|
279
|
-
- spec/support/shared_examples_for_project.rb
|
280
279
|
- spec/support/shared_examples_for_set.rb
|
280
|
+
- spec/support/shared_examples_for_workflow.rb
|
281
281
|
- spec/support/spec_adapter.rb
|
282
282
|
- spec/support/zk_setup.rb
|
283
283
|
- tmp/.gitkeep
|
@@ -306,32 +306,32 @@ signing_key:
|
|
306
306
|
specification_version: 4
|
307
307
|
summary: ''
|
308
308
|
test_files:
|
309
|
-
- spec/fixtures/project_data/grouped_pairwise_priority.json
|
310
|
-
- spec/fixtures/project_data/grouped_pairwise_random.json
|
311
|
-
- spec/fixtures/project_data/grouped_priority.json
|
312
|
-
- spec/fixtures/project_data/grouped_random.json
|
313
|
-
- spec/fixtures/project_data/pairwise_priority.json
|
314
|
-
- spec/fixtures/project_data/pairwise_random.json
|
315
|
-
- spec/fixtures/project_data/priority.json
|
316
|
-
- spec/fixtures/project_data/random.json
|
317
309
|
- spec/fixtures/user_data/complete_user.json
|
318
310
|
- spec/fixtures/user_data/new_user.json
|
319
311
|
- spec/fixtures/user_data/partial_user.json
|
312
|
+
- spec/fixtures/workflow_data/grouped_pairwise_priority.json
|
313
|
+
- spec/fixtures/workflow_data/grouped_pairwise_random.json
|
314
|
+
- spec/fixtures/workflow_data/grouped_priority.json
|
315
|
+
- spec/fixtures/workflow_data/grouped_random.json
|
316
|
+
- spec/fixtures/workflow_data/pairwise_priority.json
|
317
|
+
- spec/fixtures/workflow_data/pairwise_random.json
|
318
|
+
- spec/fixtures/workflow_data/priority.json
|
319
|
+
- spec/fixtures/workflow_data/random.json
|
320
320
|
- spec/server/api/add_seen_spec.rb
|
321
321
|
- spec/server/api/add_spec.rb
|
322
322
|
- spec/server/api/remove_spec.rb
|
323
323
|
- spec/server/api/sample_spec.rb
|
324
324
|
- spec/server/api/user_load_spec.rb
|
325
|
-
- spec/server/
|
325
|
+
- spec/server/grouped_workflow_spec.rb
|
326
326
|
- spec/server/node_set_spec.rb
|
327
|
-
- spec/server/project_spec.rb
|
328
327
|
- spec/server/server_spec.rb
|
329
328
|
- spec/server/user_spec.rb
|
329
|
+
- spec/server/workflow_spec.rb
|
330
330
|
- spec/spec_helper.rb
|
331
331
|
- spec/support/cellect_helper.rb
|
332
332
|
- spec/support/shared_api_context.rb
|
333
333
|
- spec/support/shared_examples_for_node_set.rb
|
334
|
-
- spec/support/shared_examples_for_project.rb
|
335
334
|
- spec/support/shared_examples_for_set.rb
|
335
|
+
- spec/support/shared_examples_for_workflow.rb
|
336
336
|
- spec/support/spec_adapter.rb
|
337
337
|
- spec/support/zk_setup.rb
|
@@ -1,76 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
module Cellect::Server
|
4
|
-
describe GroupedProject do
|
5
|
-
SET_TYPES.collect{ |type| "grouped_#{ type }" }.each do |project_type|
|
6
|
-
context project_type do
|
7
|
-
it_behaves_like 'project', :project
|
8
|
-
let(:project){ GroupedProject[project_type] }
|
9
|
-
let(:user){ project.user 123 }
|
10
|
-
let(:set_klass){ project.prioritized? ? DiffSet::PrioritySet : DiffSet::RandomSet }
|
11
|
-
before(:each){ pass_until project, is: :ready }
|
12
|
-
|
13
|
-
it 'should provide unseen from a random group for users' do
|
14
|
-
project.groups = { }
|
15
|
-
project.groups[1] = set_klass.new
|
16
|
-
project.groups[1].should_receive(:subtract).with user.seen, 3
|
17
|
-
project.unseen_for 123, limit: 3
|
18
|
-
end
|
19
|
-
|
20
|
-
it 'should provide unseen from a specific group for users' do
|
21
|
-
3.times{ |i| project.groups[i] = set_klass.new }
|
22
|
-
project.group(1).should_receive(:subtract).with user.seen, 3
|
23
|
-
project.unseen_for 123, group_id: 1, limit: 3
|
24
|
-
end
|
25
|
-
|
26
|
-
it 'should sample subjects from a random group without a user' do
|
27
|
-
project.groups = { }
|
28
|
-
project.groups[1] = set_klass.new
|
29
|
-
project.group(1).should_receive(:sample).with 3
|
30
|
-
project.sample limit: 3
|
31
|
-
end
|
32
|
-
|
33
|
-
it 'should sample subjects from a specific group without a user' do
|
34
|
-
3.times{ |i| project.groups[i] = set_klass.new }
|
35
|
-
project.group(1).should_receive(:sample).with 3
|
36
|
-
project.sample group_id: 1, limit: 3
|
37
|
-
end
|
38
|
-
|
39
|
-
it 'should sample subjects from a random group for a user' do
|
40
|
-
project.groups = { }
|
41
|
-
project.groups[1] = set_klass.new
|
42
|
-
project.groups[1].should_receive(:subtract).with user.seen, 3
|
43
|
-
project.sample user_id: 123, limit: 3
|
44
|
-
end
|
45
|
-
|
46
|
-
it 'should sample subjects from a specific group for a user' do
|
47
|
-
3.times{ |i| project.groups[i] = set_klass.new }
|
48
|
-
project.group(1).should_receive(:subtract).with user.seen, 3
|
49
|
-
project.sample user_id: 123, group_id: 1, limit: 3
|
50
|
-
end
|
51
|
-
|
52
|
-
it 'should add subjects' do
|
53
|
-
project.groups[1] = set_klass.new
|
54
|
-
|
55
|
-
if project.prioritized?
|
56
|
-
project.groups[1].should_receive(:add).with 123, 456
|
57
|
-
project.add subject_id: 123, group_id: 1, priority: 456
|
58
|
-
else
|
59
|
-
project.groups[1].should_receive(:add).with 123
|
60
|
-
project.add subject_id: 123, group_id: 1
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
it 'should remove subjects' do
|
65
|
-
project.groups[1] = set_klass.new
|
66
|
-
project.groups[1].should_receive(:remove).with 123
|
67
|
-
project.remove subject_id: 123, group_id: 1
|
68
|
-
end
|
69
|
-
|
70
|
-
it 'should be grouped' do
|
71
|
-
project.should be_grouped
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
data/spec/server/project_spec.rb
DELETED
@@ -1,62 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
module Cellect::Server
|
4
|
-
describe Project do
|
5
|
-
SET_TYPES.each do |project_type|
|
6
|
-
context project_type do
|
7
|
-
it_behaves_like 'project', :project
|
8
|
-
let(:project){ Project[project_type] }
|
9
|
-
let(:user){ project.user 123 }
|
10
|
-
before(:each){ pass_until project, is: :ready }
|
11
|
-
|
12
|
-
it 'should provide unseen for users' do
|
13
|
-
project.subjects.should_receive(:subtract).with user.seen, 3
|
14
|
-
project.unseen_for 123, limit: 3
|
15
|
-
end
|
16
|
-
|
17
|
-
it 'should sample subjects without a user' do
|
18
|
-
project.subjects.should_receive(:sample).with 3
|
19
|
-
project.sample limit: 3
|
20
|
-
end
|
21
|
-
|
22
|
-
it 'should sample subjects with a user' do
|
23
|
-
project.subjects.should_receive(:subtract).with user.seen, 3
|
24
|
-
project.sample user_id: 123, limit: 3
|
25
|
-
end
|
26
|
-
|
27
|
-
it 'should add subjects' do
|
28
|
-
if project.prioritized?
|
29
|
-
project.subjects.should_receive(:add).with 123, 456
|
30
|
-
project.add subject_id: 123, priority: 456
|
31
|
-
else
|
32
|
-
project.subjects.should_receive(:add).with 123
|
33
|
-
project.add subject_id: 123
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
it 'should remove subjects' do
|
38
|
-
project.subjects.should_receive(:add).with 123
|
39
|
-
project.add subject_id: 123
|
40
|
-
end
|
41
|
-
|
42
|
-
it 'should be notified of a user ttl expiry' do
|
43
|
-
async_project = double
|
44
|
-
project.should_receive(:async).and_return async_project
|
45
|
-
async_project.should_receive(:remove_user).with user.id
|
46
|
-
user.ttl_expired!
|
47
|
-
end
|
48
|
-
|
49
|
-
it 'should remove users when their ttl expires' do
|
50
|
-
id = user.id
|
51
|
-
project.remove_user id
|
52
|
-
project.users.should_not have_key id
|
53
|
-
expect{ user.id }.to raise_error
|
54
|
-
end
|
55
|
-
|
56
|
-
it 'should not be grouped' do
|
57
|
-
project.should_not be_grouped
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|