Skip to content

Commit

Permalink
Implemented virtual class introspection for zafu and [grid] helper.
Browse files Browse the repository at this point in the history
  • Loading branch information
gaspard committed Nov 22, 2010
1 parent f9c103d commit 523e449
Show file tree
Hide file tree
Showing 13 changed files with 210 additions and 71 deletions.
32 changes: 26 additions & 6 deletions app/models/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,25 +102,40 @@ class Node < ActiveRecord::Base

include Property

# This must come before the first call to make_schema.
include Zena::Use::Kpath::InstanceMethods

def virtual_class
@virtual_class ||= if self[:vclass_id]
VirtualClass.find_by_id(self[:vclass_id])
else
VirtualClass.find_by_name(self.class.name)
end
end

def virtual_class=(vclass)
@virtual_class = vclass
self[:vclass_id] = vclass.id
self[:kpath] = vclass.kpath
end

# We want to use a Role as schema for properties defined in the real_class instead of Property::Schema.
def self.make_schema
::Role.new(:name => name).tap do |role|
role.kpath = self.kpath
role.real_class = self
end
end

alias schema virtual_class

# We use the virtual_class as proxy for method type resolution.
def safe_eval(code)
eval RubyLess.translate(schema, code)
# def safe_eval(code)
# eval RubyLess.translate(schema, code)
# end

def safe_method_type(signature, receiver = nil)
schema.safe_method_type(signature, receiver)
end

# Should be the same serialization as in Version and Site
Expand Down Expand Up @@ -156,7 +171,6 @@ def safe_eval(code)
belongs_to :skin
before_validation :set_defaults
before_validation :node_before_validation
validates_presence_of :title
validate :validate_node
before_create :node_before_create
before_save :change_klass
Expand Down Expand Up @@ -219,7 +233,8 @@ def self.author_proc
:v_publish_from => Time, :v_backup => Boolean,
:zip => Number, :parent_id => {:class => Number, :nil => true, :method => 'parent_zip'},
:user => 'User',
:author => author_proc
:author => author_proc,
:vclass => {:class => 'VirtualClass', :method => 'virtual_class'}

# This is needed so that we can use secure_scope and secure in search.
extend Zena::Acts::Secure
Expand Down Expand Up @@ -385,7 +400,7 @@ def change_to_classes_for_form
def allowed_change_to_classes
change_to_classes_for_form.map {|k,v| v}
end

# TODO: remove and use VirtualClass[...].classes_for_form directly
def classes_for_form(opts={})
VirtualClass[self.name].classes_for_form(opts)
Expand Down Expand Up @@ -872,6 +887,9 @@ def safe_method_type(signature, receiver = nil)
method = signature.first
if type = super
type
elsif method == 'cached_role_ids'
# TODO: how to avoid everything ending in '_id' being caught as relations ?
nil
elsif method =~ /^(.+)_((id|zip|status|comment)(s?))\Z/ && !instance_methods.include?(method)
key = $3 == 'id' ? "zip#{$4}" : $2
{:method => "rel[#{$1.inspect}].try(:other_#{key})", :nil => true, :class => ($4.blank? ? Number : [Number])}
Expand Down Expand Up @@ -1541,6 +1559,8 @@ def node_before_validation

# Make sure the node is complete before creating it (check parent and project references)
def validate_node
errors.add(:title, "Can't be blank.") if title.blank?

if @parent_zip_error
errors.add('parent_id', @parent_zip_error)
@parent_zip_error = nil
Expand Down
17 changes: 16 additions & 1 deletion app/models/role.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ class Role < ActiveRecord::Base
after_destroy :expire_vclass_cache

include RubyLess
safe_method :columns => {:class => ['Column'], :method => 'columns.values', :nil => false}
# All columns defined for a VirtualClass (kpath based).
safe_method :all_columns => {:class => ['Column'], :method => 'zafu_all_columns'}

# Columns defined in the role.
safe_method :columns => {:class => ['Column'], :method => 'zafu_columns'}

safe_method :name => String

# We use property to store index information, default values and such
Expand All @@ -37,7 +42,17 @@ def superclass=(klass)
errors.add('superclass', 'invalid')
end
end

