lolita 3.0.7 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (128) hide show
  1. data/GUIDELINE +4 -0
  2. data/README.rdoc +50 -0
  3. data/VERSION +1 -1
  4. data/app/controllers/lolita/rest_controller.rb +27 -11
  5. data/app/helpers/components/lolita/configuration/list_component.rb +11 -0
  6. data/app/helpers/lolita_helper.rb +1 -1
  7. data/app/views/components/lolita/configuration/field/_display.html.erb +9 -0
  8. data/app/views/components/lolita/configuration/field/_label.html.erb +1 -0
  9. data/app/views/components/lolita/{field → configuration/field}/_object.html.erb +0 -0
  10. data/app/views/components/lolita/configuration/field/array/_display.html.erb +5 -0
  11. data/app/views/components/lolita/configuration/field/array/habtm/_display.html.erb +26 -0
  12. data/app/views/components/lolita/configuration/field/array/select/_display.html.erb +5 -0
  13. data/app/views/components/lolita/configuration/field/big_decimal/_display.html.erb +1 -0
  14. data/app/views/components/lolita/{field/_boolean.html.erb → configuration/field/boolean/_display.html.erb} +0 -0
  15. data/app/views/components/lolita/configuration/field/date/_display.html.erb +1 -0
  16. data/app/views/components/lolita/{field/_string.html.erb → configuration/field/float/_display.html.erb} +0 -0
  17. data/app/views/components/lolita/configuration/field/integer/_display.html.erb +1 -0
  18. data/app/views/components/lolita/configuration/field/string/_display.html.erb +5 -0
  19. data/app/views/components/lolita/{field/_disabled.html.erb → configuration/field/string/disabled/_display.html.erb} +0 -0
  20. data/app/views/components/lolita/{field/_password.html.erb → configuration/field/string/password/_display.html.erb} +0 -0
  21. data/app/views/components/lolita/configuration/field/string/text/_display.html.erb +34 -0
  22. data/app/views/components/lolita/configuration/field/time/_display.html.erb +1 -0
  23. data/app/views/components/lolita/{field_set → configuration/field_set}/_display.html.erb +1 -1
  24. data/app/views/components/lolita/configuration/list/_body.html.erb +5 -0
  25. data/app/views/components/lolita/{list → configuration/list}/_body_cell.html.erb +0 -0
  26. data/app/views/components/lolita/{list → configuration/list}/_checkbox_cell.html.erb +0 -0
  27. data/app/views/components/lolita/{list → configuration/list}/_checkbox_header.html.erb +0 -0
  28. data/app/views/components/lolita/configuration/list/_display.html.erb +12 -0
  29. data/app/views/components/lolita/configuration/list/_filter.html.erb +8 -0
  30. data/app/views/components/lolita/configuration/list/_header.html.erb +9 -0
  31. data/app/views/components/lolita/{list → configuration/list}/_header_cell.html.erb +0 -0
  32. data/app/views/components/lolita/{list → configuration/list}/_new_resource.html.erb +0 -0
  33. data/app/views/components/lolita/configuration/list/_paginator.html.erb +3 -0
  34. data/app/views/components/lolita/configuration/list/_row.html.erb +7 -0
  35. data/app/views/components/lolita/{list → configuration/list}/_title.html.erb +0 -0
  36. data/app/views/components/lolita/{list → configuration/list}/_tool_cell.html.erb +0 -0
  37. data/app/views/components/lolita/{list → configuration/list}/_tool_header.html.erb +0 -0
  38. data/app/views/components/lolita/configuration/tab/_display.html.erb +17 -0
  39. data/app/views/components/lolita/configuration/tab/_fields.html.erb +7 -0
  40. data/app/views/components/lolita/configuration/tab/content/_display.html.erb +1 -0
  41. data/app/views/components/lolita/configuration/tab/default/_display.html.erb +9 -0
  42. data/app/views/components/lolita/{tabs → configuration/tabs}/_display.html.erb +4 -3
  43. data/app/views/{lolita/layouts → layouts/lolita}/application.html.erb +0 -0
  44. data/app/views/{lolita/layouts → layouts/lolita}/application.html.erb_spec.rb +0 -0
  45. data/author +1 -1
  46. data/lib/lolita.rb +31 -12
  47. data/lib/lolita/adapter/active_record.rb +16 -5
  48. data/lib/lolita/adapter/mongoid.rb +2 -2
  49. data/lib/lolita/base_configuration.rb +44 -2
  50. data/lib/lolita/builder.rb +31 -14
  51. data/lib/lolita/configuration/column.rb +92 -86
  52. data/lib/lolita/configuration/columns.rb +65 -65
  53. data/lib/lolita/configuration/factory.rb +8 -8
  54. data/lib/lolita/configuration/field.rb +156 -100
  55. data/lib/lolita/configuration/field/array.rb +74 -0
  56. data/lib/lolita/configuration/field/big_decimal.rb +12 -0
  57. data/lib/lolita/configuration/field/boolean.rb +7 -5
  58. data/lib/lolita/configuration/field/date.rb +13 -0
  59. data/lib/lolita/configuration/field/integer.rb +7 -5
  60. data/lib/lolita/configuration/field/string.rb +8 -6
  61. data/lib/lolita/configuration/field/time.rb +13 -0
  62. data/lib/lolita/configuration/fields.rb +36 -0
  63. data/lib/lolita/configuration/filter.rb +63 -0
  64. data/lib/lolita/configuration/list.rb +101 -91
  65. data/lib/lolita/configuration/page.rb +1 -0
  66. data/lib/lolita/configuration/tab.rb +137 -131
  67. data/lib/lolita/configuration/tab/content.rb +14 -12
  68. data/lib/lolita/configuration/tab/default.rb +15 -13
  69. data/lib/lolita/configuration/tabs.rb +2 -2
  70. data/lib/lolita/controllers/component_helpers.rb +26 -14
  71. data/lib/lolita/controllers/internal_helpers.rb +14 -0
  72. data/lib/lolita/controllers/url_helpers.rb +47 -10
  73. data/lib/lolita/dbi/base.rb +50 -50
  74. data/lib/lolita/errors.rb +2 -2
  75. data/lib/lolita/hooks.rb +298 -0
  76. data/lib/lolita/hooks/named_hook.rb +122 -0
  77. data/lib/lolita/lazy_loader.rb +46 -46
  78. data/lib/lolita/mapping.rb +3 -2
  79. data/lib/lolita/navigation.rb +48 -0
  80. data/lib/lolita/observed_array.rb +7 -0
  81. data/lib/lolita/rails/routes.rb +29 -3
  82. data/lolita.gemspec +65 -48
  83. data/public/javascripts/lolita/tab.js +5 -0
  84. data/public/javascripts/rails.js +137 -137
  85. data/public/stylesheets/lolita/style.css +3 -1
  86. data/spec/builder_spec.rb +42 -0
  87. data/spec/configuration/field_spec.rb +29 -18
  88. data/spec/configuration/filter_spec.rb +60 -0
  89. data/spec/configuration/tab_spec.rb +28 -20
  90. data/spec/configuration/tabs_spec.rb +8 -4
  91. data/spec/controllers/lolita_rest_spec.rb +15 -0
  92. data/spec/hooks_spec.rb +191 -0
  93. data/spec/lolita_spec.rb +6 -4
  94. data/spec/navigation/tree_spec.rb +59 -0
  95. data/spec/rails_app/app/mongoid/post.rb +2 -0
  96. data/spec/rails_app/app/views/components/lolita/{list → configuration/list}/_body_cell.html.erb +0 -0
  97. data/spec/rails_app/config/application.rb +1 -0
  98. data/spec/rails_app/lib/lolita/configuration/field/my_custom_collection.rb +14 -0
  99. data/spec/simple_spec_helper.rb +1 -0
  100. data/spec/spec_helper.rb +1 -2
  101. metadata +66 -49
  102. data/README.md +0 -5
  103. data/app/helpers/components/lolita/list_component.rb +0 -9
  104. data/app/views/components/lolita/field/_collection.html.erb +0 -5
  105. data/app/views/components/lolita/field/_date.html.erb +0 -1
  106. data/app/views/components/lolita/field/_datetime.html.erb +0 -1
  107. data/app/views/components/lolita/field/_display.html.erb +0 -6
  108. data/app/views/components/lolita/field/_integer.html.erb +0 -1
  109. data/app/views/components/lolita/field/_label.html.erb +0 -1
  110. data/app/views/components/lolita/field/_select.html.erb +0 -1
  111. data/app/views/components/lolita/field/_text.html.erb +0 -27
  112. data/app/views/components/lolita/list/_body.html.erb +0 -5
  113. data/app/views/components/lolita/list/_display.html.erb +0 -11
  114. data/app/views/components/lolita/list/_header.html.erb +0 -9
  115. data/app/views/components/lolita/list/_paginator.html.erb +0 -4
  116. data/app/views/components/lolita/list/_row.html.erb +0 -7
  117. data/app/views/components/lolita/tab/_content.html.erb +0 -1
  118. data/app/views/components/lolita/tab/_default.html.erb +0 -11
  119. data/app/views/components/lolita/tab/_display.html.erb +0 -7
  120. data/app/views/components/lolita/tab/_fields.html.erb +0 -7
  121. data/lib/lolita/configuration/field/collection.rb +0 -71
  122. data/lib/lolita/configuration/field/datetime.rb +0 -10
  123. data/lib/lolita/configuration/field/disabled.rb +0 -10
  124. data/lib/lolita/configuration/field/password.rb +0 -10
  125. data/lib/lolita/configuration/field/text.rb +0 -10
  126. data/lib/lolita/hooks/base.rb +0 -58
  127. data/lib/lolita/hooks/component.rb +0 -15
  128. data/lib/lolita/hooks/hooks.rb +0 -15
