reactive-mvc 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/LICENSE +21 -0
  2. data/Manifest +34 -0
  3. data/README +30 -0
  4. data/Rakefile +5 -0
  5. data/lib/reactive-mvc.rb +26 -0
  6. data/lib/reactive-mvc/controller.rb +16 -0
  7. data/lib/reactive-mvc/controller/base.rb +405 -0
  8. data/lib/reactive-mvc/controller/filters.rb +767 -0
  9. data/lib/reactive-mvc/controller/flash.rb +161 -0
  10. data/lib/reactive-mvc/controller/helpers.rb +203 -0
  11. data/lib/reactive-mvc/controller/layout.rb +285 -0
  12. data/lib/reactive-mvc/controller/output.rb +262 -0
  13. data/lib/reactive-mvc/controller/rescue.rb +208 -0
  14. data/lib/reactive-mvc/dispatcher.rb +133 -0
  15. data/lib/reactive-mvc/view.rb +18 -0
  16. data/lib/reactive-mvc/view/base.rb +388 -0
  17. data/lib/reactive-mvc/view/helpers.rb +38 -0
  18. data/lib/reactive-mvc/view/partials.rb +207 -0
  19. data/lib/reactive-mvc/view/paths.rb +125 -0
  20. data/lib/reactive-mvc/view/renderable.rb +98 -0
  21. data/lib/reactive-mvc/view/renderable_partial.rb +49 -0
  22. data/lib/reactive-mvc/view/template.rb +110 -0
  23. data/lib/reactive-mvc/view/template_error.rb +9 -0
  24. data/lib/reactive-mvc/view/template_handler.rb +9 -0
  25. data/lib/reactive-mvc/view/template_handlers.rb +43 -0
  26. data/lib/reactive-mvc/view/template_handlers/builder.rb +15 -0
  27. data/lib/reactive-mvc/view/template_handlers/erb.rb +20 -0
  28. data/lib/reactive-mvc/view/template_handlers/ruby_code.rb +9 -0
  29. data/reactive_app_generators/mvc/USAGE +10 -0
  30. data/reactive_app_generators/mvc/mvc_generator.rb +48 -0
  31. data/reactive_app_generators/mvc/templates/application_controller.rb +2 -0
  32. data/reactive_generators/controller/USAGE +7 -0
  33. data/reactive_generators/controller/controller_generator.rb +24 -0
  34. data/reactive_generators/controller/templates/controller.rb +2 -0
  35. metadata +113 -0