def zafu_all_columns
@zafu_all_columns ||= (roles.flatten.map{|r| r.zafu_columns}).flatten.sort {|a,b| a.name <=> b.name}
end

def zafu_columns
@zafu_columns ||= defined_columns.values.select do |c|
# Allow all dynamic properties and all safe static properties
!real_class || real_class.safe_method_type([c.name])
end.sort {|a,b| a.name <=> b.name}
end

private
def set_defaults
Expand Down
17 changes: 16 additions & 1 deletion app/models/virtual_class.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ class VirtualClass < Role
include Zena::Use::Fulltext::VirtualClassMethods
include Zena::Use::PropEval::VirtualClassMethods
include Zena::Use::ScopeIndex::VirtualClassMethods

safe_method :roles => {:class => ['Role'], :method => 'zafu_roles'}

class Cache
def initialize
Expand Down Expand Up @@ -123,7 +125,7 @@ def build_vclass_from_real_class(real_class)
vclass = VirtualClass.new(:name => real_class.name)
vclass.kpath = real_class.kpath
vclass.real_class = real_class
vclass.include_role real_class
vclass.include_role real_class.schema
vclass
end

Expand Down Expand Up @@ -370,6 +372,19 @@ def import_result
@import_result || errors[:base]
end

# List all roles ordered by ascending kpath and name
def zafu_roles
@zafu_roles ||= roles.flatten.uniq.reject do |r|
r.zafu_columns.empty?
end.sort do |a, b|
if a.kpath == b.kpath
a.name <=> b.name
else
a.kpath <=> b.kpath
end
end
end

