noodall-core 0.1.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.
- data/.document +5 -0
- data/.gitignore +22 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +62 -0
- data/LICENSE +20 -0
- data/README.rdoc +17 -0
- data/Rakefile +36 -0
- data/VERSION +1 -0
- data/lib/noodall-core.rb +24 -0
- data/lib/noodall/component.rb +45 -0
- data/lib/noodall/global_update_time.rb +27 -0
- data/lib/noodall/indexer.rb +11 -0
- data/lib/noodall/multi_parameter_attributes.rb +83 -0
- data/lib/noodall/node.rb +350 -0
- data/lib/noodall/permalink.rb +27 -0
- data/lib/noodall/search.rb +123 -0
- data/lib/noodall/tagging.rb +68 -0
- data/noodall-core.gemspec +79 -0
- data/spec/component_spec.rb +51 -0
- data/spec/factories/component.rb +16 -0
- data/spec/factories/node.rb +15 -0
- data/spec/node_spec.rb +330 -0
- data/spec/spec_helper.rb +32 -0
- metadata +155 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
module Noodall
|
2
|
+
class Permalink < Array
|
3
|
+
def initialize(*args)
|
4
|
+
if args.length > 1
|
5
|
+
super args
|
6
|
+
else
|
7
|
+
super args.first.to_s.split('/')
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
self.join('/')
|
13
|
+
end
|
14
|
+
|
15
|
+
def inspect
|
16
|
+
"<Permalink #{self.to_s}>"
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.to_mongo(value)
|
20
|
+
value.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.from_mongo(value)
|
24
|
+
new( *value.to_s.split('/') )
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module Noodall
|
2
|
+
module Search
|
3
|
+
STOPWORDS = ["about", "above", "above", "across", "after", "afterwards", "again", "against", "all", "almost", "alone", "along", "already", "also", "although", "always", "among", "amongst", "amoungst", "amount", "and", "another", "any", "anyhow", "anyone", "anything", "anyway", "anywhere", "are", "around", "back", "became", "because", "become", "becomes", "becoming", "been", "before", "beforehand", "behind", "being", "below", "beside", "besides", "between", "beyond", "bill", "both", "bottom", "but", "call", "can", "cannot", "cant", "con", "could", "couldnt", "cry", "describe", "detail", "done", "down", "due", "during", "each", "eight", "either", "eleven", "else", "elsewhere", "empty", "enough", "etc", "even", "ever", "every", "everyone", "everything", "everywhere", "except", "few", "fifteen", "fify", "fill", "find", "fire", "first", "five", "for", "former", "formerly", "forty", "found", "four", "from", "front", "full", "further", "get", "give", "had", "has", "hasnt", "have", "hence", "her", "here", "hereafter", "hereby", "herein", "hereupon", "hers", "herself", "him", "himself", "his", "how", "however", "hundred", "inc", "indeed", "interest", "into", "its", "itself", "keep", "last", "latter", "latterly", "least", "less", "ltd", "made", "many", "may", "meanwhile", "might", "mill", "mine", "more", "moreover", "most", "mostly", "move", "much", "must", "myself", "name", "namely", "neither", "never", "nevertheless", "next", "nine", "nobody", "none", "noone", "nor", "not", "nothing", "now", "nowhere", "off", "often", "once", "one", "only", "onto", "other", "others", "otherwise", "our", "ours", "ourselves", "out", "over", "own", "part", "per", "perhaps", "please", "put", "rather", "same", "see", "seem", "seemed", "seeming", "seems", "serious", "several", "she", "should", "show", "side", "since", "sincere", "six", "sixty", "some", "somehow", "someone", "something", "sometime", "sometimes", "somewhere", "still", "such", "system", "take", "ten", "than", "that", "the", "the", "their", "them", "themselves", "then", "thence", "there", "thereafter", "thereby", "therefore", "therein", "thereupon", "these", "they", "thickv", "thin", "third", "this", "those", "though", "three", "through", "throughout", "thru", "thus", "together", "too", "top", "toward", "towards", "twelve", "twenty", "two", "under", "until", "upon", "very", "via", "was", "well", "were", "what", "whatever", "when", "whence", "whenever", "where", "whereafter", "whereas", "whereby", "wherein", "whereupon", "wherever", "whether", "which", "while", "whither", "who", "whoever", "whole", "whom", "whose", "why", "will", "with", "within", "without", "would", "yet", "you", "your", "yours", "yourself", "yourselves"]
|
4
|
+
|
5
|
+
def self.configure(model)
|
6
|
+
require 'lingua/stemmer'
|
7
|
+
|
8
|
+
model.class_eval do
|
9
|
+
key :_keywords, Array, :index => true
|
10
|
+
attr_accessor :relevance
|
11
|
+
|
12
|
+
before_save :_update_keywords
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def searchable_keys(*keys)
|
18
|
+
@@searchable_keys ||= Set.new
|
19
|
+
@@searchable_keys += keys
|
20
|
+
|
21
|
+
@@searchable_keys
|
22
|
+
end
|
23
|
+
|
24
|
+
def language(lang = 'en')
|
25
|
+
@language ||= lang
|
26
|
+
end
|
27
|
+
|
28
|
+
def search(query, options = {})
|
29
|
+
if options[:per_page] || options[:per_page]
|
30
|
+
per_page = options.delete(:per_page)
|
31
|
+
page = options.delete(:page)
|
32
|
+
end
|
33
|
+
plucky_query = query(options.reverse_merge(
|
34
|
+
:order => 'relevance DESC'
|
35
|
+
))
|
36
|
+
criteria = plucky_query.criteria.to_hash
|
37
|
+
options = plucky_query.options.to_hash
|
38
|
+
|
39
|
+
# Extract words from the query and clean up
|
40
|
+
words = query.downcase.split(/\W/) - STOPWORDS
|
41
|
+
words.reject!{|w| w.length < 3}
|
42
|
+
criteria.merge!( :_keywords => { :$all => words } )
|
43
|
+
|
44
|
+
# The Search result
|
45
|
+
search_result = collection.map_reduce(search_map(words), search_reduce, {:query => criteria, :finalize => search_finalize})
|
46
|
+
|
47
|
+
# Add value to sort options because model is stored in the value key
|
48
|
+
options[:sort].map! do |s,v|
|
49
|
+
["value.#{s}",v]
|
50
|
+
end
|
51
|
+
|
52
|
+
search_query = Plucky::Query.new(search_result, options)
|
53
|
+
|
54
|
+
if per_page
|
55
|
+
results = search_query.paginate(:per_page => per_page, :page => page)
|
56
|
+
else
|
57
|
+
results = search_query.all
|
58
|
+
end
|
59
|
+
# clean up tmp collection
|
60
|
+
search_result.drop
|
61
|
+
#return results mappped to objects
|
62
|
+
results.tap do |docs|
|
63
|
+
docs.map! { |hash| load(hash['value']) }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def search_map(words)
|
68
|
+
#convert words into Regex OR
|
69
|
+
q = words.map do |k|
|
70
|
+
Regexp.escape(k)
|
71
|
+
end.join("|")
|
72
|
+
"function(){" +
|
73
|
+
"this.relevance = this._keywords.filter(" +
|
74
|
+
"function(z){" +
|
75
|
+
"return String(z).match(/(#{q})/);" +
|
76
|
+
"}).length;" +
|
77
|
+
"emit(this._id, this);" +
|
78
|
+
"}"
|
79
|
+
end
|
80
|
+
|
81
|
+
def search_reduce
|
82
|
+
"function( key , values ){return { model: values[0]};}"
|
83
|
+
end
|
84
|
+
|
85
|
+
def search_finalize
|
86
|
+
"function( key , values ){return values.model;}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
module InstanceMethods
|
91
|
+
protected
|
92
|
+
def _update_keywords
|
93
|
+
s = Lingua::Stemmer.new(:language => self.class.language)
|
94
|
+
|
95
|
+
self._keywords = []
|
96
|
+
|
97
|
+
self.class.searchable_keys.each do |search_key|
|
98
|
+
self._keywords += keywords_for_value(s, send(search_key)).compact
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
def keywords_for_value(stemmer, val)
|
104
|
+
if val.kind_of?(String)
|
105
|
+
words = val.downcase.split(/\W/) - STOPWORDS
|
106
|
+
words.reject!{|w| w.length < 3}
|
107
|
+
words.map do |word|
|
108
|
+
stem = stemmer.stem(word)
|
109
|
+
if stem != word
|
110
|
+
[stem, word]
|
111
|
+
else
|
112
|
+
word
|
113
|
+
end
|
114
|
+
end.flatten
|
115
|
+
elsif val.kind_of?(Array)
|
116
|
+
val.map { |e| keywords_for_value(stemmer, e) }.flatten
|
117
|
+
else
|
118
|
+
[val]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Noodall
|
2
|
+
module Tagging
|
3
|
+
def self.configure(model)
|
4
|
+
model.class_eval do
|
5
|
+
key :tags, Array, :index => true
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def tag_cloud(options = {})
|
11
|
+
return [] if self.count == 0 # Don't bother if there is nothing in this collection
|
12
|
+
query = query(options.reverse_merge(
|
13
|
+
:order => 'value DESC'
|
14
|
+
))
|
15
|
+
tags_map = collection.map_reduce(tag_cloud_map, tag_cloud_reduce, {:query => query.criteria.to_hash})
|
16
|
+
if tags_map.count > 0
|
17
|
+
tags = tags_map.find({}, query.options.to_hash ).to_a.collect{ |hash| Tag.new(hash['_id'], hash['value']) }
|
18
|
+
tags_map.drop # clean up tmp collection
|
19
|
+
tags
|
20
|
+
else
|
21
|
+
[]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def tag_cloud_map
|
26
|
+
"function(){" +
|
27
|
+
"this.tags.forEach(" +
|
28
|
+
"function(z){" +
|
29
|
+
"emit( z , 1 );" +
|
30
|
+
"}" +
|
31
|
+
")}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def tag_cloud_reduce
|
35
|
+
"function( key , values ){" +
|
36
|
+
"var total = 0;" +
|
37
|
+
"for ( var i=0; i<values.length; i++ ){" +
|
38
|
+
"total += values[i];" +
|
39
|
+
"}" +
|
40
|
+
"return total;" +
|
41
|
+
"}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
module InstanceMethods
|
46
|
+
def tag_list=(string)
|
47
|
+
self.tags = string.to_s.split(',').map{ |t| t.strip.downcase }.reject(&:blank?).compact.uniq
|
48
|
+
end
|
49
|
+
|
50
|
+
def tag_list
|
51
|
+
tags.join(', ')
|
52
|
+
end
|
53
|
+
|
54
|
+
def related(options ={})
|
55
|
+
self.class.all(options.merge({:_id => {'$ne' => self._id}, :tags => /(#{self.tags.join('|')})/i}))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Tag
|
60
|
+
attr_reader :name, :count
|
61
|
+
|
62
|
+
def initialize(name, count)
|
63
|
+
@name = name
|
64
|
+
@count = count.to_i
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{noodall-core}
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Steve England"]
|
12
|
+
s.date = %q{2010-10-10}
|
13
|
+
s.description = %q{Core data objects for Noodall}
|
14
|
+
s.email = %q{steve@wearebeef.co.uk}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".gitignore",
|
22
|
+
"Gemfile",
|
23
|
+
"Gemfile.lock",
|
24
|
+
"LICENSE",
|
25
|
+
"README.rdoc",
|
26
|
+
"Rakefile",
|
27
|
+
"VERSION",
|
28
|
+
"lib/noodall-core.rb",
|
29
|
+
"lib/noodall/component.rb",
|
30
|
+
"lib/noodall/global_update_time.rb",
|
31
|
+
"lib/noodall/indexer.rb",
|
32
|
+
"lib/noodall/multi_parameter_attributes.rb",
|
33
|
+
"lib/noodall/node.rb",
|
34
|
+
"lib/noodall/permalink.rb",
|
35
|
+
"lib/noodall/search.rb",
|
36
|
+
"lib/noodall/tagging.rb",
|
37
|
+
"noodall-core.gemspec",
|
38
|
+
"spec/component_spec.rb",
|
39
|
+
"spec/factories/component.rb",
|
40
|
+
"spec/factories/node.rb",
|
41
|
+
"spec/node_spec.rb",
|
42
|
+
"spec/spec_helper.rb"
|
43
|
+
]
|
44
|
+
s.homepage = %q{http://github.com/beef/noodall-core}
|
45
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
46
|
+
s.require_paths = ["lib"]
|
47
|
+
s.rubygems_version = %q{1.3.7}
|
48
|
+
s.summary = %q{Core data objects for Noodall}
|
49
|
+
s.test_files = [
|
50
|
+
"spec/component_spec.rb",
|
51
|
+
"spec/factories/component.rb",
|
52
|
+
"spec/factories/node.rb",
|
53
|
+
"spec/node_spec.rb",
|
54
|
+
"spec/spec_helper.rb"
|
55
|
+
]
|
56
|
+
|
57
|
+
if s.respond_to? :specification_version then
|
58
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
59
|
+
s.specification_version = 3
|
60
|
+
|
61
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
62
|
+
s.add_runtime_dependency(%q<mongo_mapper>, ["= 0.8.4"])
|
63
|
+
s.add_runtime_dependency(%q<ramdiv-mongo_mapper_acts_as_tree>, ["= 0.1.1"])
|
64
|
+
s.add_runtime_dependency(%q<canable>, ["= 0.1.1"])
|
65
|
+
s.add_runtime_dependency(%q<ruby-stemmer>, [">= 0"])
|
66
|
+
else
|
67
|
+
s.add_dependency(%q<mongo_mapper>, ["= 0.8.4"])
|
68
|
+
s.add_dependency(%q<ramdiv-mongo_mapper_acts_as_tree>, ["= 0.1.1"])
|
69
|
+
s.add_dependency(%q<canable>, ["= 0.1.1"])
|
70
|
+
s.add_dependency(%q<ruby-stemmer>, [">= 0"])
|
71
|
+
end
|
72
|
+
else
|
73
|
+
s.add_dependency(%q<mongo_mapper>, ["= 0.8.4"])
|
74
|
+
s.add_dependency(%q<ramdiv-mongo_mapper_acts_as_tree>, ["= 0.1.1"])
|
75
|
+
s.add_dependency(%q<canable>, ["= 0.1.1"])
|
76
|
+
s.add_dependency(%q<ruby-stemmer>, [">= 0"])
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Noodall::Component do
|
4
|
+
|
5
|
+
it "should allow you to define the slots that are available" do
|
6
|
+
Noodall::Node.slots :wide, :small, :main
|
7
|
+
|
8
|
+
Noodall::Component.possible_slots.should == [:wide, :small, :main]
|
9
|
+
|
10
|
+
class Promo < Noodall::Component
|
11
|
+
allowed_positions :small, :wide, :main, :egg, :nog
|
12
|
+
end
|
13
|
+
|
14
|
+
Promo.positions.should have(3).things
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should list components classes avaiable to a slot" do
|
18
|
+
|
19
|
+
class Promo < Noodall::Component
|
20
|
+
allowed_positions :small, :wide
|
21
|
+
end
|
22
|
+
|
23
|
+
Promo.positions.should have(2).things
|
24
|
+
|
25
|
+
Noodall::Component.positions_classes(:small).should include(Promo)
|
26
|
+
Noodall::Component.positions_classes(:main).should_not include(Promo)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should be validated by the node" do
|
30
|
+
class Promo < Noodall::Component
|
31
|
+
allowed_positions :small, :wide
|
32
|
+
end
|
33
|
+
|
34
|
+
node = Factory(:page)
|
35
|
+
node.main_slot_0 = Promo.new()
|
36
|
+
|
37
|
+
node.save
|
38
|
+
|
39
|
+
node.errors.should have(1).things
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should know it's node" do
|
43
|
+
node = Factory(:page)
|
44
|
+
node.small_slot_0 = Factory(:content)
|
45
|
+
|
46
|
+
node.save!
|
47
|
+
|
48
|
+
node.small_slot_0.node.should == node
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# A Dummy slot for tests
|
2
|
+
class Content < Noodall::Component
|
3
|
+
key :title, String
|
4
|
+
key :url, String
|
5
|
+
key :url_text, String
|
6
|
+
|
7
|
+
allowed_positions :small, :wide
|
8
|
+
end
|
9
|
+
|
10
|
+
# And a factory to build it
|
11
|
+
Factory.define :content do |component|
|
12
|
+
component.title { Faker::Lorem.words(4).join('') }
|
13
|
+
component.url { 'http://www.google.com' }
|
14
|
+
component.url_text { 'More' }
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Dummy node class for the tests
|
2
|
+
class Page < Noodall::Node
|
3
|
+
#sub_templates PageA, PageB, ArticlesList, LandingPage, EventsList
|
4
|
+
root_template!
|
5
|
+
|
6
|
+
main_slots 1
|
7
|
+
small_slots 4
|
8
|
+
wide_slots 3
|
9
|
+
end
|
10
|
+
|
11
|
+
# And a factory to build it
|
12
|
+
Factory.define :page do |node|
|
13
|
+
node.title { Faker::Lorem.words(3).join(' ') }
|
14
|
+
node.published_at { Time.now }
|
15
|
+
end
|
data/spec/node_spec.rb
ADDED
@@ -0,0 +1,330 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Noodall::Node do
|
4
|
+
before(:each) do
|
5
|
+
@valid_attributes = {
|
6
|
+
:title => "My First Node"
|
7
|
+
}
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should create a new instance given valid attributes" do
|
11
|
+
Noodall::Node.create!(@valid_attributes)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should be invalid if attributes are incorrect" do
|
15
|
+
c = Noodall::Node.create
|
16
|
+
c.valid?.should == false
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should know it's root templates" do
|
20
|
+
class LandingPage < Noodall::Node
|
21
|
+
root_template!
|
22
|
+
end
|
23
|
+
|
24
|
+
Noodall::Node.template_classes.should include(LandingPage)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should know what class it is" do
|
28
|
+
page = Page.create!(@valid_attributes)
|
29
|
+
page.reload
|
30
|
+
|
31
|
+
page.class.should == Page
|
32
|
+
|
33
|
+
node = Noodall::Node.find(page.id)
|
34
|
+
|
35
|
+
node.class.should == Page
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should be found by permalink" do
|
39
|
+
page = Factory(:page, :title => "My Page", :publish => true)
|
40
|
+
Noodall::Node.find_by_permalink('my-page').should == page
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should allow you to set the number of slots" do
|
44
|
+
class NicePage < Noodall::Node
|
45
|
+
wide_slots 3
|
46
|
+
small_slots 5
|
47
|
+
end
|
48
|
+
|
49
|
+
NicePage.slots_count.should == 8
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "within a tree" do
|
53
|
+
before(:each) do
|
54
|
+
@root = Page.create!(:title => "Root")
|
55
|
+
|
56
|
+
@child = Page.create!(:title => "Ickle Kid", :parent => @root)
|
57
|
+
|
58
|
+
@grandchild = Page.create!(:title => "Very Ickle Kid", :parent => @child)
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should create permlink based on tree" do
|
62
|
+
@grandchild.permalink.to_s.should == "root/ickle-kid/very-ickle-kid"
|
63
|
+
@grandchild.reload
|
64
|
+
@grandchild.permalink.to_s.should == "root/ickle-kid/very-ickle-kid"
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should be under the correct path once moved" do
|
68
|
+
grand_child_2 = Page.create!(:title => "Ickle Kid", :parent => @child)
|
69
|
+
root_2 = Page.create!(:title => "Root 2")
|
70
|
+
|
71
|
+
grand_child_2.parent = root_2
|
72
|
+
grand_child_2.save!
|
73
|
+
|
74
|
+
grand_child_2.path.should_not include(@child.id)
|
75
|
+
grand_child_2.path.should include(root_2.id)
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should be under the correct path once it's parent is moved" do
|
79
|
+
grand_child_2 = Page.create!(:title => "Ickle Kid 2", :parent => @child)
|
80
|
+
root_2 = Page.create!(:title => "Root 2")
|
81
|
+
@child.parent = root_2
|
82
|
+
@child.save(:validate => false)
|
83
|
+
|
84
|
+
grand_child_2.reload
|
85
|
+
|
86
|
+
grand_child_2.path.should_not include(@root.id)
|
87
|
+
grand_child_2.path.should include(root_2.id)
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should allow parent to be set to nill using a the string 'none' to make it easy to set via forms" do
|
91
|
+
@grandchild.update_attributes!(:parent => 'none')
|
92
|
+
@grandchild.reload
|
93
|
+
@grandchild.parent.should be(nil)
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
describe "Publish dates" do
|
99
|
+
before(:each) do
|
100
|
+
@node = Factory(:page, :published_at => 3.days.ago, :published_to => 3.days.since)
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should be able to be published now" do
|
104
|
+
node = Factory(:page, :publish => true)
|
105
|
+
node.published?.should == true
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should be able to be hidden now" do
|
109
|
+
@node.hide = true
|
110
|
+
@node.save
|
111
|
+
@node.published?.should == false
|
112
|
+
end
|
113
|
+
|
114
|
+
it "should determine if it is published" do
|
115
|
+
@node.published?.should == true
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should know it it is to be published" do
|
119
|
+
@node.published_at = 1.hour.since
|
120
|
+
@node.save
|
121
|
+
@node.published?.should == false
|
122
|
+
@node.pending?.should == true
|
123
|
+
@node.expired?.should == false
|
124
|
+
end
|
125
|
+
|
126
|
+
it "should know it has been published" do
|
127
|
+
@node.published_to = 1.hour.ago
|
128
|
+
@node.save
|
129
|
+
@node.published?.should == false
|
130
|
+
@node.pending?.should == false
|
131
|
+
@node.expired?.should == true
|
132
|
+
end
|
133
|
+
|
134
|
+
it "should allow you to clear published to" do
|
135
|
+
@node.published_to = 1.hour.ago
|
136
|
+
@node.save
|
137
|
+
@node.published?.should == false
|
138
|
+
|
139
|
+
@node.published_to = nil
|
140
|
+
@node.save
|
141
|
+
@node.published?.should == true
|
142
|
+
end
|
143
|
+
|
144
|
+
it "should be findable by publish dates" do
|
145
|
+
nodes = Noodall::Node.all(:published_at => { :$lte => Time.now }, :published_to => { :$gte => Time.now })
|
146
|
+
nodes.should have(1).things
|
147
|
+
|
148
|
+
nodes = Noodall::Node.all(:published_at => { :$lte => 4.days.ago }, :published_to => { :$gte => 4.days.ago })
|
149
|
+
nodes.should have(0).things
|
150
|
+
|
151
|
+
nodes = Noodall::Node.all(:published_at => { :$lte => 4.days.since }, :published_to => { :$gte => 4.days.since })
|
152
|
+
nodes.should have(0).things
|
153
|
+
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
157
|
+
|
158
|
+
it "should update the global update timestamp" do
|
159
|
+
Noodall::GlobalUpdateTime::Stamp.should_receive(:update!)
|
160
|
+
p = Factory(:page)
|
161
|
+
end
|
162
|
+
|
163
|
+
it "should be able to list all slots" do
|
164
|
+
ObjectSpace.each_object(Class) do |c|
|
165
|
+
next unless c.ancestors.include?(Noodall::Node) and (c != Noodall::Node)
|
166
|
+
c.new.slots.should be_instance_of(Array)
|
167
|
+
end
|
168
|
+
|
169
|
+
node = Factory(:page)
|
170
|
+
node.small_slot_0 = Content.new(:body => "Some text")
|
171
|
+
node.small_slot_1 = Content.new(:body => "Some more text")
|
172
|
+
|
173
|
+
node.save!
|
174
|
+
|
175
|
+
node.slots.should have(2).things
|
176
|
+
|
177
|
+
node.slots.first.body.should == "Some text"
|
178
|
+
node.slots.last.body.should == "Some more text"
|
179
|
+
end
|
180
|
+
|
181
|
+
it "should use a tree structure" do
|
182
|
+
root = Page.create!(@valid_attributes)
|
183
|
+
|
184
|
+
child = Page.create!(:title => "Ickle Kid", :parent => root)
|
185
|
+
|
186
|
+
grandchild = Page.create!(:title => "Very Ickle Kid", :parent => child)
|
187
|
+
|
188
|
+
child.parent.should == root
|
189
|
+
|
190
|
+
grandchild.root.should == root
|
191
|
+
|
192
|
+
root.children.first.should == child
|
193
|
+
|
194
|
+
sec_child = Page.create!(:title => "Ickle Kid 2", :parent => root)
|
195
|
+
|
196
|
+
child.siblings.first.should == sec_child
|
197
|
+
|
198
|
+
root.children.last.should == sec_child
|
199
|
+
|
200
|
+
sec_child.position = 0
|
201
|
+
sec_child.save
|
202
|
+
|
203
|
+
root.children.first.should == sec_child
|
204
|
+
|
205
|
+
child.reload
|
206
|
+
|
207
|
+
child.position.should == 1
|
208
|
+
|
209
|
+
third_child = Page.create!(:title => "Ickle Kid 3", :parent => root, :position => 0)
|
210
|
+
|
211
|
+
root.children.first.should == third_child
|
212
|
+
|
213
|
+
sec_child.reload
|
214
|
+
|
215
|
+
sec_child.position.should == 1
|
216
|
+
|
217
|
+
child.reload
|
218
|
+
|
219
|
+
child.position.should == 2
|
220
|
+
|
221
|
+
last_child = Page.create!(:title => "Ickle Kid 4", :parent => root, :position => 33)
|
222
|
+
|
223
|
+
last_child.reload
|
224
|
+
|
225
|
+
root.children.should have(4).things
|
226
|
+
|
227
|
+
|
228
|
+
root.children.last.should == last_child
|
229
|
+
|
230
|
+
last_child.position.should == 3
|
231
|
+
end
|
232
|
+
|
233
|
+
it "shold allow groups to be set by strings for easy form access" do
|
234
|
+
node = Factory(:page)
|
235
|
+
node.destroyable_groups_list = 'Webbies, Dudes,Things, Dudes, ,'
|
236
|
+
node.destroyable_groups.should == ['Webbies', 'Dudes', 'Things']
|
237
|
+
|
238
|
+
node = Factory(:page, :destroyable_groups_list => 'Webbies, Dudes,Things, Dudes, ,')
|
239
|
+
node.destroyable_groups.should == ['Webbies', 'Dudes', 'Things']
|
240
|
+
end
|
241
|
+
|
242
|
+
it "should restrict user accces by groups" do
|
243
|
+
# Stub a simple user model
|
244
|
+
class ::User
|
245
|
+
include MongoMapper::Document
|
246
|
+
include Canable::Cans
|
247
|
+
|
248
|
+
key :name, String
|
249
|
+
key :groups, Array
|
250
|
+
key :admin, Boolean
|
251
|
+
end
|
252
|
+
|
253
|
+
john = User.create!(:name => 'John', :groups => ['Webbies','Blokes'])
|
254
|
+
steve = User.create!(:name => 'Steve', :groups => ['Dudes'])
|
255
|
+
|
256
|
+
ruby = Factory(:page, :updatable_groups => ['Dudes'], :destroyable_groups => ['Webbies', 'Dudes'], :publishable_groups => ['Dudes'] )
|
257
|
+
|
258
|
+
ruby.all_groups.should have(2).things
|
259
|
+
|
260
|
+
john.can_create?(ruby).should == true
|
261
|
+
steve.can_create?(ruby).should == true
|
262
|
+
|
263
|
+
ruby.creator = john
|
264
|
+
ruby.save
|
265
|
+
|
266
|
+
john.can_view?(ruby).should == true
|
267
|
+
steve.can_view?(ruby).should == true
|
268
|
+
|
269
|
+
john.can_update?(ruby).should == false
|
270
|
+
steve.can_update?(ruby).should == true
|
271
|
+
|
272
|
+
john.can_destroy?(ruby).should == true
|
273
|
+
steve.can_destroy?(ruby).should == true
|
274
|
+
end
|
275
|
+
|
276
|
+
it "should be searchable" do
|
277
|
+
3.times do |i|
|
278
|
+
Factory(:page, :title => "My Searchable Page #{i}")
|
279
|
+
end
|
280
|
+
|
281
|
+
top_hit = Factory(:page, :title => "My Extra Searchable Page", :description => "My Extra Searchable Page")
|
282
|
+
|
283
|
+
3.times do |i|
|
284
|
+
Factory(:page, :title => "My Unfit Page #{i}")
|
285
|
+
end
|
286
|
+
|
287
|
+
results = Page.search("Searchable")
|
288
|
+
|
289
|
+
results.should have(4).things
|
290
|
+
|
291
|
+
results.first.should == top_hit
|
292
|
+
|
293
|
+
results = Page.search("Searchable", :per_page => 2)
|
294
|
+
|
295
|
+
results.should have(2).things
|
296
|
+
results.total_pages.should == 2
|
297
|
+
|
298
|
+
results.first.should == top_hit
|
299
|
+
|
300
|
+
results = Page.search("supercalifragilistic")
|
301
|
+
|
302
|
+
results.should have(0).things
|
303
|
+
end
|
304
|
+
|
305
|
+
it "should return related" do
|
306
|
+
Factory(:page, :title => "My Page 1", :tag_list => 'one,two,three')
|
307
|
+
Factory(:page, :title => "My Page 2", :tag_list => 'two,three,four')
|
308
|
+
Factory(:page, :title => "My Page 3", :tag_list => 'three,four,five')
|
309
|
+
ref = Factory(:page, :title => "My Page 4", :tag_list => 'five,nine')
|
310
|
+
|
311
|
+
ref.related.should have(1).things
|
312
|
+
|
313
|
+
Factory(:page, :title => "My Page 5", :tag_list => 'nine')
|
314
|
+
|
315
|
+
ref.related.should have(2).things
|
316
|
+
end
|
317
|
+
|
318
|
+
it "should know who can be a parent" do
|
319
|
+
class LandindPage < Noodall::Node
|
320
|
+
sub_templates Page
|
321
|
+
end
|
322
|
+
class ArticlesList < Noodall::Node
|
323
|
+
sub_templates LandindPage
|
324
|
+
end
|
325
|
+
|
326
|
+
Page.parent_classes.should include(LandingPage)
|
327
|
+
Page.parent_classes.should_not include(ArticlesList)
|
328
|
+
end
|
329
|
+
|
330
|
+
end
|