noodall-core 0.1.0

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