@@ -1,20 +1,22 @@
1
1
  module Lolita
2
2
  module Configuration
3
- class ContentTab < Lolita::Configuration::Tab
3
+ module Tab
4
+ class Content < Lolita::Configuration::Tab::Base
4
5
 
5
- def initialize(dbi,*args,&block)
6
- @type=:content
7
- super
8
- set_default_fields
9
- end
6
+ def initialize(dbi,*args,&block)
7
+ @type=:content
8
+ super
9
+ set_default_fields
10
+ end
10
11
 
11
- private
12
+ private
12
13
 
13
14
 
14
- def set_default_fields
15
- default_fields if @fields.empty?
16
- end
17
-
18
- end
15
+ def set_default_fields
16
+ default_fields if @fields.empty?
17
+ end
18
+
19
+ end
20
+ end
19
21
  end
20
22
  end
@@ -1,22 +1,24 @@
1
1
  module Lolita
2
2
  module Configuration
3
- class DefaultTab < Lolita::Configuration::Tab
3
+ module Tab
4
+ class Default < Lolita::Configuration::Tab::Base
4
5
 
5
- def initialize *args,&block
6
- self.type=:default
7
- super
8
- end
6
+ def initialize *args,&block
7
+ self.type=:default
8
+ super
9
+ end
9
10
 