private
def attached_roles
::Role.all(
Expand Down
2 changes: 1 addition & 1 deletion config/gems.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ shoulda: '= 2.10.3'

querybuilder: '>= 0.9.3'
yamltest: '>= 0.7.0'
rubyless: '>= 0.8.0'
rubyless: '>= 0.8.1'
property: '>= 2.1.0'
versions: '>= 0.3.1'
zafu: '>= 0.7.5'
Expand Down
8 changes: 5 additions & 3 deletions lib/zena/acts/enrollable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def has_role?(role_id)
end

def zafu_possible_roles
roles = virtual_class.roles.flatten.uniq.select {|r| r.class == Role }
roles = virtual_class.zafu_roles.select {|r| r.class == Role }
roles.empty? ? nil : roles
end

Expand Down Expand Up @@ -159,8 +159,10 @@ def prepare_roles
end

role_ids = []
virtual_class.roles.flatten.uniq.each do |role|
next unless role.class == Role # Do not index VirtualClasses (information exists through kpath).
virtual_class.zafu_roles.each do |role|
# Do not index VirtualClasses (information exists through kpath) and do not
# index static roles.
next unless role.class == Role && role.id
role_ids << role.id if role.column_names & keys != []
end

Expand Down
46 changes: 1 addition & 45 deletions lib/zena/acts/secure_node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def acts_as_secure_node
belongs_to :user
before_validation :secure_reference_before_validation
# we move all before_validation on update and create here so that it is triggered before multiversion's before_validation
before_validation :secure_before_validation
before_validation_on_create :secure_before_validation_on_create

validate :record_must_be_secured
#validate {|r| r.errors.add(:base, 'record not secured') unless r.instance_variable_get(:@visitor)}
Expand All @@ -25,32 +25,6 @@ def acts_as_secure_node
before_destroy :secure_on_destroy

include Zena::Acts::SecureNode::InstanceMethods

class << self

# kpath is a class shortcut to avoid tons of 'OR type = Page OR type = Document'
# we build this path with the first letter of each class. The example bellow
# shows how the kpath is built:
# class hierarchy
# Node --> N
# Note --> NN Page --> NP
# Document Form Section
# NPD NPF NPP
# So now, to get all Pages, your sql becomes : WHERE kpath LIKE 'NP%'
# to get all Documents : WHERE kpath LIKE 'NPD%'
# all pages without Documents : WHERE kpath LIKE 'NP%' AND NOT LIKE 'NPD%'
attr_accessor :kpath

def kpath
@kpath ||= make_kpath
end

private
def make_kpath
superclass.respond_to?(:kpath) ? (superclass.kpath + ksel) : ksel
end
end

extend Zena::Acts::SecureNode::ClassMethods
end

Expand Down Expand Up @@ -144,14 +118,6 @@ def full_drive_was_true?(vis=visitor, ugps=visitor.group_ids)
( vis.user? && ugps.include?(dgroup_id_was) )
end

def secure_before_validation
if new_record?
secure_before_validation_on_create
else
secure_before_validation_on_update
end
end

def secure_before_validation_on_create
# set defaults before validation
self[:site_id] = visitor.site.id
Expand All @@ -174,7 +140,6 @@ def secure_before_validation_on_create
end

def secure_before_validation_on_update
self[:kpath] = self.vclass.kpath if vclass_id_changed? or type_changed?
true
end

Expand Down Expand Up @@ -461,15 +426,6 @@ def clean_options(options)
end
end

# kpath selector for the current class
def ksel
self.to_s[0..0]
end

# Replace Rails subclasses normal behavior
def type_condition
" #{table_name}.kpath LIKE '#{kpath}%' "
end
end # ClassMethods

end #SecureNode
Expand Down
4 changes: 2 additions & 2 deletions lib/zena/acts/serializable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ def default_serialization_options
def export_properties
res = {}
prop = self.prop
schema.column_names.each do |key|
next if key == 'cached_role_ids' # FIXME: use a flag to mark as non-exportable
schema.zafu_all_columns.each do |col|
key = col.name
value = prop[key]
next if value.blank?
res[key] = value
Expand Down
14 changes: 13 additions & 1 deletion lib/zena/use/display.rb
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,19 @@ def r_zena
"<a class='zena' href='http://zenadmin.org' title='Zena <%= Zena::VERSION %>'>#{text}</a>"
end
end


def r_grid
return parser_error("not in a list context") unless node.list_context?
return parser_error("not a Node list") unless node.single_class <= Node
klass = "#{node.single_class.name}Class"
@blocks = [make(:void, :method => 'void', :text => %Q{<table class='grid'>
<tr do='#{klass}' do='roles'><th class='role' colspan='\#{columns.size}' do='each' do='name'/></tr>
<tr do='#{klass}' do='roles' do='each' do='columns'><th do='each' do='name'/></tr>
<tr do='each'><r:#{klass} do='roles' do='each' do='columns'><td do='each' do='@node.send(name)'/></r:#{klass}></tr>
</table>})]
expand_with
end

private
def show_number(method)
if fmt = @params[:format]
Expand Down
60 changes: 60 additions & 0 deletions lib/zena/use/kpath.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
module Zena
module Use
module Kpath
module ClassMethods
# kpath selector for the current class
def ksel
self.to_s[0..0]
end

# Replace Rails subclasses normal behavior
def type_condition
" #{table_name}.kpath LIKE '#{kpath}%' "
end
end # ClassMethods

module InstanceMethods
def self.included(base)
base.class_eval do
extend ClassMethods

before_validation :set_kpath
end

class << base
# The kpath must be set in the class's metaclass so that each sub-class has its own
# @kpath.

# kpath is a class shortcut to avoid tons of 'OR type = Page OR type = Document'
# we build this path with the first letter of each class. The example bellow
# shows how the kpath is built:
# class hierarchy
# Node --> N
# Note --> NN Page --> NP
# Document Form Section
# NPD NPF NPP
# So now, to get all Pages, your sql becomes : WHERE kpath LIKE 'NP%'
# to get all Documents : WHERE kpath LIKE 'NPD%'
# all pages without Documents : WHERE kpath LIKE 'NP%' AND NOT LIKE 'NPD%'
attr_accessor :kpath

def kpath
@kpath ||= make_kpath
end

private
def make_kpath
superclass.respond_to?(:kpath) ? (superclass.kpath + ksel) : ksel
end
end
end

private
def set_kpath
self[:kpath] = self.vclass.kpath if vclass_id_changed? or type_changed?
true
end
end # InstanceMethods
end # Kpath
end # Use
end # Zena

0 comments on commit 523e449

Please sign in to comment.