@@ -0,0 +1,133 @@
1
+ module Reactive
2
+ module Mvc
3
+
4
+ class Dispatcher < Reactive::Dispatcher::Base
5
+ class << self
6
+ def candidate?(request)
7
+ request.params[:dispatcher] == :mvc
8
+ end
9
+ def instance_for(request)
10
+ @@singleton ||= new
11
+ end
12
+ end
13
+
14
+ def dispatch(request)
15
+ self.request = request
16
+ controller = recognize(request).new
17
+ self.response = Response.new
18
+
19
+ log_processing
20
+ controller.process(request, response)
21
+
22
+ handle_response
23
+ end
24
+
25
+ protected
26
+
27
+ def recognize(request)
28
+ if request.params[:controller].nil? && request.params[:asset]
29
+ AssetsController
30
+ else
31
+ if request.params[:controller].nil? && request.params[:model]
32
+ model_to_params(request.params)
33
+ end
34
+ "#{request.params[:controller].camelize}Controller".constantize
35
+ end
36
+ end
37
+
38
+ def model_to_params(params)
39
+ record = params.delete(:model)
40
+ name =record.class.to_s
41
+ params[:controller] = name.demodulize.underscore.pluralize
42
+ # determine the action
43
+ params[:action] = if params[:action].to_s == 'save'
44
+ record.new_record? ? :create : :update
45
+ elsif params[:action]
46
+ params[:action]
47
+ else
48
+ if record.supra.new_record? then :create
49
+ elsif record.supra.changed? then :update
50
+ elsif record.supra.marked_for_destruction? then :destroy
51
+ else :show
52
+ end
53
+ end
54
+ # setup params according to the action
55
+ case params[:action]
56
+ when :create then params.merge!(name.underscore => model_to_hash(record))
57
+ when :update then params.merge!(:id => record.id, name.underscore => model_to_hash(record))
58
+ when :destroy then params.merge!(:id => record.id)
59
+ when :show then params.merge!(:id => record.id)
60
+ else raise ArgumentError, "unknown action #{params[:action]}"
61
+ end
62
+ params
63
+ end
64
+
65
+ def model_to_hash(record)
66
+ hash = record.class.meta.columns.inject({}) {|memo, attr| memo.merge!(attr.name => record.send(attr.name))}
67
+ hash[:id] = record.id unless record.new_record?
68
+ hash.merge!(has_manys_to_hash(record))
69
+ hash.merge!(has_ones_to_hash(record))
70
+ hash.merge!(belong_tos_to_hash(record))
71
+ end
72
+
73
+ def has_manys_to_hash(record)
74
+ record.class.meta.has_manys.inject({}) do |memo, assoc_meta|
75
+ modified = record.send(assoc_meta.name).collect {|assoc| has_any_to_attributes(assoc)}.compact
76
+ modified.empty? ? memo : memo.merge!("#{assoc_meta.name}_attributes".to_sym => modified)
77
+ end
78
+ end
79
+
80
+ def has_ones_to_hash(record)
81
+ record.class.meta.has_ones.inject({}) do |memo, assoc_meta|
82
+ assoc = record.send(assoc_meta.name)
83
+ attrs = assoc && has_any_to_attributes(assoc)
84
+ attrs ? memo.merge!("#{assoc_meta.name}_attributes".to_sym => attrs) : memo
85
+ end
86
+ end
87
+
88
+ def belong_tos_to_hash(record)
89
+ record.class.meta.belong_tos.inject({}) do |memo, assoc_meta|
90
+ assoc = record.send(assoc_meta.name)
91
+ if assoc && (attrs = has_any_to_attributes(assoc))
92
+ memo.merge!("#{assoc_meta.name}_attributes".to_sym => attrs)
93
+ else
94
+ memo.merge!("#{assoc_meta.name}_id".to_sym => assoc && assoc.id)
95
+ end
96
+ end
97
+ end
98
+
99
+ def has_any_to_attributes(assoc)
100
+ # changed? will catch modified records as well as new records. It even furthers to not catch new records that have not yet been filled, that's great.
101
+ if assoc.changed?
102
+ model_to_hash(assoc)
103
+ elsif assoc.marked_for_destruction?
104
+ {:_delete => true, :id => assoc.id}
105
+ end
106
+ end
107
+
108
+ end
109
+
110
+ class AssetsController
111
+ def process(request, response)
112
+ # Assets do not need any handling
113
+ response.handler_name = false
114
+
115
+ name = request.params[:asset]
116
+ glob_path = if request.params[:kind] == :ui
117
+ # construct a path like: {client/}name{_size}.*
118
+ client, size, path = request.params[:client], request.params[:size], ""
119
+ path << "{#{client}/,}" if client
120
+ path << name
121
+ path << "{_#{size},}" if size
122
+ path << ".*"
123
+ path
124
+ else
125
+ name
126
+ end
127
+ response.body = Reactive.file_for(:assets, glob_path) or raise Reactive::Dispatcher::AssetNotFound, "Can't find '#{glob_path}' in #{Reactive.dirs_for(:assets).join(', ')}"
128
+ end
129
+
130
+ end
131
+
132
+ end
133
+ end
@@ -0,0 +1,18 @@
1
+ require 'reactive-mvc/view/template_handlers'
2
+ require 'reactive-mvc/view/renderable'
3
+ require 'reactive-mvc/view/renderable_partial'
4
+
5
+ require 'reactive-mvc/view/template'
6
+ #require 'view/inline_template'
7
+ require 'reactive-mvc/view/paths'
8
+
9
+ require 'reactive-mvc/view/base'
10
+ require 'reactive-mvc/view/partials'
11
+ require 'reactive-mvc/view/template_error'
12
+
13
+ require 'reactive-mvc/view/helpers'
14
+
15
+ Reactive::Mvc::View::Base.class_eval do
16
+ include Reactive::Mvc::View::Partials
17
+ include Reactive::Mvc::View::Helpers
18
+ end
@@ -0,0 +1,388 @@
1
+ module Reactive::Mvc
2
+ module View #:nodoc:
3
+ class Error < Reactive::Error #:nodoc:
4
+ end
5
+
6
+ class MissingTemplate < Error #:nodoc:
7
+ def initialize(paths, path, template_format = nil)
8
+ full_template_path = path.include?('.') ? path : "#{path}.*"
9
+ display_paths = paths.join(':')
10
+ template_type = (path =~ /layouts/i) ? 'layout' : 'template'
11
+ super("Missing #{template_type} #{full_template_path} in view path #{display_paths}")
12
+ end
13
+ end
14
+
15
+ # Action View templates can be written in three ways. If the template file has a <tt>.erb</tt> (or <tt>.rhtml</tt>) extension then it uses a mixture of ERb
16
+ # (included in Ruby) and HTML. If the template file has a <tt>.builder</tt> (or <tt>.rxml</tt>) extension then Jim Weirich's Builder::XmlMarkup library is used.
17
+ # If the template file has a <tt>.rjs</tt> extension then it will use ActionView::Helpers::PrototypeHelper::JavaScriptGenerator.
18
+ #
19
+ # = ERb
20
+ #
21
+ # You trigger ERb by using embeddings such as <% %>, <% -%>, and <%= %>. The <%= %> tag set is used when you want output. Consider the
22
+ # following loop for names:
23
+ #
24
+ # <b>Names of all the people</b>
25
+ # <% for person in @people %>
26
+ # Name: <%= person.name %><br/>
27
+ # <% end %>
28
+ #
29
+ # The loop is setup in regular embedding tags <% %> and the name is written using the output embedding tag <%= %>. Note that this
30
+ # is not just a usage suggestion. Regular output functions like print or puts won't work with ERb templates. So this would be wrong:
31
+ #
32
+ # Hi, Mr. <% puts "Frodo" %>
33
+ #
34
+ # If you absolutely must write from within a function, you can use the TextHelper#concat.
35
+ #
36
+ # <%- and -%> suppress leading and trailing whitespace, including the trailing newline, and can be used interchangeably with <% and %>.
37
+ #
38
+ # == Using sub templates
39
+ #
40
+ # Using sub templates allows you to sidestep tedious replication and extract common display structures in shared templates. The
41
+ # classic example is the use of a header and footer (even though the Action Pack-way would be to use Layouts):
42
+ #
43
+ # <%= render "shared/header" %>
44
+ # Something really specific and terrific
45
+ # <%= render "shared/footer" %>
46
+ #
47
+ # As you see, we use the output embeddings for the render methods. The render call itself will just return a string holding the
48
+ # result of the rendering. The output embedding writes it to the current template.
49
+ #
50
+ # But you don't have to restrict yourself to static includes. Templates can share variables amongst themselves by using instance
51
+ # variables defined using the regular embedding tags. Like this:
52
+ #
53
+ # <% @page_title = "A Wonderful Hello" %>
54
+ # <%= render "shared/header" %>
55
+ #
56
+ # Now the header can pick up on the <tt>@page_title</tt> variable and use it for outputting a title tag:
57
+ #
58
+ # <title><%= @page_title %></title>
59
+ #
60
+ # == Passing local variables to sub templates
61
+ #
62
+ # You can pass local variables to sub templates by using a hash with the variable names as keys and the objects as values:
63
+ #
64
+ # <%= render "shared/header", { :headline => "Welcome", :person => person } %>
65
+ #
66
+ # These can now be accessed in <tt>shared/header</tt> with:
67
+ #
68
+ # Headline: <%= headline %>
69
+ # First name: <%= person.first_name %>
70
+ #
71
+ # If you need to find out whether a certain local variable has been assigned a value in a particular render call,
72
+ # you need to use the following pattern:
73
+ #
74
+ # <% if local_assigns.has_key? :headline %>
75
+ # Headline: <%= headline %>
76
+ # <% end %>
77
+ #
78
+ # Testing using <tt>defined? headline</tt> will not work. This is an implementation restriction.
79
+ #
80
+ # == Template caching
81
+ #
82
+ # By default, Rails will compile each template to a method in order to render it. When you alter a template, Rails will
83
+ # check the file's modification time and recompile it.
84
+ #
85
+ # == Builder
86
+ #
87
+ # Builder templates are a more programmatic alternative to ERb. They are especially useful for generating XML content. An XmlMarkup object
88
+ # named +xml+ is automatically made available to templates with a <tt>.builder</tt> extension.
89
+ #
90
+ # Here are some basic examples:
91
+ #
92
+ # xml.em("emphasized") # => <em>emphasized</em>
93
+ # xml.em { xml.b("emph & bold") } # => <em><b>emph &amp; bold</b></em>
94
+ # xml.a("A Link", "href"=>"http://onestepback.org") # => <a href="http://onestepback.org">A Link</a>
95
+ # xml.target("name"=>"compile", "option"=>"fast") # => <target option="fast" name="compile"\>
96
+ # # NOTE: order of attributes is not specified.
97
+ #
98
+ # Any method with a block will be treated as an XML markup tag with nested markup in the block. For example, the following:
99
+ #
100
+ # xml.div {
101
+ # xml.h1(@person.name)
102
+ # xml.p(@person.bio)
103
+ # }
104
+ #
105
+ # would produce something like:
106
+ #
107
+ # <div>
108
+ # <h1>David Heinemeier Hansson</h1>
109
+ # <p>A product of Danish Design during the Winter of '79...</p>
110
+ # </div>
111
+ #
112
+ # A full-length RSS example actually used on Basecamp:
113
+ #
114
+ # xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do
115
+ # xml.channel do
116
+ # xml.title(@feed_title)
117
+ # xml.link(@url)
118
+ # xml.description "Basecamp: Recent items"
119
+ # xml.language "en-us"
120
+ # xml.ttl "40"
121
+ #
122
+ # for item in @recent_items
123
+ # xml.item do
124
+ # xml.title(item_title(item))
125
+ # xml.description(item_description(item)) if item_description(item)
126
+ # xml.pubDate(item_pubDate(item))
127
+ # xml.guid(@person.firm.account.url + @recent_items.url(item))
128
+ # xml.link(@person.firm.account.url + @recent_items.url(item))
129
+ #
130
+ # xml.tag!("dc:creator", item.author_name) if item_has_creator?(item)
131
+ # end
132
+ # end
133
+ # end
134
+ # end
135
+ #
136
+ # More builder documentation can be found at http://builder.rubyforge.org.
137
+ #
138
+ # == JavaScriptGenerator
139
+ #
140
+ # JavaScriptGenerator templates end in <tt>.rjs</tt>. Unlike conventional templates which are used to
141
+ # render the results of an action, these templates generate instructions on how to modify an already rendered page. This makes it easy to
142
+ # modify multiple elements on your page in one declarative Ajax response. Actions with these templates are called in the background with Ajax
143
+ # and make updates to the page where the request originated from.
144
+ #
145
+ # An instance of the JavaScriptGenerator object named +page+ is automatically made available to your template, which is implicitly wrapped in an ActionView::Helpers::PrototypeHelper#update_page block.
146
+ #
147
+ # When an <tt>.rjs</tt> action is called with +link_to_remote+, the generated JavaScript is automatically evaluated. Example:
148
+ #
149
+ # link_to_remote :url => {:action => 'delete'}
150
+ #
151
+ # The subsequently rendered <tt>delete.rjs</tt> might look like:
152
+ #
153
+ # page.replace_html 'sidebar', :partial => 'sidebar'
154
+ # page.remove "person-#{@person.id}"
155
+ # page.visual_effect :highlight, 'user-list'
156
+ #
157
+ # This refreshes the sidebar, removes a person element and highlights the user list.
158
+ #
159
+ # See the ActionView::Helpers::PrototypeHelper::GeneratorMethods documentation for more details.
160
+ class Base
161
+ extend ActiveSupport::Memoizable
162
+
163
+ attr_accessor :base_path, :assigns, :template_extension
164
+ attr_accessor :controller
165
+
166
+ attr_writer :template_format
167
+
168
+ attr_accessor :output_buffer
169
+
170
+ cattr_accessor :logger
171
+
172
+ class << self
173
+ delegate :erb_trim_mode=, :to => 'Reactive::Mvc::View::TemplateHandlers::ERB'
174
+
175
+ def handle_request(request)
176
+ Dispatcher.dispatch(request)
177
+ end
178
+
179
+ def register_format_handler(format, handler)
180
+ @@format_handlers[format.to_sym] = handler
181
+ end
182
+
183
+ def format_handler_for(format)
184
+ @@format_handlers[format.to_sym]
185
+ end
186
+ end
187
+
188
+ # TODO: format_handlers should be local to each descendant! not shared amongs all the hierarchy (because same format may be handled by different handlers in children classes)
189
+ @@format_handlers = {}
190
+
191
+ # Templates that are exempt from layouts
192
+ @@exempt_from_layout = Set.new([/\.rjs$/])
193
+
194
+ # Don't render layouts for templates with the given extensions.
195
+ def self.exempt_from_layout(*extensions)
196
+ regexps = extensions.collect do |extension|
197
+ extension.is_a?(Regexp) ? extension : /\.#{Regexp.escape(extension.to_s)}$/
198
+ end
199
+ @@exempt_from_layout.merge(regexps)
200
+ end
201
+
202
+ # A warning will be displayed whenever an action results in a cache miss on your view paths.
203
+ @@warn_cache_misses = false
204
+ cattr_accessor :warn_cache_misses
205
+
206
+ #attr_internal :request # will be filled by the magics: _copy_ivars_from_controller
207
+
208
+ delegate :template, :params, :request, :response,
209
+ :flash, :logger, :action_name, :controller_name, :to => :controller
210
+
211
+ module CompiledTemplates #:nodoc:
212
+ # holds compiled template code
213
+ end
214
+ include CompiledTemplates
215
+
216
+ def self.process_view_paths(value)
217
+ Reactive::Mvc::View::PathSet.new(Array(value))
218
+ end
219
+
220
+ attr_reader :helpers
221
+
222
+ class ProxyModule < Module
223
+ def initialize(receiver)
224
+ @receiver = receiver
225
+ end
226
+
227
+ def include(*args)
228
+ super(*args)
229
+ @receiver.extend(*args)
230
+ end
231
+ end
232
+
233
+ def initialize(view_paths = [], controller = nil)#:nodoc:
234
+ @assigns = {}
235
+ @assigns_added = nil
236
+ @_render_stack = []
237
+ @controller = controller
238
+ @helpers = ProxyModule.new(self)
239
+ self.view_paths = view_paths
240
+ end
241
+
242
+ attr_reader :view_paths
243
+
244
+ def view_paths=(paths)
245
+ @view_paths = self.class.process_view_paths(paths)
246
+ end
247
+
248
+ # Renders the template present at <tt>template_path</tt> (relative to the view_paths array).
249
+ # The hash in <tt>local_assigns</tt> is made available as local variables.
250
+ def render(options = {}, local_assigns = {}, &block) #:nodoc:
251
+ local_assigns ||= {}
252
+
253
+ if options.is_a?(String)
254
+ render(:file => options, :locals => local_assigns)
255
+ elsif options.is_a?(Hash)
256
+ options = options.reverse_merge(:locals => {})
257
+ if options[:layout]
258
+ _render_with_layout(options, local_assigns, &block)
259
+ elsif options[:file]
260
+ _pick_template(options[:file]).render_template(self, options[:locals])
261
+ elsif options[:partial]
262
+ render_partial(options)
263
+ # elsif options[:inline]
264
+ # InlineTemplate.new(options[:inline], options[:type]).render(self, options[:locals])
265
+ end
266
+ end
267
+ end
268
+
269
+ # The format to be used when choosing between multiple templates with
270
+ # the same name but differing formats. See +Request#template_format+
271
+ # for more details.
272
+ def template_format
273
+ if defined? @template_format
274
+ @template_format
275
+ elsif controller && controller.respond_to?(:request)
276
+ @template_format = controller.request.format
277
+ else
278
+ @template_format = default_template_format
279
+ end
280
+ end
281
+
282
+ def default_template_format
283
+ :rb
284
+ end
285
+
286
+ # Access the current template being rendered.
287
+ # Returns a ActionView::Template object.
288
+ def template
289
+ @_render_stack.last
290
+ end
291
+
292
+ def template_exists?(template_path)
293
+ _pick_template(template_path) ? true : false
294
+ rescue MissingTemplate
295
+ false
296
+ end
297
+
298
+ private
299
+ # Evaluates the local assigns and controller ivars, pushes them to the view.
300
+ def _evaluate_assigns_and_ivars #:nodoc:
301
+ unless @assigns_added
302
+ @assigns.each { |key, value| instance_variable_set("@#{key}", value) }
303
+ _copy_ivars_from_controller
304
+ @assigns_added = true
305
+ end
306
+ end
307
+
308
+ def _copy_ivars_from_controller #:nodoc:
309
+ if @controller
310
+ variables = @controller.instance_variable_names
311
+ variables -= @controller.protected_instance_variables if @controller.respond_to?(:protected_instance_variables)
312
+ variables.each { |name| instance_variable_set(name, @controller.instance_variable_get(name)) }
313
+ end
314
+ end
315
+
316
+ def _pick_template(template_path)
317
+ return template_path if template_path.respond_to?(:render)
318
+
319
+ path = template_path.sub(/^\//, '')
320
+ if m = path.match(/(.*)\.(\w+)$/)
321
+ template_file_name, template_file_extension = m[1], m[2]
322
+ else
323
+ template_file_name = path
324
+ end
325
+
326
+ # OPTIMIZE: Checks to lookup template in view path
327
+ if template = self.view_paths["#{template_file_name}.#{template_format}"]
328
+ template
329
+ elsif template = self.view_paths[template_file_name]
330
+ template
331
+ elsif (first_render = @_render_stack.first) && first_render.respond_to?(:format_and_extension) &&
332
+ (template = self.view_paths["#{template_file_name}.#{first_render.format_and_extension}"])
333
+ template
334
+ elsif template_format == :js && template = self.view_paths["#{template_file_name}.html"]
335
+ @template_format = :html
336
+ template
337
+ else
338
+ template = Template.new(template_path, view_paths)
339
+
340
+ if self.class.warn_cache_misses && logger
341
+ logger.debug "[PERFORMANCE] Rendering a template that was " +
342
+ "not found in view path. Templates outside the view path are " +
343
+ "not cached and result in expensive disk operations. Move this " +
344
+ "file into #{view_paths.join(':')} or add the folder to your " +
345
+ "view path list"
346
+ end
347
+
348
+ template
349
+ end
350
+ end
351
+ memoize :_pick_template
352
+
353
+ def _exempt_from_layout?(template_path) #:nodoc:
354
+ template = _pick_template(template_path).to_s
355
+ @@exempt_from_layout.any? { |ext| template =~ ext }
356
+ rescue MissingTemplate
357
+ return false
358
+ end
359
+
360
+ def _render_with_layout(options, local_assigns, &block) #:nodoc:
361
+ partial_layout = options.delete(:layout)
362
+
363
+ if block_given?
364
+ begin
365
+ @_proc_for_layout = block
366
+ concat(render(options.merge(:partial => partial_layout)))
367
+ ensure
368
+ @_proc_for_layout = nil
369
+ end
370
+ else
371
+ begin
372
+ original_content_for_layout = @content_for_layout if defined?(@content_for_layout)
373
+ @content_for_layout = render(options)
374
+
375
+ if (options[:inline] || options[:file] || options[:text])
376
+ @cached_content_for_layout = @content_for_layout
377
+ render(:file => partial_layout, :locals => local_assigns)
378
+ else
379
+ render(options.merge(:partial => partial_layout))
380
+ end
381
+ ensure
382
+ @content_for_layout = original_content_for_layout
383
+ end
384
+ end
385
+ end
386
+ end
387
+ end
388
+ end