module Noodall
class Node
include MongoMapper::Document
include MongoMapper::Acts::Tree
include Canable::Ables
plugin MongoMapper::Plugins::MultiParameterAttributes
plugin Indexer
plugin Search
plugin Tagging
plugin Noodall::GlobalUpdateTime
key :title, String, :required => true
key :browser_title, String
key :name, String
key :description, String
key :body, String, :default => ""
key :position, Integer, :default => nil, :index => true
key :_type, String
key :published_at, Time, :index => true
key :published_to, Time, :index => true
key :updatable_groups, Array
key :destroyable_groups, Array
key :publishable_groups, Array
key :viewable_groups, Array
key :permalink, Permalink, :required => true, :index => true
key :admin_title, String
timestamps!
userstamps!
enable_versioning
alias_method :keywords, :tag_list
alias_method :keywords=, :tag_list=
attr_accessor :publish, :hide #for publishing
attr_accessor :previous_parent_id, :moved #for redordering
acts_as_tree :order => "position", :search_class => Noodall::Node
# if there are any children that are not of an allowed template, error
validate :child_templates_allowed
scope :published, lambda { where(:published_at => { :$lte => current_time }, :published_to => { :$gte => current_time }) }
def published_children
self.children.select{|c| c.published? }
end
# Allow parent to be set to none using a string. Allows us to set the parent to nil easily via forms
def parent=(var)
self[parent_id_field] = nil
var == "none" ? super(nil) : super
end
def template
self.class.name.titleize
end
def template=(template_name)
self._type = template_name.gsub(' ','') unless template_name.blank?
end
def published?
!published_at.nil? and published_at <= current_time and (published_to.nil? or published_to >= current_time)
end
def has_draft?
!version_at(:latest).nil? && version_at(:latest).pos != version_number
end
def pending?
published_at.nil? or published_at >= current_time
end
def expired?
!published_to.nil? and published_to <= current_time
end
def first?
position == 0
end
def last?
position == siblings.count
end
def move_lower
sibling = search_class.first(:position => {"$gt" => self.position}, parent_id_field => self[parent_id_field], :order => 'position ASC')
switch_position(sibling)
end
def move_higher
sibling = search_class.first(:position => {"$lt" => self.position}, parent_id_field => self[parent_id_field], :order => 'position DESC')
switch_position(sibling)
end
#def run_callbacks(kind, options = {}, &block)
#self.class.send("#{kind}_callback_chain").run(self, options, &block)
#self.embedded_associations.each do |association|
#self.send(association.name).each do |document|
#document.run_callbacks(kind, options, &block)
#end
#end
#self.embedded_keys.each do |key|
#self.send(key.name).run_callbacks(kind, options, &block) unless self.send(key.name).nil?
#end
#end
def slots
slots = []
for slot_type in self.class.possible_slots.map(&:to_s)
self.class.send("#{slot_type}_slots_count").to_i.times do |i|
slots << self.send("#{slot_type}_slot_#{i}")
end
end
slots.compact
end
## CANS
def all_groups
updatable_groups | destroyable_groups | publishable_groups | viewable_groups
end
%w( updatable destroyable publishable viewable ).each do |permission|
define_method("#{permission}_by?") do |user|
user.admin? or send("#{permission}_groups").empty? or user.groups.any?{ |g| send("#{permission}_groups").include?(g) }
end
define_method("#{permission}_groups_list") do
send("#{permission}_groups").join(', ')
end
define_method("#{permission}_groups_list=") do |groups_string|
send("#{permission}_groups=", groups_string.downcase.split(',').map{|g| g.blank? ? nil : g.strip }.compact.uniq)
end
end
def creatable_by?(user)
parent.nil? or parent.updatable_by?(user)
end
def siblings
search_class.where(:_id => {:$ne => self._id}, parent_id_field => self[parent_id_field]).order(tree_order)
end
def self_and_siblings
search_class.where(parent_id_field => self[parent_id_field]).order(tree_order)
end
def children
search_class.where(parent_id_field => self._id).order(tree_order)
end
def in_site_map?
Noodall::Site.contains?(self.permalink.to_s)
end
# A slug for creating the permalink
def slug
(self.name.blank? ? self.title : self.name).to_s.parameterize
end
def admin_title
name
end
private
before_save :set_admin_title
def set_admin_title
self.admin_title = admin_title
end
def switch_position(sibling)
tmp = sibling.position
sibling.position = self.position
self.position = tmp
search_class.collection.update({:_id => self._id}, self.to_mongo)
search_class.collection.update({:_id => sibling._id}, sibling.to_mongo)
global_updated!
end
def current_time
self.class.current_time
end
before_validation :set_permalink
def set_permalink
if permalink.blank?
# inherit the parents permalink and append the current node's .name or .title attribute
# this code takes name over title for the current node's slug
# this way enables children to inherit the parent's custom (user defined) permalink also
permalink_args = self.parent.nil? ? [] : self.parent.permalink.dup
permalink_args << self.slug unless self.slug.blank?
self.permalink = Permalink.new(*permalink_args)
end
end
before_save :set_position
def set_position
write_attribute :position, siblings.size if position.nil?
end
before_save :clean_slots
# This method removes any uneeded modules from the object
# modules that would otherwise remain hidden
# if the objects class was changed
def clean_slots
# TODO: spec this
slot_types = self.class.possible_slots.map(&:to_s)
# collect all of the slot attributes
# (so we don't have to loop through the whole object each time)
slots = self.attributes.select{|k,v| k =~ /^(#{slot_types.join('|')})_slot_\d+$/ }
# for each type of slot
for slot_type in slot_types
# get the number of slots of this type in the (possibly new) class
slot_count = self._type.constantize.send("#{slot_type}_slots_count").to_i
# loop through all of the slot attributes for this type
slots.select{|k,v| k =~ /^#{slot_type}_slot_\d+$/ }.each do |key, slot|
index = key[/#{slot_type}_slot_(\d+)$/, 1].to_i
# set the slot to nil
write_attribute(key.to_sym, nil) if index >= slot_count
end
end
end
before_save :set_path
def set_path
write_attribute :path, parent.path + [parent._id] unless parent.nil?
end
before_create :inherit_permisions
def inherit_permisions
unless parent.nil?
self.updatable_groups = parent.updatable_groups
self.destroyable_groups = parent.destroyable_groups
self.publishable_groups = parent.publishable_groups
self.viewable_groups = parent.viewable_groups
end
end
before_update :move_check
def move_check
set_previous_parent if self.parent_id_changed?
self.moved = true if self.position_changed? or self.parent_id_changed?
end
before_destroy :set_previous_parent #so the child list it was removed from normalises order
def set_previous_parent
self.previous_parent_id = self.parent_id_was
end
before_create :set_moved #so if it is placed at the top of list it normalises order
def set_moved
self.moved = true
end
after_save :order_siblings
def order_siblings
search_class.collection.update({:_id => {"$ne" => self._id}, :position => {"$gte" => self.position}, parent_id_field => self[parent_id_field]}, { "$inc" => { :position => 1 }}, { :multi => true }) if moved
self_and_siblings.each_with_index do |sibling, index|
unless sibling.position == index
sibling.position = index
search_class.collection.save(sibling.to_mongo, :safe => true)
end
end
order_previous_siblings
end
after_destroy :order_previous_siblings
def order_previous_siblings
unless previous_parent_id.nil?
search_class.where(parent_id_field => previous_parent_id).order(tree_order).each_with_index do |sibling, index|
unless sibling.position == index
sibling.position = index
search_class.collection.save(sibling.to_mongo, :safe => true)
end
end
end
end
before_save :set_published
def set_published
if publish
write_attribute :published_at, current_time if published_at.nil?
write_attribute :published_to, 10.years.from_now if published_to.nil?
end
if hide
write_attribute :published_at, nil
write_attribute :published_to, 10.years.from_now
end
end
before_save :set_name
def set_name
self.name = self.title if self.name.blank?
end
# Validate that child templates (set via sub_templates) are allowed if the template is changed
def child_templates_allowed
unless !_type_changed? or children.empty?
errors.add(:base, "Template cannot be changed as sub content is not allowed in this template") unless children.select{|c| !self._type.constantize.template_classes.include?(c.class)}.empty?
end
end
class << self
@@slots = []
# DEPRECATED: Please use slot instead.
#
# Set the names of the slots that will be avaiable to fill with components
# For each name new methods will be created;
#
# _slots(count)
# This allow you to set the number of slots available in a template
# _slots_count(count)
# Reads back the count you set
def slots(*args)
warn "[DEPRECATION] `slots` is deprecated. Please use `slot` instead."
slots = args.map(&:to_sym).uniq
slots.each do |s|
slot(s)
end
end
# Define a slot type and what components are allowed to be place in that
# slot type.
#
# Generates methods in Noodall::Node models that allow you to set and read the
# number of slots of the name defined
#
# Noodall::Node.slot :small, Gallery, Picture
#
# class NicePage < Noodall::Node
# small_slots 3
# end
#
# NicePage.small_slots_count # => 3
#
# n = NicePage.new
# n.small_slot_0 = Gallery.new(...)
#
def slot(slot_name, *allowed_components)
if @@slots.include?(slot_name.to_sym)
warn "[WARNING] Overriding slot definition"
else
@@slots << slot_name.to_sym
puts "Noodall::Node Defined slot: #{slot_name}"
define_singleton_method("#{slot_name}_slots") do |count|
instance_variable_set("@#{slot_name}_slots_count", count)
count.times do |i|
slot_sym = "#{slot_name}_slot_#{i}".to_sym
key slot_sym, Noodall::Component
validates slot_sym, :slot => { :slot_type => slot_name }
validates_associated slot_sym
end
end
define_singleton_method("#{slot_name}_slot_components") do
class_variable_get "@@#{slot_name}_slot_components".to_sym
end
define_singleton_method("#{slot_name}_slots_count") do
instance_variable_get("@#{slot_name}_slots_count")
end
end
class_variable_set "@@#{slot_name}_slot_components".to_sym, allowed_components
end
def slots_count
@@slots.inject(0) { |total, slot| total + send("#{slot}_slots_count").to_i }
end
def possible_slots
@@slots
end
def roots(options = {})
self.where(options.reverse_merge({parent_id_field => nil})).order(tree_order)
end
def find_by_permalink(permalink)
node = find_one(:permalink => permalink.to_s, :published_at => { :$lte => current_time })
raise MongoMapper::DocumentNotFound if node.nil? or node.expired?
node
end
def template_classes
return root_templates if self == Noodall::Node
@template_classes || []
end
def template_names
template_classes.map{|c| c.name.titleize }.sort
end
# Returns a lst of all node template classes available in
# in the tree
def all_template_classes
templates = []
root_templates.each do |template|
templates << template
templates = templates + template.template_classes
end
templates.uniq
end
def all_template_names
all_template_classes.map{|c| c.name.titleize }.sort
end
# Set the Node templates that can be a child of this templates
# in the tree
def sub_templates(*arr)
@template_classes = arr
end
@@root_templates = []
# Set the Node templates that can be a root of a tree
#
# Noodall::Node.root_templates Home, LandingPage
#
# Returns a list of the root templates
#
# Noodall::Node.root_templates # => [Home, LandingPage]
def root_templates(*templates)
@@root_templates = templates unless templates.empty?
@@root_templates
end
# DEPRECATED: Please use root_templates/tt> instead.
def root_template!
warn "[DEPRECATION] `root_template` is deprecated. Please use `root_templates` instead."
@@root_templates << self
end
def root_template?
@@root_templates.include?(self)
end
# Returns a list of classes that can have this model as a child
def parent_classes
all_template_classes.find_all do |c|
c.template_classes.include?(self)
end
end
# If rails style time zones are unavaiable fallback to standard now
def current_time
Time.zone ? Time.zone.now : Time.now
end
end
class SlotValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value.nil? or Noodall::Component.positions_classes(options[:slot_type]).one?{|c| c.name == value._type }
record.errors[attribute] << "cannnot contain a #{value.class.name.humanize} component"
end
end
end
end
end