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 ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+ .bundle
21
+
22
+ ## PROJECT::SPECIFIC
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'mongo_mapper', '~> 0.8.4'
4
+ gem 'bson_ext', '~> 1.0.9'
5
+ gem 'ramdiv-mongo_mapper_acts_as_tree', '~> 0.1.1'
6
+ gem 'canable', '~> 0.1.1'
7
+ gem 'ruby-stemmer'
8
+ gem 'i18n'
9
+
10
+ group :development do
11
+ gem 'jeweler', '~> 1.4.0'
12
+ gem "rspec", "~> 2.0.0.beta.22"
13
+ gem "database_cleaner", "~> 0.5.2"
14
+ gem "factory_girl", "~> 1.3.2"
15
+ gem "faker", "~> 0.3.1"
16
+ gem "rake"
17
+ end
18
+
data/Gemfile.lock ADDED
@@ -0,0 +1,62 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activesupport (3.0.0)
5
+ bson (1.0.9)
6
+ bson_ext (1.0.9)
7
+ canable (0.1.1)
8
+ database_cleaner (0.5.2)
9
+ diff-lcs (1.1.2)
10
+ factory_girl (1.3.2)
11
+ faker (0.3.1)
12
+ gemcutter (0.6.1)
13
+ git (1.2.5)
14
+ i18n (0.4.1)
15
+ jeweler (1.4.0)
16
+ gemcutter (>= 0.1.0)
17
+ git (>= 1.2.5)
18
+ rubyforge (>= 2.0.0)
19
+ jnunemaker-validatable (1.8.4)
20
+ activesupport (>= 2.3.4)
21
+ json_pure (1.4.6)
22
+ mongo (1.0.9)
23
+ bson (>= 1.0.5)
24
+ mongo_mapper (0.8.4)
25
+ activesupport (>= 2.3.4)
26
+ jnunemaker-validatable (~> 1.8.4)
27
+ plucky (~> 0.3.5)
28
+ plucky (0.3.5)
29
+ mongo (~> 1.0.8)
30
+ rake (0.8.7)
31
+ ramdiv-mongo_mapper_acts_as_tree (0.1.1)
32
+ mongo_mapper (>= 0.6.8)
33
+ rspec (2.0.0.beta.22)
34
+ rspec-core (= 2.0.0.beta.22)
35
+ rspec-expectations (= 2.0.0.beta.22)
36
+ rspec-mocks (= 2.0.0.beta.22)
37
+ rspec-core (2.0.0.beta.22)
38
+ rspec-expectations (2.0.0.beta.22)
39
+ diff-lcs (>= 1.1.2)
40
+ rspec-mocks (2.0.0.beta.22)
41
+ rspec-core (= 2.0.0.beta.22)
42
+ rspec-expectations (= 2.0.0.beta.22)
43
+ ruby-stemmer (0.8.2)
44
+ rubyforge (2.0.4)
45
+ json_pure (>= 1.1.7)
46
+
47
+ PLATFORMS
48
+ ruby
49
+
50
+ DEPENDENCIES
51
+ bson_ext (~> 1.0.9)
52
+ canable (~> 0.1.1)
53
+ database_cleaner (~> 0.5.2)
54
+ factory_girl (~> 1.3.2)
55
+ faker (~> 0.3.1)
56
+ i18n
57
+ jeweler (~> 1.4.0)
58
+ mongo_mapper (~> 0.8.4)
59
+ rake
60
+ ramdiv-mongo_mapper_acts_as_tree (~> 0.1.1)
61
+ rspec (~> 2.0.0.beta.22)
62
+ ruby-stemmer
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Steve England
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,17 @@
1
+ = noodall (Node-All) core
2
+
3
+ Noodal is a CMS using Mongo(mapper/id). It is nice.
4
+
5
+ == Note on Patches/Pull Requests
6
+
7
+ * Fork the project.
8
+ * Make your feature addition or bug fix.
9
+ * Add tests for it. This is important so I don't break it in a
10
+ future version unintentionally.
11
+ * Commit, do not mess with rakefile, version, or history.
12
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
13
+ * Send me a pull request. Bonus points for topic branches.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2010 Steve England. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,36 @@
1
+ require "bundler"
2
+ Bundler.setup(:default, :development)
3
+ require "rspec/core/rake_task"
4
+
5
+ begin
6
+ require 'jeweler'
7
+ Jeweler::Tasks.new do |gem|
8
+ gem.name = "noodall-core"
9
+ gem.summary = %Q{Core data objects for Noodall}
10
+ gem.description = %Q{Core data objects for Noodall}
11
+ gem.email = "steve@wearebeef.co.uk"
12
+ gem.homepage = "http://github.com/beef/noodall-core"
13
+ gem.authors = ["Steve England"]
14
+ gem.add_dependency('mongo_mapper', '0.8.4')
15
+ gem.add_dependency('ramdiv-mongo_mapper_acts_as_tree', '0.1.1')
16
+ gem.add_dependency('canable', '0.1.1')
17
+ gem.add_dependency('ruby-stemmer')
18
+
19
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
20
+ end
21
+ Jeweler::GemcutterTasks.new
22
+ rescue LoadError
23
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
24
+ end
25
+
26
+ RSpec::Core::RakeTask.new(:spec)
27
+
28
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
29
+ spec.rcov = true
30
+ end
31
+
32
+ task :spec => :check_dependencies
33
+
34
+ task :default => :spec
35
+
36
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,24 @@
1
+ require 'mongo_mapper'
2
+ require 'mongo_mapper_acts_as_tree'
3
+ require 'canable'
4
+ require 'noodall/global_update_time'
5
+ require 'noodall/multi_parameter_attributes'
6
+ require 'noodall/search'
7
+ require 'noodall/tagging'
8
+ require 'noodall/permalink'
9
+ require 'noodall/indexer'
10
+ require 'noodall/component'
11
+ require 'noodall/node'
12
+
13
+
14
+ # Ruby 1.8 Compatibilty
15
+ class Object
16
+ unless defined?(define_singleton_method)
17
+ puts "Redefining 'define_singleton_method' to work with Ruby1.8"
18
+ def define_singleton_method(sym, &block)
19
+ singleton_class.instance_eval do
20
+ define_method sym, block
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,45 @@
1
+ module Noodall
2
+ class Component
3
+ include MongoMapper::EmbeddedDocument
4
+
5
+ key :_type, String
6
+ key :style, String
7
+
8
+ embedded_in :node
9
+
10
+ module ClassMethods
11
+ def possible_slots
12
+ Node.possible_slots
13
+ end
14
+
15
+ def allowed_positions(*args)
16
+ @allowed_positions = args.reject{|a| !Node.possible_slots.include?(a) }.uniq
17
+ end
18
+
19
+ def positions
20
+ @allowed_positions || []
21
+ end
22
+
23
+ def positions_classes(position)
24
+ classes = []
25
+ ObjectSpace.each_object(Class) do |c|
26
+ next unless c.ancestors.include?(Component) and (c != Component) and c.positions.include?(position)
27
+ classes << c
28
+ end
29
+ classes
30
+ end
31
+
32
+ def positions_names(position)
33
+ positions_classes(position).collect{|c| c.name.titleize }
34
+ end
35
+
36
+ # Allow us to set the component to nil if we get a blank
37
+ def to_mongo(value)
38
+ return nil if value.blank?
39
+ super
40
+ end
41
+
42
+ end
43
+ extend ClassMethods
44
+ end
45
+ end
@@ -0,0 +1,27 @@
1
+ module Noodall
2
+ module GlobalUpdateTime
3
+ class Stamp
4
+ def self.read
5
+ Rails.cache.read('global_update_time') if defined?(Rails)
6
+ end
7
+
8
+ def self.update!
9
+ Rails.cache.write('global_update_time', Time.zone.now.utc) if defined?(Rails)
10
+ end
11
+ end
12
+
13
+ def self.configure(model)
14
+ model.class_eval do
15
+ after_save :global_updated!
16
+ after_destroy :global_updated!
17
+ end
18
+ end
19
+
20
+ module InstanceMethods
21
+ # Cache the updated time
22
+ def global_updated!
23
+ GlobalUpdateTime::Stamp.update!
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,11 @@
1
+ module Noodall
2
+ module Indexer
3
+ module ClassMethods
4
+ def ensure_index(spec, options={})
5
+ #collection.create_index(spec, options)
6
+ # TODO: something clever here as runtime indexing is bad
7
+ end
8
+ end
9
+ end
10
+ end
11
+
@@ -0,0 +1,83 @@
1
+ module Noodall
2
+ module MultiParameterAttributes
3
+ module InstanceMethods
4
+ def attributes=(attrs)
5
+ multi_parameter_attributes = []
6
+ attrs.each do |name, value|
7
+ return if attrs.blank?
8
+ if name.to_s.include?("(")
9
+ multi_parameter_attributes << [ name, value ]
10
+ else
11
+ writer_method = "#{name}="
12
+ if respond_to?(writer_method)
13
+ self.send(writer_method, value)
14
+ else
15
+ self[name.to_s] = value
16
+ end
17
+ end
18
+ end
19
+
20
+ assign_multiparameter_attributes(multi_parameter_attributes)
21
+ end
22
+
23
+ def assign_multiparameter_attributes(pairs)
24
+ execute_callstack_for_multiparameter_attributes(
25
+ extract_callstack_for_multiparameter_attributes(pairs)
26
+ )
27
+ end
28
+
29
+ def execute_callstack_for_multiparameter_attributes(callstack)
30
+ callstack.each do |name, values_with_empty_parameters|
31
+ # in order to allow a date to be set without a year, we must keep the empty values.
32
+ # Otherwise, we wouldn't be able to distinguish it from a date with an empty day.
33
+ values = values_with_empty_parameters.reject(&:blank?)
34
+
35
+ if values.any?
36
+ key = self.class.keys[name]
37
+ raise ArgumentError, "Unknown key #{name}" if key.nil?
38
+ klass = key.type
39
+
40
+ value = if Time == klass
41
+ Time.zone.local(*values.map(&:to_i))
42
+ elsif Date == klass
43
+ begin
44
+ values = values_with_empty_parameters.collect do |v| v.nil? ? 1 : v end
45
+ Date.new(*values)
46
+ rescue ArgumentError => ex # if Date.new raises an exception on an invalid date
47
+ Time.zone.local(*values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
48
+ end
49
+ else
50
+ klass.new(*values)
51
+ end
52
+ else
53
+ value = nil
54
+ end
55
+ writer_method = "#{name}="
56
+ if respond_to?(writer_method)
57
+ self.send(writer_method, value)
58
+ else
59
+ self[name.to_s] = value
60
+ end
61
+ end
62
+ end
63
+
64
+ def extract_callstack_for_multiparameter_attributes(pairs)
65
+ attributes = { }
66
+
67
+ for pair in pairs
68
+ multiparameter_name, value = pair
69
+ attribute_name = multiparameter_name.split("(").first
70
+ attributes[attribute_name] = [] unless attributes.include?(attribute_name)
71
+
72
+ attributes[attribute_name] << [ find_parameter_position(multiparameter_name), value ]
73
+ end
74
+
75
+ attributes.each { |name, values| attributes[name] = values.sort_by{ |v| v.first }.collect { |v| v.last } }
76
+ end
77
+
78
+ def find_parameter_position(multiparameter_name)
79
+ multiparameter_name.scan(/\(([0-9]*).*\)/).first.first
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,350 @@
1
+ module Noodall
2
+ class Node
3
+ include MongoMapper::Document
4
+ include MongoMapper::Acts::Tree
5
+ include Canable::Ables
6
+
7
+ plugin MultiParameterAttributes
8
+ plugin Indexer
9
+ plugin Search
10
+ plugin Tagging
11
+ plugin GlobalUpdateTime
12
+
13
+ key :title, String, :required => true
14
+ key :link_name, String
15
+ key :description, String
16
+ key :body, String
17
+ key :position, Integer, :default => nil
18
+ key :_type, String
19
+ key :published_at, Time
20
+ key :published_to, Time
21
+ key :updatable_groups, Array
22
+ key :destroyable_groups, Array
23
+ key :publishable_groups, Array
24
+ key :permalink, Permalink, :required => true, :index => true
25
+
26
+ timestamps!
27
+ userstamps!
28
+
29
+ alias_method :keywords, :tag_list
30
+ alias_method :keywords=, :tag_list=
31
+
32
+ attr_accessor :publish, :hide #for publishing
33
+
34
+ acts_as_tree :order => "position"
35
+
36
+ searchable_keys :title, :description, :keywords, :body
37
+
38
+ validates_true_for :template, :message => "cannot be changed as sub content is not allowed in this template", :logic => lambda { children.reject{|c| self._type.constantize.template_classes.include?(c.class)}.empty? }
39
+
40
+ def published_children
41
+ self.children.select{|c| c.published? }
42
+ end
43
+
44
+ # Allow parent to be set to none using a string. Allows us to set the parent to nil easily via forms
45
+ def parent=(var)
46
+ self[parent_id_field] = nil
47
+ var == "none" ? super(nil) : super
48
+ end
49
+
50
+ def template
51
+ self.class.name.titleize
52
+ end
53
+
54
+ def template=(template_name)
55
+ self._type = template_name.gsub(' ','') unless template_name.blank?
56
+ end
57
+
58
+ def published?
59
+ !published_at.nil? and published_at <= current_time and (published_to.nil? or published_to >= current_time)
60
+ end
61
+
62
+ def pending?
63
+ published_at.nil? or published_at >= current_time
64
+ end
65
+
66
+ def expired?
67
+ !published_to.nil? and published_to <= current_time
68
+ end
69
+
70
+ def first?
71
+ position == 0
72
+ end
73
+
74
+ def last?
75
+ position == self.class.count(:_id => {"$ne" => self._id}, parent_id_field => self[parent_id_field])
76
+ end
77
+ def move_lower
78
+ sibling = self.class.first(:position => {"$gt" => self.position}, parent_id_field => self[parent_id_field], :order => 'position ASC')
79
+
80
+ tmp = sibling.position
81
+ sibling.position = self.position
82
+ self.position = tmp
83
+
84
+ self.class.collection.update({:_id => self._id}, self.to_mongo)
85
+ self.class.collection.update({:_id => sibling._id}, sibling.to_mongo)
86
+
87
+ global_updated!
88
+ end
89
+ def move_higher
90
+ sibling = self.class.first(:position => {"$lt" => self.position}, parent_id_field => self[parent_id_field], :order => 'position DESC')
91
+
92
+ tmp = sibling.position
93
+ sibling.position = self.position
94
+ self.position = tmp
95
+
96
+ self.class.collection.update({:_id => self._id}, self.to_mongo)
97
+ self.class.collection.update({:_id => sibling._id}, sibling.to_mongo)
98
+
99
+ global_updated!
100
+ end
101
+
102
+ def run_callbacks(kind, options = {}, &block)
103
+ self.class.send("#{kind}_callback_chain").run(self, options, &block)
104
+ self.embedded_associations.each do |association|
105
+ self.send(association.name).each do |document|
106
+ document.run_callbacks(kind, options, &block)
107
+ end
108
+ end
109
+ self.embedded_keys.each do |key|
110
+ self.send(key.name).run_callbacks(kind, options, &block) unless self.send(key.name).nil?
111
+ end
112
+ end
113
+
114
+ def slots
115
+ slots = []
116
+ for slot_type in self.class.possible_slots.map(&:to_s)
117
+ self.class.main_slots_count.to_i.times do |i|
118
+ slots << self.send("#{slot_type}_slot_#{i}")
119
+ end
120
+ end
121
+ slots.compact
122
+ end
123
+
124
+ ## CANS
125
+ def all_groups
126
+ updatable_groups | destroyable_groups | publishable_groups
127
+ end
128
+
129
+ %w( updatable destroyable publishable ).each do |permission|
130
+ define_method("#{permission}_by?") do |user|
131
+ user.admin? or send("#{permission}_groups").empty? or user.groups.any?{ |g| send("#{permission}_groups").include?(g) }
132
+ end
133
+
134
+ define_method("#{permission}_groups_list") do
135
+ send("#{permission}_groups").join(', ')
136
+ end
137
+
138
+ define_method("#{permission}_groups_list=") do |groups_string|
139
+ send("#{permission}_groups=", groups_string.split(',').map{|g| g.blank? ? nil : g.strip }.compact.uniq)
140
+ end
141
+ end
142
+
143
+ def creatable_by?(user)
144
+ parent.nil? or parent.updatable_by?(user)
145
+ end
146
+
147
+ # tree method that allow oprions to be passed
148
+ def siblings(options = {})
149
+ self.class.all(options.merge(:_id => {"$ne" => self._id}, parent_id_field => self[parent_id_field], :order => tree_order))
150
+ end
151
+
152
+ def self_and_siblings(options = {})
153
+ self.class.all(options.merge(parent_id_field => self[parent_id_field], :order => tree_order))
154
+ end
155
+
156
+ def children(options = {})
157
+ self.class.all(options.merge(parent_id_field => self._id, :order => tree_order))
158
+ end
159
+
160
+ private
161
+
162
+ def current_time
163
+ self.class.current_time
164
+ end
165
+
166
+ before_validation :set_permalink
167
+ def set_permalink
168
+ self.permalink = Permalink.new(*(ancestors << self).map{ |a| a.title.parameterize }) if permalink.blank? and not title.blank?
169
+ end
170
+
171
+ before_save :set_position
172
+ def set_position
173
+ write_attribute :position, siblings.size if position.nil?
174
+ end
175
+
176
+ before_save :clean_slots
177
+
178
+ # This method removes any uneeded modules from the object
179
+ # modules that would otherwise remain hidden
180
+ # if the objects class was changed
181
+ def clean_slots
182
+
183
+ # TODO: spec this
184
+
185
+ slot_types = self.class.possible_slots.map(&:to_s)
186
+ # collect all of the slot attributes
187
+ # (so we don't have to loop through the whole object each time)
188
+ slots = self.attributes.select{|k,v| k =~ /^(#{slot_types.join('|')})_slot_\d+$/ }
189
+
190
+ # for each type of slot
191
+ for slot_type in slot_types
192
+ # get the number of slots of this type in the (possibly new) class
193
+ slot_count = self._type.constantize.send("#{slot_type}_slots_count").to_i
194
+
195
+ # loop through all of the slot attributes for this type
196
+ slots.select{|k,v| k =~ /^#{slot_type}_slot_\d+$/ }.each do |key, slot|
197
+
198
+ index = key[/#{slot_type}_slot_(\d+)$/, 1].to_i
199
+
200
+ logger.debug "Deleting #{key} #{self.send(key).inspect}" if index >= slot_count
201
+ # set the slot to nil
202
+ write_attribute(key.to_sym, nil) if index >= slot_count
203
+ end
204
+ end
205
+ end
206
+
207
+ before_save :set_path
208
+ def set_path
209
+ write_attribute :path, parent.path + [parent._id] unless parent.nil?
210
+ end
211
+
212
+ before_create :inherit_permisions
213
+ def inherit_permisions
214
+ unless parent.nil?
215
+ self.updatable_groups = parent.updatable_groups
216
+ self.destroyable_groups = parent.destroyable_groups
217
+ self.publishable_groups = parent.publishable_groups
218
+ end
219
+ end
220
+
221
+ after_save :order_siblings
222
+ def order_siblings
223
+ if position_changed?
224
+ self.class.collection.update({:_id => {"$ne" => self._id}, :position => {"$gte" => self.position}, parent_id_field => self[parent_id_field]}, { "$inc" => { :position => 1 }}, { :multi => true })
225
+ self_and_siblings.each_with_index do |sibling, index|
226
+ unless sibling.position == index
227
+ sibling.position = index
228
+ self.class.collection.save(sibling.to_mongo, :safe => true)
229
+ end
230
+ end
231
+ end
232
+ end
233
+
234
+ before_save :set_published
235
+ def set_published
236
+ if publish
237
+ write_attribute :published_at, current_time if published_at.nil?
238
+ write_attribute :published_to, 10.years.from_now if published_to.nil?
239
+ end
240
+ if hide
241
+ write_attribute :published_at, nil
242
+ write_attribute :published_to, 10.years.from_now
243
+ end
244
+ end
245
+
246
+ module ClassMethods
247
+ @@slots = []
248
+
249
+ # Set the names of the slots that will be avaiable to fill with components
250
+ # For each name new methods will be created;
251
+ #
252
+ # <name>_slots(count)
253
+ # This allow you to set the number of slots available in a template
254
+ # <name>_slots_count(count)
255
+ # Reads back the count you set
256
+ def slots(*args)
257
+ @@slots = args.map(&:to_sym).uniq
258
+
259
+ @@slots.each do |slot|
260
+ puts "Noodall::Node Defined slot: #{slot}"
261
+ define_singleton_method("#{slot}_slots") do |count|
262
+ instance_variable_set("@#{slot}_slots_count", count)
263
+ count.times do |i|
264
+ key "#{slot}_slot_#{i}", Noodall::Component
265
+ validates_each "#{slot}_slot_#{i}", :logic => lambda { errors.add("#{slot}_slot_#{i}", "is not allowed in a #{slot} slot") unless send("#{slot}_slot_#{i}").nil? or Noodall::Component.positions_classes(slot).include?(send("#{slot}_slot_#{i}").class) } # TODO: Nicer message please
266
+ validates_associated "#{slot}_slot_#{i}"
267
+ end
268
+ end
269
+ define_singleton_method("#{slot}_slots_count") { instance_variable_get("@#{slot}_slots_count") }
270
+ end
271
+ end
272
+
273
+ def slots_count
274
+ @@slots.inject(0) { |total, slot| total + send("#{slot}_slots_count").to_i }
275
+ end
276
+
277
+ def possible_slots
278
+ @@slots
279
+ end
280
+
281
+ def roots(options = {})
282
+ self.all(options.merge(parent_id_field => nil, :order => tree_order))
283
+ end
284
+
285
+ def find_by_permalink(permalink)
286
+ node = find_one(:permalink => permalink.to_s, :published_at => { :$lte => current_time })
287
+ raise MongoMapper::DocumentNotFound if node.nil? or node.expired?
288
+ node
289
+ end
290
+
291
+ def template_classes
292
+ return @template_classes if @template_classes
293
+ classes = []
294
+ ObjectSpace.each_object(Class) do |c|
295
+ next unless c.ancestors.include?(Noodall::Node) and (c != Noodall::Node) and c.root_template?
296
+ classes << c
297
+ end
298
+ @template_classes = classes
299
+ end
300
+
301
+ def template_names
302
+ template_classes.collect{|c| c.name.titleize}.sort
303
+ end
304
+
305
+ def all_template_classes
306
+ templates = []
307
+ template_classes.each do |template|
308
+ templates << template
309
+ templates = templates + template.template_classes
310
+ end
311
+ templates.uniq.collect{ |c| c.name.titleize }.sort
312
+ end
313
+
314
+ def sub_templates(*arr)
315
+ @template_classes = arr
316
+ end
317
+
318
+ def root_template!
319
+ @root_template = true
320
+ end
321
+
322
+ def root_template?
323
+ @root_template
324
+ end
325
+
326
+ def single_collection_inherited?
327
+ false
328
+ end
329
+
330
+ # Returns a list of classes that can have this model as a child
331
+ def parent_classes
332
+ classes = []
333
+ ObjectSpace.each_object(Class) do |c|
334
+ next unless c.ancestors.include?(Noodall::Node) and (c != Noodall::Node) and c.template_classes.include?(self)
335
+ classes << c
336
+ end
337
+ classes
338
+ end
339
+
340
+ # If rails style time zones are unavaiable fallback to standard now
341
+ def current_time
342
+ Time.zone ? Time.zone.now : Time.now
343
+ end
344
+ end
345
+ extend ClassMethods
346
+
347
+
348
+ end
349
+
350
+ end