10
- private
11
+ private
11
12
 
12
- def set_default_fields
13
- warn("Default fields are not set for DefaultTab.")
14
- end
13
+ def set_default_fields
14
+ warn("Default fields are not set for DefaultTab.")
15
+ end
15
16
 
16
- def validate
17
- super
18
- if fields.empty?
19
- raise Lolita::NoFieldsGivenError, "At least one field must be specified for default tab."
17
+ def validate
18
+ super
19
+ if fields.empty?
20
+ raise Lolita::NoFieldsGivenError, "At least one field must be specified for default tab."
21
+ end
20
22
  end
21
23
  end
22
24
  end
@@ -105,7 +105,7 @@ module Lolita
105
105
  end
106
106
 
107
107
  def default_tab_types
108
- Lolita::Configuration::Tab.default_types
108
+ Lolita::Configuration::Tab::Base.default_types
109
109
  end
110
110
 
111
111
  def set_tab_attributes(tab)
@@ -120,7 +120,7 @@ module Lolita
120
120
 
121
121
  def build_element(element,&block)
122
122
  current_tab=if element.is_a?(Hash) || element.is_a?(Symbol)
123
- Lolita::Configuration::Tab.new(@dbi,element,&block)
123
+ Lolita::Configuration::Tab.add(@dbi,element,&block)
124
124
  else
125
125
  element
126
126
  end
@@ -8,7 +8,7 @@ module Lolita
8
8
  # Component should have fallowing module structure Components::[NameSpace]::[Component name]Component
9
9
  # Components::Lolita::ListComponent
10
10
  # ====Example
11
- # render_component :"lolita/list", :dispaly
11
+ # render_component :"lolita/configuration/list", :dispaly
12
12
  # # try to find /helpers/components/lolita/list_component.rb in every directory in $: that
13
13
  # # ends with /helpers
14
14
  # # require this file if found and extend self with Components::Lolita::ListComponent.
@@ -26,18 +26,20 @@ module Lolita
26
26
  # render_component "lolita/list", :display
27
27
  # render_component "lolita/list/display"
28
28
  def render_component *args
29
+
29
30
  @opts=args.extract_options!
30
31
  name=args[0]
31
32
  state=args[1]
32
33
  format=@opts.delete(:format)
33
34
  raise "Can't render component without name!" unless name
34
35
  will_use_component name
36
+ partial_name=File.join("/components",name.to_s,state ? state.to_s : nil)
35
37
  if format
36
38
  with_format(format) do
37
- render(:partial=>"/components/#{name}#{state ? "/#{state}" : nil}",:locals=>@opts)
39
+ render(:partial=>partial_name,:locals=>@opts)
38
40
  end
39
41
  else
40
- render(:partial=>"/components/#{name}#{state ? "/#{state}" : nil}",:locals=>@opts)
42
+ render(:partial=>partial_name,:locals=>@opts)
41
43
  end
42
44
  end
43
45
 
@@ -51,26 +53,36 @@ module Lolita
51
53
 
52
54
  # Require component helper file and extend current instance with component helper module.
53
55
  # ====Example
54
- # will_use_component :"lolita/list"
56
+ # will_use_component :"lolita/configuration/list"
55
57
  def will_use_component component_name
56
- @used_component_helpers||=[]
57
- unless @used_component_helpers.include?(component_name)
58
- if path=component_helper_path(component_name)
59
- self.class.class_eval do
60
- require path
58
+ helpers_for_component(component_name) do |possible_component_name|
59
+ @used_component_helpers||=[]
60
+ unless @used_component_helpers.include?(possible_component_name)
61
+ if path=component_helper_path(possible_component_name)
62
+ self.class.class_eval do
63
+ require path
64
+ end
65
+ class_name=possible_component_name.to_s.camelize
66
+ self.extend("Components::#{class_name}Component".constantize) rescue nil #FIXME too slow
61
67
  end
62
- class_name=component_name.to_s.camelize
63
- self.extend("Components::#{class_name}Component".constantize)
68
+ @used_component_helpers<<possible_component_name
64
69
  end
65
- @used_component_helpers<<component_name
66
70
  end
67
71
  end
68
72
 
73
+ def helpers_for_component component_name
74
+ names=component_name.to_s.split("/")
75
+ start_index=1 # first is lolita
76
+ start_index.upto(names.size) do |index|
77
+ yield names.slice(0..index).join("/")
78
+ end
79
+ end
80
+
69
81
  # Find path for given component.
70
82
  #
71
83
  # component_helper_path :"lolita/list" #=> [path_to_lolita]/app/helpers/components/lolita/list_component.rb
72
84
  def component_helper_path component_name
73
- helper_paths=$:.reject{|p| !p.match(/\/helpers$/)}
85
+ @helper_paths||=$:.reject{|p| !p.match(/\/helpers$/)}
74
86
  get_path=lambda{|paths|
75
87
  extra_path=component_name.to_s.split("/")
76
88
  component=extra_path.pop
@@ -82,7 +94,7 @@ module Lolita
82
94
  end
83
95
  nil
84
96
  }
85
- path=get_path.call(helper_paths)
97
+ path=get_path.call(@helper_paths)
86
98
  path
87
99
  end
88
100
 
@@ -11,6 +11,7 @@ module Lolita
11
11
 
12
12
  helper_method *helpers
13
13
  prepend_before_filter :is_lolita_resource?
14
+ prepend_around_filter :switch_locale
14
15
  end
15
16
 
16
17
  # Return instance variable named as resource
@@ -63,11 +64,24 @@ module Lolita
63
64
  end
64
65
 
65
66
  def build_response_for(conf_part,options={})
67
+ # FIXME when asked for some resources that always create new object, there may
68
+ # not be any args, like lolita.report on something like that
69
+
66
70
  @component_options=options
67
71
  @component_object=resource_class.lolita.send(conf_part.to_sym)
68
72
  @component_builder=@component_object.build(@component_options)
69
73
  end
70
74
 
75
+
76
+ private
77
+
78
+ def switch_locale
79
+ old_locale=I18n.locale
80
+ Lolita.locale=params[:locale]
81
+ I18n.locale=Lolita.locale
82
+ yield
83
+ I18n.locale=old_locale
84
+ end
71
85
  end
72
86
  end
73
87
  end
@@ -2,24 +2,60 @@ module Lolita
2
2
  module Controllers
3
3
  module UrlHelpers
4
4
 
5
- protected
5
+ def self.included(model_klass)
6
+ model_klass.class_eval do
7
+
8
+ # Overrides url_for when controller or view uses this helper.
9
+ # It catches hash options and replaces with lolita named route
10
+ # Without this method routes always looks like /lolita/rest/[method]
11
+ # ====Example
12
+ # # in routes.rb
13
+ # lolita_for :posts
14
+ # # GET /lolita/posts
15
+ # # in view
16
+ # url_for #=> /lolita/posts
17
+ # url_for(:controller=>"/posts",:action=>:index) #=> /posts
18
+ # # GET /posts
19
+ # url_for #=> /posts
20
+ def url_for_with_lolita options = {}
21
+ if options.is_a?(Hash) && !options[:use_route] && self.respond_to?(:lolita_mapping)
22
+ controller = options[:controller].to_s
23
+ if Lolita.mappings[lolita_mapping.name].controllers.values.include?(controller)
24
+ resource_type = {
25
+ :index => :lolita_resources_path,
26
+ :new => :new_lolita_resource_path,
27
+ :edit => :edit_lolita_resource_path,
28
+ :create => :lolita_resources_path
29
+ }
30
+ action = (options[:action] || params[:action]).to_sym
31
+ options = self.send(resource_type[action] || :lolita_resource_path,options)
32
+ end
33
+ end
34
+ url_for_without_lolita(options)
35
+ end
36
+ alias_method_chain :url_for, :lolita
37
+
38
+ end
39
+ end
6
40
 
41
+ protected
42
+
7
43
  def lolita_resources_path(*args)
8
44
  options=args.extract_options!
9
45
  mapping=args[0]
10
- send(lolita_resource_name(mapping,nil,true),options)
46
+ send(lolita_resource_name(:mapping=>mapping,:plural=>true),options)
11
47
  end
12
-
48
+
13
49
  def lolita_resource_path(*args) # TODO test
14
50
  options=args.extract_options!
15
51
  mapping=args[0]
16
- send(lolita_resource_name(mapping),options)
52
+ send(lolita_resource_name(:mapping=>mapping),options)
17
53
  end
18
54
 
19
55
  def new_lolita_resource_path(*args)
20
56
  options=args.extract_options!
21
57
  mapping=args[0]
22
- send(lolita_resource_name(mapping,:new),options)
58
+ send(lolita_resource_name(:mapping=>mapping,:action=>:new),options)
23
59
  end
24
60
 
25
61
  def edit_lolita_resource_path(*args)
@@ -27,14 +63,15 @@ module Lolita
27
63
  options[:id]||=resource.id if resource
28
64
  raise "Can edit resource without id." unless options[:id]
29
65
  mapping=args[0]
30
- send(lolita_resource_name(mapping,:edit),options)
66
+ send(lolita_resource_name(:mapping=>mapping,:action=>:edit),options)
31
67
  end
32
68
 
33
- def lolita_resource_name(mapping=nil,action=nil,plural=nil) #TODO test if path is right
34
- mapping=(mapping||lolita_mapping)
35
- name=!plural ? mapping.name : mapping.plural
69
+ def lolita_resource_name(options={}) #TODO test if path is right
70
+ options.assert_valid_keys(:mapping,:plural,:action)
71
+ mapping=(options[:mapping]||lolita_mapping)
72
+ name=!options[:plural] ? mapping.name : mapping.plural
36
73
  name="#{mapping.path}_#{name}"
37
- :"#{action}#{action ? "_" : ""}#{name}_path"
74
+ :"#{options[:action]}#{options[:action] ? "_" : ""}#{name}_path"
38
75
  end
39
76
  end
40
77
  end
@@ -1,50 +1,50 @@
1
- module Lolita
2
- module DBI
3
- # Lolita::DBI::Base is DataBase Interface class, that handle the request to ORM classes.
4
- # Depending on given class DBI::Base detect which ORM class is used and include right adapter
5
- # for that class. Other Lolita classes that need to manipulate with data need to have dbi object
6
- # or it can be created in that class.
7
- # Lolita::DBI::Base support Mongoid and ActiveRecord::Base, or details see Lolita::Adapter.
8
- class Base
9
-
10
- attr_reader :adapter_name #return connected adapter name
11
- attr_reader :klass # return related orm class object
12
- attr_reader :adapter # connected Adaptee for adapter
13
- # Expect ORM class that is supported by Lolita. See Adapter for available adapters.
14
- def initialize(class_object)
15
- @klass=class_object
16
- detect_adapter
17
- connect_adapter
18
- end
19
-
20
- # Detect which ORM class is given and based on it connect Adapter.
21
- def detect_adapter
22
- if defined?(Mongoid) && defined?(Mongoid::Document) && self.klass.ancestors.include?(Mongoid::Document)
23
- @adapter_name=:mongoid
24
- elsif defined?(ActiveRecord) && defined?(ActiveRecord::Base) && self.klass.ancestors.include?(ActiveRecord::Base)
25
- @adapter_name=:active_record
26
- else
27
- raise NotORMClassError.new("Lolita::DBI::Base can not find appropriate #{self.klass} class adapter.")
28
- end
29
- end
30
-
31
- # Connect Adapter by including adapter module into DBI::Base class.
32
- def connect_adapter()
33
- @adapter="Lolita::Adapter::#{self.adapter_name.to_s.camelize}".constantize.new(self)
34
- end
35
-
36
- def method_missing(metod,*args,&block)
37
- @adapter.send(metod,*args,&block)
38
- end
39
-
40
- class << self
41
- # Return Array of available adapters.
42
- def adapters
43
- Dir[File.expand_path(File.join(File.dirname(__FILE__),'..','adapter','**','*.rb'))].map {|f|
44
- File.basename(f,".rb").to_sym
45
- }.reject{|el| el==:abstract_adapter}
46
- end
47
- end
48
- end
49
- end
50
- end
1
+ module Lolita
2
+ module DBI
3
+ # Lolita::DBI::Base is DataBase Interface class, that handle the request to ORM classes.
4
+ # Depending on given class DBI::Base detect which ORM class is used and include right adapter
5
+ # for that class. Other Lolita classes that need to manipulate with data need to have dbi object
6
+ # or it can be created in that class.
7
+ # Lolita::DBI::Base support Mongoid and ActiveRecord::Base, or details see Lolita::Adapter.
8
+ class Base
9
+
10
+ attr_reader :adapter_name #return connected adapter name
11
+ attr_reader :klass # return related orm class object
12
+ attr_reader :adapter # connected Adaptee for adapter
13
+ # Expect ORM class that is supported by Lolita. See Adapter for available adapters.
14
+ def initialize(class_object)
15
+ @klass=class_object
16
+ detect_adapter
17
+ connect_adapter
18
+ end
19
+
20
+ # Detect which ORM class is given and based on it connect Adapter.
21
+ def detect_adapter
22
+ if defined?(Mongoid) && defined?(Mongoid::Document) && self.klass.ancestors.include?(Mongoid::Document)
23
+ @adapter_name=:mongoid
24
+ elsif defined?(ActiveRecord) && defined?(ActiveRecord::Base) && self.klass.ancestors.include?(ActiveRecord::Base)
25
+ @adapter_name=:active_record
26
+ else
27
+ raise NotORMClassError.new("Lolita::DBI::Base can not find appropriate #{self.klass} class adapter.")
28
+ end
29
+ end
30
+
31
+ # Connect Adapter by including adapter module into DBI::Base class.
32
+ def connect_adapter()
33
+ @adapter="Lolita::Adapter::#{self.adapter_name.to_s.camelize}".constantize.new(self)
34
+ end
35
+
36
+ def method_missing(metod,*args,&block)
37
+ @adapter.send(metod,*args,&block)
38
+ end
39
+
40
+ class << self
41
+ # Return Array of available adapters.
42
+ def adapters
43
+ Dir[File.expand_path(File.join(File.dirname(__FILE__),'..','adapter','**','*.rb'))].map {|f|
44
+ File.basename(f,".rb").to_sym
45
+ }.reject{|el| el==:abstract_adapter}
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -7,7 +7,7 @@ module Lolita
7
7
  class FieldTypeError < ArgumentError; end
8
8
  class ModuleNotFound < ArgumentError; end
9
9
  class NotFound < ArgumentError; end
10
- class TabNotFoundError < NameError; end
11
- class FieldNotFoundError < NameError; end
10
+ class ConfigurationClassNotFound < NameError; end
12
11
  class AssociationError < ArgumentError; end
12
+ class HookNotFound < ArgumentError;end
13
13
  end
@@ -0,0 +1,298 @@
1
+ module Lolita
2
+ # Provide hook mechanism for Lolita. To use hooks for class start with including this in your own class.
3
+ # Next step is hook definition. This may be done using Lolita::Hooks#add_hook method.
4
+ # Hooks are stored in class <i>@hooks</i> variable, that is Hash and each key is hook name
5
+ # and each hook also is Hash that have <em>:methods</em> and <em>:blocks</em>
6
+ # keys. Both of those are Array, and each time you call callback method, like <i>before_save</i> and so on, block
7
+ # and/or methods is stored. <b>Each time</b> #fire is called all blocks and methods will be executed.
8
+ # It may look like this.
9
+ # class MyClass
10
+ # include Lolita::Hooks
11
+ # add_hook :before_save, :after_save
12
+ # end
13
+ # # This will define two hooks for MyClass.
14
+ # To add hook callback just call hook name on class and pass method(-s) or block.
15
+ # MyClass.after_save :write_log
16
+ # MyClass.before_save do
17
+ # validate(self)
18
+ # end
19
+ # ==Scopes
20
+ # Most times hook callbacks are defined for class like in previous example, but also it's possible to do it
21
+ # on class instances. Difference between calling it on class or on instance is that instance callbacks will
22
+ # be called only when event is fired on instance. Class callbacks will be called on class and also on instance
23
+ # callbacks.
24
+ # my_object=MyClass.new
25
+ # MyClass.before_save do
26
+ # puts "class callback"
27
+ # end
28
+ # my_object.before_save do
29
+ # puts "instance callback"
30
+ # end
31
+ #
32
+ # MyClass.fire(:before_save) #=>
33
+ # class_callback
34
+ #
35
+ # my_object.fire(:before_save) #=>
36
+ # class_callback
37
+ # instance_callback
38
+ # # As you can see, first class callbacks is called and after that instance callbacks.
39
+ #
40
+ # ==Firing events
41
+ # To execute callbacks, events should be called on object. Event names is same hooks names. #fire can be called
42
+ # on class or on instance. Also it is possible to pass block to fire event, that will replace callback block
43
+ # or if #let_content is called than it will work like wrapper, like this
44
+ # # this is continuation of previous code
45
+ # MyClass.fire(:before_save) do
46
+ # puts "replaced text"
47
+ # end
48
+ # # will produce #=> replaced text
49
+ #
50
+ # MyClass.fire(:before_save) do
51
+ # puts "before callback"
52
+ # let_content
53
+ # puts "after callback"
54
+ # end
55
+ # # this will produce #=>
56
+ # # before callback
57
+ # # class callback
58
+ # # after callback
59
+ # ==Named hooks
60
+ # See Lolita::Hooks::NamedHook for details.
61
+ module Hooks
62
+ def self.included(base)
63
+ base.extend(ClassMethods)
64
+ base.extend(CommonMethods)
65
+ base.class_eval{
66
+ include CommonMethods
67
+ include InstanceMethods
68
+ }
69
+ end
70
+
71
+ # Look for named hook with singular or plural name of method.
72
+ def self.method_missing method_name,*args, &block
73
+ if named_hook=(Lolita::Hooks::NamedHook.by_name(method_name))
74
+ named_hook[:_class]
75
+ else
76
+ super
77
+ end
78
+ end
79
+
80
+ # Shared methods between class and instance.
81
+ module CommonMethods
82
+
83
+ # All callbacks for class or instance.
84
+ def callbacks
85
+ var=self.instance_variable_get(:@callbacks)
86
+ unless var
87
+ var={}
88
+ self.instance_variable_set(:@callbacks,var)
89
+ end
90
+ instance_variable_get(:@callbacks)
91
+ end
92
+ end
93
+
94
+ module ClassMethods
95
+
96
+ # Setter for #hook_scope.
97
+ def hooks_scope=(object)
98
+ @hooks_scope=object
99
+ end
100
+
101
+ # Hooks scope is used to execute callbacks. By default it is class itself.
102
+ def hooks_scope
103
+ @hooks_scope||self
104
+ end
105
+
106
+ # Setter for #callback_content
107
+ def given_callback_content=(content)
108
+ @given_callback_content=content
109
+ end
110
+
111
+ # Callback content is used to let callback content executed insede of fire block.
112
+ def given_callback_content
113
+ @given_callback_content
114
+ end
115
+
116
+ # All hooks for class. This is Array of hook names.
117
+ def hooks
118
+ @hooks||=[]
119
+ @hooks
120
+ end
121
+
122
+ # Reset all hooks and callbacks to defaults.
123
+ def clear_hooks
124
+ @hooks=[]
125
+ @callbacks={}
126
+ end
127
+
128
+ # This method is used to add hooks for class. It accept one or more hook names.
129
+ # ====Example
130
+ # add_hook :before_save
131
+ # MyClass.add_hooks :after_save, :around_save
132
+ def add_hook(*names)
133
+ (names||[]).each{|hook_name|
134
+ self.class_eval <<-HOOK,__FILE__,__LINE__+1
135
+ def self.#{hook_name}(*methods,&block)
136
+ options=methods.extract_options!
137
+ in_hooks_scope(options[:scope]) do
138
+ register_callback(:"#{hook_name}",*methods,&block)
139
+ end
140
+ end
141
+
142
+ def #{hook_name}(*method,&block)
143
+ self.class.#{hook_name}(*method,:scope=>self,&block)
144
+ end
145
+ HOOK
146
+ register_hook(hook_name)
147
+ }
148
+ end
149
+
150
+ # Fire is used to execute callback. Method accept one or more <i>hook_names</i> and optional block.
151
+ # It will raise error if hook don't exist for this class. Also it accept <em>:scope</em> options, that
152
+ # is used to #get_callbacks and #run_callbacks.
153
+ # ====Example
154
+ # MyClass.fire(:before_save,:after_save,:scope=>MyClass.new)
155
+ # # this will call callbacks in MyClass instance scope, that means that self will be MyClass instance.
156
+ def fire(*hook_names,&block)
157
+ options=hook_names.extract_options!
158
+ (hook_names || []).each do |hook_name|
159
+ raise Lolita::HookNotFound, "Hook #{hook_name} is not defined for #{self}." unless self.has_hook?(hook_name)
160
+ in_hooks_scope(options[:scope]) do
161
+ callback=get_callback(hook_name)
162
+ run_callback(callback,&block)
163
+ end
164
+ end
165
+ end
166
+
167
+ # Is hook with <em>name</em> is defined for class.
168
+ def has_hook?(name)
169
+ self.hooks.include?(name.to_sym)
170
+ end
171
+
172
+ # Try to recognize named fire methods like
173
+ # MyClass.fire_after_save # will call MyClass.fire(:after_save)
174
+ def method_missing(*args, &block)
175
+ unless self.recognize_hook_methods(*args,&block)
176
+ super
177
+ end
178
+ end
179
+
180
+ # Call callback block inside of fire block.
181
+ # ====Example
182
+ # MyClass.fire(:before_save) do
183
+ # do_stuff
184
+ # let_content # execute callback block(-s) in same scope as fire is executed.
185
+ # end
186
+ def let_content
187
+ if content=self.given_callback_content
188
+ run_block(self.given_callback_content)
189
+ end
190
+ end
191
+
192
+ # Set #method_missing
193
+ def recognize_hook_methods method_name, *args, &block
194
+ if method_name.to_s.match(/^fire_(\w+)/)
195
+ self.fire($1,&block)
196
+ true
197
+ end
198
+ end
199
+
200
+ protected
201
+
202
+ # Switch between self and given <em>scope</em>. Block will be executed with <em>scope</em>.
203
+ # And after that it will switch back to self.
204
+ def in_hooks_scope(scope)
205
+ begin
206
+ self.hooks_scope=scope||self
207
+ yield
208
+ ensure
209
+ self.hooks_scope=self
210
+ end
211
+ end
212
+
213
+ # Run callback. Each callback is Hash with <i>:methods</i> Array and </i>:blocks</i> Array
214
+ def run_callback(callback,&block)
215
+ run_methods(callback[:methods],&block)
216
+ run_blocks(callback[:blocks],&block)
217
+ end
218
+
219
+ # Run methods from <em>methods</em> Array
220
+ def run_methods methods, &block
221
+ (methods||[]).each do |method_name|
222
+ hooks_scope.__send__(method_name,&block)
223
+ end
224
+ end
225
+
226
+ # Run blocks from <em>blocks</em> Array. Also it set #given_callback_content if block is given, this
227
+ # will allow to call #let_content. Each block is runned with #run_block.
228
+ def run_blocks blocks,&given_block
229
+ (blocks||[]).each do |block|
230
+ begin
231
+ if block_given?
232
+ self.given_callback_content=given_block
233
+ end
234
+ run_block(block,&given_block)
235
+ ensure
236
+ self.given_callback_content=nil
237
+ end
238
+ end
239
+ end
240
+
241
+ # Run block in scope.
242
+ def run_block block, &given_block
243
+ hooks_scope.instance_eval(&block)
244
+ end
245
+
246
+ # Return all callbacks
247
+ # If scope is not class then it merge class callbacks with scope callbacks. That means that
248
+ # class callbacks always will be called before scope callbacks.
249
+ def get_callback(name)
250
+ scope_callbacks=hooks_scope.callbacks[name.to_sym] || {}
251
+ unless hooks_scope==self
252
+ class_callbacks=self.callbacks[name.to_sym] || {}
253
+ [:methods,:blocks].each do |attr|
254
+ scope_callbacks[attr]=((class_callbacks[attr] || [])+(scope_callbacks[attr] || [])).uniq
255
+ end
256
+ end
257
+ scope_callbacks
258
+ end
259
+
260
+ # Register callback with given scope.
261
+ def register_callback(name,*methods,&block)
262
+ temp_callback=hooks_scope.callbacks[name]||{}
263
+ temp_callback[:methods]||=[]
264
+ temp_callback[:methods]+=(methods||[]).compact
265
+ temp_callback[:blocks]||=[]
266
+ temp_callback[:blocks]<< block if block_given?
267
+ hooks_scope.callbacks[name]=temp_callback
268
+ end
269
+
270
+ # Register hook for scope.
271
+ def register_hook(name)
272
+ self.hooks<<name
273
+ end
274
+ end
275
+
276
+ # Methods for instance.
277
+ module InstanceMethods
278
+
279
+ # See Lolita::Hooks::ClassMethods#fire
280
+ def fire(*hook_names)
281
+ self.class.fire(*hook_names,:scope=>self)
282
+ end
283
+
284
+ # See Lolita::Hooks::ClassMethods#let_content
285
+ def let_content
286
+ self.class.let_content
287
+ end
288
+
289
+ # See Lolita::Hooks::ClassMethods#method_missing
290
+ def method_missing(*args,&block)
291
+ unless self.class.recognize_hook_methods(*args,&block)
292
+ super
293
+ end
294
+ end
295
+ end
296
+
297
+ end
298
+ end