yodel 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. data/.document +5 -0
  2. data/.gitignore +9 -0
  3. data/Gemfile +2 -0
  4. data/Gemfile.lock +63 -0
  5. data/LICENSE +1 -0
  6. data/README.rdoc +20 -0
  7. data/Rakefile +1 -0
  8. data/bin/yodel +4 -0
  9. data/lib/yodel.rb +22 -0
  10. data/lib/yodel/application/application.rb +44 -0
  11. data/lib/yodel/application/extension.rb +59 -0
  12. data/lib/yodel/application/request_handler.rb +48 -0
  13. data/lib/yodel/application/yodel.rb +25 -0
  14. data/lib/yodel/command/command.rb +94 -0
  15. data/lib/yodel/command/deploy.rb +67 -0
  16. data/lib/yodel/command/dns_server.rb +16 -0
  17. data/lib/yodel/command/installer.rb +229 -0
  18. data/lib/yodel/config/config.rb +30 -0
  19. data/lib/yodel/config/environment.rb +16 -0
  20. data/lib/yodel/config/yodel.rb +21 -0
  21. data/lib/yodel/exceptions/destroyed_record.rb +2 -0
  22. data/lib/yodel/exceptions/domain_not_found.rb +16 -0
  23. data/lib/yodel/exceptions/duplicate_layout.rb +2 -0
  24. data/lib/yodel/exceptions/exceptions.rb +3 -0
  25. data/lib/yodel/exceptions/inconsistent_lock_state.rb +2 -0
  26. data/lib/yodel/exceptions/invalid_field.rb +2 -0
  27. data/lib/yodel/exceptions/invalid_index.rb +2 -0
  28. data/lib/yodel/exceptions/invalid_mixin.rb +2 -0
  29. data/lib/yodel/exceptions/invalid_model_field.rb +2 -0
  30. data/lib/yodel/exceptions/layout_not_found.rb +2 -0
  31. data/lib/yodel/exceptions/mass_assignment.rb +2 -0
  32. data/lib/yodel/exceptions/missing_migration.rb +2 -0
  33. data/lib/yodel/exceptions/missing_root_directory.rb +15 -0
  34. data/lib/yodel/exceptions/unable_to_acquire_lock.rb +2 -0
  35. data/lib/yodel/exceptions/unauthorised.rb +2 -0
  36. data/lib/yodel/exceptions/unknown_field.rb +2 -0
  37. data/lib/yodel/middleware/development_server.rb +180 -0
  38. data/lib/yodel/middleware/error_pages.rb +72 -0
  39. data/lib/yodel/middleware/public_assets.rb +78 -0
  40. data/lib/yodel/middleware/request.rb +16 -0
  41. data/lib/yodel/middleware/site_detector.rb +22 -0
  42. data/lib/yodel/mime_types/default_mime_set.rb +28 -0
  43. data/lib/yodel/mime_types/mime_type.rb +68 -0
  44. data/lib/yodel/mime_types/mime_type_set.rb +41 -0
  45. data/lib/yodel/mime_types/mime_types.rb +6 -0
  46. data/lib/yodel/mime_types/yodel.rb +15 -0
  47. data/lib/yodel/models/api/api.rb +1 -0
  48. data/lib/yodel/models/api/api_call.rb +87 -0
  49. data/lib/yodel/models/core/associations/association.rb +37 -0
  50. data/lib/yodel/models/core/associations/associations.rb +22 -0
  51. data/lib/yodel/models/core/associations/counts/many_association.rb +18 -0
  52. data/lib/yodel/models/core/associations/counts/one_association.rb +22 -0
  53. data/lib/yodel/models/core/associations/embedded/embedded_association.rb +47 -0
  54. data/lib/yodel/models/core/associations/embedded/embedded_record_array.rb +12 -0
  55. data/lib/yodel/models/core/associations/embedded/many_embedded_association.rb +62 -0
  56. data/lib/yodel/models/core/associations/embedded/one_embedded_association.rb +49 -0
  57. data/lib/yodel/models/core/associations/query/many_query_association.rb +10 -0
  58. data/lib/yodel/models/core/associations/query/one_query_association.rb +10 -0
  59. data/lib/yodel/models/core/associations/query/query_association.rb +64 -0
  60. data/lib/yodel/models/core/associations/record_association.rb +38 -0
  61. data/lib/yodel/models/core/associations/store/many_store_association.rb +32 -0
  62. data/lib/yodel/models/core/associations/store/one_store_association.rb +14 -0
  63. data/lib/yodel/models/core/associations/store/store_association.rb +51 -0
  64. data/lib/yodel/models/core/attachments/attachment.rb +73 -0
  65. data/lib/yodel/models/core/attachments/image.rb +38 -0
  66. data/lib/yodel/models/core/core.rb +15 -0
  67. data/lib/yodel/models/core/fields/alias_field.rb +32 -0
  68. data/lib/yodel/models/core/fields/array_field.rb +64 -0
  69. data/lib/yodel/models/core/fields/attachment_field.rb +42 -0
  70. data/lib/yodel/models/core/fields/boolean_field.rb +28 -0
  71. data/lib/yodel/models/core/fields/change_sensitive_array.rb +96 -0
  72. data/lib/yodel/models/core/fields/change_sensitive_hash.rb +53 -0
  73. data/lib/yodel/models/core/fields/color_field.rb +4 -0
  74. data/lib/yodel/models/core/fields/date_field.rb +35 -0
  75. data/lib/yodel/models/core/fields/decimal_field.rb +19 -0
  76. data/lib/yodel/models/core/fields/email_field.rb +10 -0
  77. data/lib/yodel/models/core/fields/enum_field.rb +33 -0
  78. data/lib/yodel/models/core/fields/field.rb +154 -0
  79. data/lib/yodel/models/core/fields/fields.rb +29 -0
  80. data/lib/yodel/models/core/fields/fields_field.rb +31 -0
  81. data/lib/yodel/models/core/fields/filter_mixin.rb +9 -0
  82. data/lib/yodel/models/core/fields/filtered_string_field.rb +5 -0
  83. data/lib/yodel/models/core/fields/filtered_text_field.rb +5 -0
  84. data/lib/yodel/models/core/fields/function_field.rb +28 -0
  85. data/lib/yodel/models/core/fields/hash_field.rb +54 -0
  86. data/lib/yodel/models/core/fields/html_field.rb +15 -0
  87. data/lib/yodel/models/core/fields/image_field.rb +11 -0
  88. data/lib/yodel/models/core/fields/integer_field.rb +25 -0
  89. data/lib/yodel/models/core/fields/password_field.rb +21 -0
  90. data/lib/yodel/models/core/fields/self_field.rb +27 -0
  91. data/lib/yodel/models/core/fields/string_field.rb +15 -0
  92. data/lib/yodel/models/core/fields/tags_field.rb +7 -0
  93. data/lib/yodel/models/core/fields/text_field.rb +7 -0
  94. data/lib/yodel/models/core/fields/time_field.rb +36 -0
  95. data/lib/yodel/models/core/functions/function.rb +471 -0
  96. data/lib/yodel/models/core/functions/functions.rb +2 -0
  97. data/lib/yodel/models/core/functions/trigger.rb +14 -0
  98. data/lib/yodel/models/core/log/log.rb +33 -0
  99. data/lib/yodel/models/core/log/log_entry.rb +12 -0
  100. data/lib/yodel/models/core/model/abstract_model.rb +59 -0
  101. data/lib/yodel/models/core/model/model.rb +460 -0
  102. data/lib/yodel/models/core/model/mongo_model.rb +25 -0
  103. data/lib/yodel/models/core/model/site_model.rb +17 -0
  104. data/lib/yodel/models/core/mongo/mongo.rb +3 -0
  105. data/lib/yodel/models/core/mongo/primary_key_factory.rb +12 -0
  106. data/lib/yodel/models/core/mongo/query.rb +68 -0
  107. data/lib/yodel/models/core/mongo/record_index.rb +89 -0
  108. data/lib/yodel/models/core/record/abstract_record.rb +411 -0
  109. data/lib/yodel/models/core/record/embedded_record.rb +47 -0
  110. data/lib/yodel/models/core/record/mongo_record.rb +83 -0
  111. data/lib/yodel/models/core/record/record.rb +386 -0
  112. data/lib/yodel/models/core/record/section.rb +21 -0
  113. data/lib/yodel/models/core/record/site_record.rb +31 -0
  114. data/lib/yodel/models/core/site/migration.rb +52 -0
  115. data/lib/yodel/models/core/site/remote.rb +61 -0
  116. data/lib/yodel/models/core/site/site.rb +202 -0
  117. data/lib/yodel/models/core/validations/email_address_validation.rb +24 -0
  118. data/lib/yodel/models/core/validations/embedded_records_validation.rb +31 -0
  119. data/lib/yodel/models/core/validations/errors.rb +51 -0
  120. data/lib/yodel/models/core/validations/excluded_from_validation.rb +10 -0
  121. data/lib/yodel/models/core/validations/excludes_combinations_validation.rb +18 -0
  122. data/lib/yodel/models/core/validations/format_validation.rb +10 -0
  123. data/lib/yodel/models/core/validations/included_in_validation.rb +10 -0
  124. data/lib/yodel/models/core/validations/includes_combinations_validation.rb +14 -0
  125. data/lib/yodel/models/core/validations/length_validation.rb +28 -0
  126. data/lib/yodel/models/core/validations/password_confirmation_validation.rb +11 -0
  127. data/lib/yodel/models/core/validations/required_validation.rb +9 -0
  128. data/lib/yodel/models/core/validations/unique_validation.rb +9 -0
  129. data/lib/yodel/models/core/validations/validation.rb +39 -0
  130. data/lib/yodel/models/core/validations/validations.rb +15 -0
  131. data/lib/yodel/models/email/email.rb +79 -0
  132. data/lib/yodel/models/migrations/01_record_model.rb +29 -0
  133. data/lib/yodel/models/migrations/02_page_model.rb +45 -0
  134. data/lib/yodel/models/migrations/03_layout_model.rb +38 -0
  135. data/lib/yodel/models/migrations/04_group_model.rb +61 -0
  136. data/lib/yodel/models/migrations/05_user_model.rb +24 -0
  137. data/lib/yodel/models/migrations/06_snippet_model.rb +13 -0
  138. data/lib/yodel/models/migrations/07_search_page_model.rb +32 -0
  139. data/lib/yodel/models/migrations/08_default_site_options.rb +21 -0
  140. data/lib/yodel/models/migrations/09_security_page_models.rb +36 -0
  141. data/lib/yodel/models/migrations/10_record_proxy_page_model.rb +17 -0
  142. data/lib/yodel/models/migrations/11_email_model.rb +28 -0
  143. data/lib/yodel/models/migrations/12_api_call_model.rb +23 -0
  144. data/lib/yodel/models/migrations/13_redirect_page_model.rb +13 -0
  145. data/lib/yodel/models/migrations/14_menu_model.rb +20 -0
  146. data/lib/yodel/models/models.rb +8 -0
  147. data/lib/yodel/models/pages/form_builder.rb +379 -0
  148. data/lib/yodel/models/pages/html_decorator.rb +132 -0
  149. data/lib/yodel/models/pages/layout.rb +120 -0
  150. data/lib/yodel/models/pages/menu.rb +32 -0
  151. data/lib/yodel/models/pages/page.rb +378 -0
  152. data/lib/yodel/models/pages/pages.rb +7 -0
  153. data/lib/yodel/models/pages/record_proxy_page.rb +188 -0
  154. data/lib/yodel/models/pages/redirect_page.rb +11 -0
  155. data/lib/yodel/models/search/search.rb +1 -0
  156. data/lib/yodel/models/search/search_page.rb +58 -0
  157. data/lib/yodel/models/security/facebook_login_page.rb +55 -0
  158. data/lib/yodel/models/security/group.rb +10 -0
  159. data/lib/yodel/models/security/guests_group.rb +5 -0
  160. data/lib/yodel/models/security/login_page.rb +20 -0
  161. data/lib/yodel/models/security/logout_page.rb +13 -0
  162. data/lib/yodel/models/security/noone_group.rb +5 -0
  163. data/lib/yodel/models/security/owner_group.rb +8 -0
  164. data/lib/yodel/models/security/password.rb +5 -0
  165. data/lib/yodel/models/security/password_reset_page.rb +47 -0
  166. data/lib/yodel/models/security/security.rb +10 -0
  167. data/lib/yodel/models/security/user.rb +33 -0
  168. data/lib/yodel/public/core/css/core.css +257 -0
  169. data/lib/yodel/public/core/css/reset.css +48 -0
  170. data/lib/yodel/public/core/images/cross.png +0 -0
  171. data/lib/yodel/public/core/images/spinner.gif +0 -0
  172. data/lib/yodel/public/core/images/tick.png +0 -0
  173. data/lib/yodel/public/core/images/yodel.png +0 -0
  174. data/lib/yodel/public/core/js/jquery.min.js +18 -0
  175. data/lib/yodel/public/core/js/json2.js +480 -0
  176. data/lib/yodel/public/core/js/yodel_jquery.js +238 -0
  177. data/lib/yodel/request/authentication.rb +76 -0
  178. data/lib/yodel/request/flash.rb +28 -0
  179. data/lib/yodel/request/request.rb +4 -0
  180. data/lib/yodel/requires.rb +47 -0
  181. data/lib/yodel/task_queue/queue_daemon.rb +33 -0
  182. data/lib/yodel/task_queue/queue_worker.rb +32 -0
  183. data/lib/yodel/task_queue/stats_thread.rb +27 -0
  184. data/lib/yodel/task_queue/task.rb +62 -0
  185. data/lib/yodel/task_queue/task_queue.rb +40 -0
  186. data/lib/yodel/types/date.rb +5 -0
  187. data/lib/yodel/types/object_id.rb +11 -0
  188. data/lib/yodel/types/time.rb +5 -0
  189. data/lib/yodel/version.rb +3 -0
  190. data/system/Library/LaunchDaemons/com.yodelcms.dns.plist +26 -0
  191. data/system/Library/LaunchDaemons/com.yodelcms.server.plist +26 -0
  192. data/system/etc/resolver/yodel +2 -0
  193. data/system/usr/local/bin/yodel_command_runner +2 -0
  194. data/system/usr/local/etc/yodel/development_settings.rb +28 -0
  195. data/system/usr/local/etc/yodel/production_settings.rb +27 -0
  196. data/system/var/log/yodel.log +0 -0
  197. data/test/helper.rb +18 -0
  198. data/test/test_yodel.rb +4 -0
  199. data/yodel.gemspec +47 -0
  200. metadata +501 -0
@@ -0,0 +1,132 @@
1
+ # Classes including this module must implement this interface:
2
+ # path: path to the current page
3
+ # path_was: original path to the current page (if path has been updated)
4
+ # form_for_page must be called in a context where form_for is available
5
+ module HTMLDecorator
6
+ # ----------------------------------------
7
+ # Forms
8
+ # ----------------------------------------
9
+ def form_for_page(options={}, &block)
10
+ # FIXME: record proxy page needs to implement form_for with 3 parameters, not 2
11
+ if method(:form_for).arity == -3
12
+ form_for(self, path_was, options, &block)
13
+ else
14
+ form_for(self, options, &block)
15
+ end
16
+ end
17
+
18
+
19
+ # ----------------------------------------
20
+ # Actions
21
+ # ----------------------------------------
22
+ def delete_button(text, options={})
23
+ attributes = ""
24
+ if options[:confirm]
25
+ attributes << " onsubmit='return confirm(\"#{options.delete(:confirm)}\")'"
26
+ end
27
+ attributes << options.collect {|name, value| "#{name}='#{value}'"}.join(' ')
28
+ button_input = "<button>#{text}</button>"
29
+ method_input = "<input type='hidden' name='_method' value='delete'>"
30
+ "<form action='#{path}' method='post' #{attributes}>#{method_input}#{button_input}</form>"
31
+ end
32
+
33
+ def delete_link(text, options={})
34
+ attributes = ""
35
+ if options[:confirm]
36
+ confirm = "if(confirm(\"#{options.delete(:confirm)}\"))"
37
+ else
38
+ confirm = ''
39
+ end
40
+ if options[:wrap]
41
+ wrap_start = options[:wrap][0]
42
+ wrap_end = options[:wrap][1]
43
+ options.delete(:wrap)
44
+ else
45
+ wrap_start = ''
46
+ wrap_end = ''
47
+ end
48
+ attributes << options.collect {|name, value| "#{name}='#{value}'"}.join(' ')
49
+ delete_link = "#{wrap_start}<a #{attributes} onclick='#{confirm}submit()' href='#'>#{text}</a>#{wrap_end}"
50
+ method_input = "<input type='hidden' name='_method' value='delete'>"
51
+ "<form action='#{path}' method='post' class='delete'>#{method_input}#{delete_link}</form>"
52
+ end
53
+
54
+ def immediately(action, options={})
55
+ action_record = options.delete(:record)
56
+ action_path = options.delete(:path)
57
+
58
+ # remaining options are field mutations
59
+ fields = fields_to_json(options)
60
+
61
+ # perform the action directly on a record
62
+ if action_path.nil?
63
+ action_record ||= self
64
+ action_record.from_json(fields)
65
+ action_record.save
66
+ ''
67
+ else
68
+ Hpricot::Elem.new('script', {}, [
69
+ Hpricot::Text.new(action_to_javascript(action, action_path, fields))
70
+ ])
71
+ end
72
+ end
73
+
74
+ def on_click(action, options={}, &block)
75
+ # on_click requires child elements
76
+ return '' unless block_given?
77
+
78
+ # determine the path and fields of the request
79
+ action_record = options.delete(:record) || self
80
+ action_path = options.delete(:path) || action_record.path
81
+ fields = fields_to_json(options)
82
+
83
+ Ember::Template.wrap_content_block(block) do |content|
84
+ Hpricot::Elem.new('span', {
85
+ 'class' => 'yodel-remote-action',
86
+ 'data-action' => CGI.escape_html(action_to_javascript(action, action_path, fields))
87
+ }, [Hpricot::Text.new(content.join)])
88
+ end
89
+ end
90
+
91
+ # fields is a hash containing a set of fields to change, the
92
+ # operation to perform, and the value to perform the operation
93
+ # with, for example: {age: {set: 40}, name: {set: 'Bob'}}
94
+ # The return value is a hash representing the same set of changes,
95
+ # formatted in a way Record#from_json responds to.
96
+ def fields_to_json(fields)
97
+ fields.each_with_object({}) do |(key, value), hash|
98
+ field_action = value.keys.first.to_s
99
+ field_value = value[value.keys.first]
100
+ hash[key.to_s] = {'_action' => field_action, '_value' => field_value}
101
+ end
102
+ end
103
+ private :fields_to_json
104
+
105
+ def action_to_javascript(action, path, fields)
106
+ case action
107
+ when :update
108
+ "Yodel.Records.update('#{path}', #{fields.to_json});"
109
+ when :delete
110
+ "Yodel.Records.delete('#{path}');"
111
+ end
112
+ end
113
+ private :action_to_javascript
114
+
115
+
116
+ # ----------------------------------------
117
+ # Formatters
118
+ # ----------------------------------------
119
+ def paragraph(index, field=:content)
120
+ text = self[field]
121
+ paragraphs = Hpricot(text).search('/p')
122
+ return '' if paragraphs.nil? || paragraphs[index].nil?
123
+ paragraphs[index].inner_html
124
+ end
125
+
126
+ def paragraphs_from(index, field=:content)
127
+ text = self[field]
128
+ paragraphs = Hpricot(text).children
129
+ return '' if paragraphs.nil? || paragraphs[index..-1].nil?
130
+ paragraphs[index..-1].collect {|p| p.to_s}.join('')
131
+ end
132
+ end
@@ -0,0 +1,120 @@
1
+ class Layout < Record
2
+ MAX_LOCK_ATTEMPTS = 1000 # FIXME: is this appropriate? how long does each attempt take?
3
+
4
+ # in development, all layout records are destroyed and recreated each
5
+ # refresh, so the database view of the layout structure is consistent
6
+ # with what is on disk. FileLayout objects lazy load the layout from
7
+ # disk as needed. In production, PersistentLayout records store the
8
+ # layout markup in the database.
9
+ def self.reload_layouts(site)
10
+ # acquire a lock on the site to prevent multiple reloads colliding
11
+ attempts = 0
12
+ updated = 0
13
+
14
+ # sites created in production that are yet to be pushed to won't have
15
+ # any models (including layouts) or any layouts to load
16
+ return if site.model_types['layouts'].nil?
17
+
18
+ while updated == 0 && attempts < MAX_LOCK_ATTEMPTS
19
+ updated = Site.collection.update(
20
+ {'_id' => site.id, 'layout_lock' => {'$exists' => false}},
21
+ {'$set' => {'layout_lock' => true}},
22
+ safe: true)['n']
23
+ attempts += 1
24
+ end
25
+
26
+ raise UnableToAcquireLock if attempts == MAX_LOCK_ATTEMPTS
27
+
28
+ # lock has been acquired, this section is guarded
29
+ site.layouts.all.each(&:destroy)
30
+ Yodel.mime_types.each do |mime_type|
31
+ mime_type_name = mime_type.name.to_s
32
+ mime_type_extensions = mime_type.extensions.join(',')
33
+ site.layout_directories.each do |directory|
34
+ scan_folder(directory, site, mime_type_name, mime_type_extensions, nil)
35
+ end
36
+ end
37
+
38
+ # release the lock
39
+ ensure
40
+ if attempts != 0
41
+ updated = Site.collection.update(
42
+ {'_id' => site.id, 'layout_lock' => {'$exists' => true}},
43
+ {'$unset' => {'layout_lock' => 1}},
44
+ safe: true)['n']
45
+ raise InconsistentLockState if updated != 1
46
+ end
47
+ end
48
+
49
+ def render(page)
50
+ method = "render_#{Yodel.mime_types[mime_type.to_sym].layout_processor}"
51
+ if respond_to?(method)
52
+ send(method, page)
53
+ else
54
+ render_default(page)
55
+ end
56
+ end
57
+
58
+ def self.render(name, &block)
59
+ define_method("render_#{name}", block)
60
+ end
61
+
62
+ render :default do |page|
63
+ markup
64
+ end
65
+
66
+
67
+ private
68
+ def self.scan_folder(path, site, mime_type_name, mime_type_extensions, parent)
69
+ return unless File.directory?(path)
70
+
71
+ # create layouts for all mime type files, then scan for any
72
+ # sub-layouts in folders with the same name as the layout
73
+ Dir.glob(File.join(path, "*.{#{mime_type_extensions}}")).each do |file_path|
74
+ name = File.basename(file_path, File.extname(file_path))
75
+ if site.layouts.exists?(name: name, mime_type: mime_type_name)
76
+ other_layout = site.layouts.where(name: name, mime_type: mime_type_name).first
77
+ raise DuplicateLayout, file_path, other_layout.path
78
+ end
79
+
80
+ layout = site.file_layouts.new
81
+ layout.name = name
82
+ layout.parent = parent
83
+ layout.path = file_path
84
+ layout.mime_type = mime_type_name
85
+ layout.save
86
+
87
+ # scan for sub layouts
88
+ sub_layouts_folder = File.join(File.dirname(file_path), name)
89
+ scan_folder(sub_layouts_folder, site, mime_type_name, mime_type_extensions, layout)
90
+ end
91
+ end
92
+ end
93
+
94
+
95
+ class PersistentLayout < Layout
96
+ # markup is defined as a field of persistent layouts
97
+ def options
98
+ {}
99
+ end
100
+ end
101
+
102
+ class FileLayout < Layout
103
+ def markup
104
+ IO.read(path)
105
+ end
106
+
107
+ def options
108
+ {source_file: path}
109
+ end
110
+
111
+ render :ember do |page|
112
+ page.set_content(Ember::Template.new(markup, options).render(page.get_binding))
113
+ parent.render(page) if parent
114
+ page.content
115
+ end
116
+
117
+ render :eval do |page|
118
+ page.instance_eval(markup)
119
+ end
120
+ end
@@ -0,0 +1,32 @@
1
+ class Menu < Record
2
+ def render(page)
3
+ if include_all_children
4
+ items = root.children.collect do |child|
5
+ exception = exceptions.find {|except| except.page == child}
6
+ next if exception && !exception.show
7
+ render_item(child, page, 0, exception ? exception.depth : depth)
8
+ end
9
+ else
10
+ items = exceptions.all.collect do |exception|
11
+ render_item(exception.page, page, 0, exception.depth)
12
+ end
13
+ end
14
+
15
+ if include_root
16
+ items.unshift(render_item(root, page, 0, 0))
17
+ end
18
+
19
+ "<nav>#{items.join}</nav>"
20
+ end
21
+
22
+ private
23
+ def render_item(item, page, current_depth, max_depth)
24
+ items = ["<a href='#{item.path}' class='#{'selected' if item == page}'>#{item.title}</a>"]
25
+ if current_depth < max_depth
26
+ item.children.each do |child|
27
+ items << render_item(child, page, current_depth + 1, max_depth)
28
+ end
29
+ end
30
+ items.join
31
+ end
32
+ end
@@ -0,0 +1,378 @@
1
+ class Page < Record
2
+ include Authentication
3
+ include HTMLDecorator
4
+
5
+ # ----------------------------------------
6
+ # Paths and permalinks
7
+ # ----------------------------------------
8
+ # Permalinks are unique within the scope of the siblings of a page. Only reassign
9
+ # a permalink after a title has changed, or if the title is a function type (and
10
+ # could change on every update of the page).
11
+ before_validation :assign_permalink
12
+ def assign_permalink
13
+ return unless (title_changed? && title?) || (!title.nil? && values['title'].nil?)
14
+
15
+ # until we detect changes to fields used by cached functions, force a refresh of the value
16
+ generate_unloaded_field('title') if field('title').type == 'Function'
17
+
18
+ permalink_character = site.option('pages.permalink_character') || '-'
19
+ base_permalink = title.parameterize(permalink_character)
20
+ suffix = ''
21
+ count = 0
22
+
23
+ # ensure other pages don't have the same path as this page
24
+ page_siblings = siblings.all.select {|record| record.field?('permalink')}
25
+ while page_siblings.any? {|page| page.permalink == base_permalink + suffix}
26
+ count += 1
27
+ suffix = "#{permalink_character}#{count}"
28
+ end
29
+
30
+ # set the page's permalink, then construct its path and reset any child paths
31
+ self.permalink = base_permalink + suffix
32
+ assign_path
33
+ end
34
+
35
+ def assign_path(prefix=nil)
36
+ if prefix
37
+ new_prefix = prefix + '/' + permalink
38
+ else
39
+ new_prefix = '/' + parents.reverse[1..-1].collect(&:permalink).join('/')
40
+ end
41
+
42
+ self.path = new_prefix
43
+ count = 0
44
+
45
+ while site.pages.where(:path => self.path, :_id.ne => self.id).exists?
46
+ count += 1
47
+ self.path = "#{new_prefix}#{count}"
48
+ end
49
+
50
+ save_without_validation if prefix
51
+ children.each {|child| child.assign_path(new_prefix)}
52
+ end
53
+
54
+
55
+ # ----------------------------------------
56
+ # Layout helpers
57
+ # ----------------------------------------
58
+ def snippet(name)
59
+ site.snippets[name.to_s].content
60
+ end
61
+
62
+ def menu(name)
63
+ site.menus[name.to_s].render(self)
64
+ end
65
+
66
+ def flash
67
+ @flash ||= Flash.new(session)
68
+ end
69
+
70
+
71
+ # ----------------------------------------
72
+ # Forms
73
+ # ----------------------------------------
74
+ def form_for(record, action, options={}, &block)
75
+ options[:method] = record.new? ? 'post' : 'put'
76
+ if options[:remote]
77
+ components = action.split('?')
78
+ components[0] += '.json' unless components.first.end_with?('.json')
79
+ action = components.join('?')
80
+ options[:success] = 'window.location = record.path;' if record.new?
81
+ end
82
+ FormBuilder.new(record, action, options, &block).render
83
+ end
84
+
85
+
86
+ # ----------------------------------------
87
+ # Response content
88
+ # ----------------------------------------
89
+ def self.respond_to(http_method)
90
+ # FIXME: this is not thread safe
91
+ @_http_method = http_method
92
+ yield
93
+ @_http_method = nil
94
+ end
95
+
96
+ def self.with(mime_type, &block)
97
+ # two instance methods may be defined from the response definition
98
+ action_name = "respond_to_#{@_http_method}_with_#{mime_type}"
99
+ default_action_name = "default_response_to_#{@_http_method}"
100
+ default_action_mime_var = "@default_response_to_#{@_http_method}_mime_type"
101
+
102
+ # create or overwrite the main action for this http_method/mime_type pair
103
+ define_method(action_name, block)
104
+
105
+ # if this is the first response definition for the http_method, assign it
106
+ # as the default response for requests matching the http_method, but not
107
+ # matching a mime_type that has been responded to
108
+ unless instance_methods(false).include?(default_action_name.to_sym)
109
+ define_method(default_action_name, block)
110
+ instance_variable_set(default_action_mime_var, Yodel.mime_types[mime_type])
111
+ end
112
+ end
113
+
114
+ # request handling
115
+ def respond_to_request(request, response, mime_type)
116
+ # initialise request & response for use by the environment accessors
117
+ @_request = request
118
+ @_response = response
119
+ @_mime_type = mime_type
120
+
121
+ # determine the default action name for the request
122
+ http_method = request.request_method.downcase
123
+ action = "respond_to_#{http_method}_with_#{mime_type.name}"
124
+
125
+ # if there is no action for the request, default to the first for the http_method of the request
126
+ if !respond_to?(action)
127
+ action = "default_response_to_#{http_method}"
128
+
129
+ if !respond_to?(action)
130
+ response.write "Unable to respond to a request using http method: #{http_method}"
131
+ response['Content-Type'] = 'text/plain'
132
+ return
133
+ else
134
+ default_mime = self.class.instance_variable_get("@default_response_to_#{http_method}_mime_type")
135
+ mime_type = default_mime if mime_type != default_mime
136
+ Yodel.config.logger.warn "No response matches this request, falling back to a default response."
137
+ end
138
+ end
139
+
140
+ # only send a builder object as a parameter to the action if required
141
+ if mime_type.has_builder?
142
+ data = send(action, mime_type.create_builder)
143
+ else
144
+ data = send(action)
145
+ end
146
+ return if @finished
147
+
148
+ # process the response and set headers
149
+ response.write mime_type.process(data)
150
+ response['Content-Type'] = "#{mime_type.default_mime_type}; charset=utf-8"
151
+
152
+ # write the flash to the session if appropriate
153
+ @flash.finalize if @flash
154
+ end
155
+
156
+ # basic environment accessors
157
+ def request; @_request; end
158
+ def request=(r); @_request = r; end;
159
+ def env; @_request.env; end
160
+ def params; @_request.params; end
161
+ def response; @_response; end
162
+ def response=(r); @_response = r; end
163
+ def mime_type; @_mime_type; end
164
+ def mime_type=(m); @_mime_type = m; end
165
+ def session; @_request.env['rack.session'] ||= {}; end
166
+
167
+ # By default, responses are assumed to be 200 (successful). Use
168
+ # status to change the code returned along with your response content.
169
+ def status(code)
170
+ response.status = code
171
+ end
172
+
173
+ # FIXME: make layout take a string or symbol param, remove .to_s from render_or_default, change blog.rb layout to use a symbol not string, check all other calls to layout() and change appropriately
174
+ # Determine the first best layout to be used by this page for rendering
175
+ def layout(mime_type, editing=false)
176
+ # if we're in production we'll have a reference to a layout record
177
+ #return page_layout_record if page_layout_record # FIXME: implement layout caching
178
+
179
+ # try and return a layout by name or by the name of the page's class
180
+ layout_name = editing ? edit_layout : page_layout
181
+ layout = site.layouts.where(name: layout_name, mime_type: mime_type).first
182
+
183
+ unless layout
184
+ layout_name = model.name.underscore
185
+ layout_name = "edit_#{layout_name}" if editing
186
+ layout = site.layouts.where(name: layout_name, mime_type: mime_type).first
187
+ end
188
+
189
+ return layout if layout
190
+
191
+ # otherwise fall back to the parent's layout
192
+ return parent.layout(mime_type) unless parent.nil?
193
+ raise LayoutNotFound
194
+ end
195
+
196
+ def render_layout(name, mime_type)
197
+ layout_record = site.layouts.where(name: name, mime_type: mime_type.to_s).first
198
+ raise LayoutNotFound if layout_record.nil?
199
+ layout_record.render(self)
200
+ end
201
+
202
+
203
+ # ----------------------------------------
204
+ # Default request handling
205
+ # ----------------------------------------
206
+ def render_or_default(mime_type, &block)
207
+ @content ||= content
208
+ editing = @_request && params && params['action'] == 'edit'
209
+ layout(mime_type.to_s, editing).render(self)
210
+ rescue LayoutNotFound
211
+ yield
212
+ end
213
+
214
+ def set_content(content)
215
+ @content = content
216
+ end
217
+
218
+ def content(*section)
219
+ if section.empty?
220
+ @content ||= get('content')
221
+ else
222
+ instance_variable_get("@content_for_#{section.first}") || ''
223
+ end
224
+ end
225
+
226
+ def content_for(section, options={}, &block)
227
+ if block_given?
228
+ content = Ember::Template.content_from_block(block).join
229
+ elsif options.key?(:partial)
230
+ content = partial(options[:partial])
231
+ end
232
+ instance_variable_set("@content_for_#{section}", content)
233
+ end
234
+
235
+ def partial(name)
236
+ name = name.to_s
237
+ name = name + '.html' unless name.end_with?('.html')
238
+ path = File.join(site.partials_directory, name)
239
+ raise LayoutNotFound, path unless File.exist?(path)
240
+ Ember::Template.new(IO.read(path), {source_file: path}).render(get_binding)
241
+ end
242
+
243
+ def page(*args)
244
+ if args.empty?
245
+ self
246
+ else
247
+ site.pages.where(path: args.first).first
248
+ end
249
+ end
250
+
251
+ def user_allowed_to?(action)
252
+ allowed = super(current_user(:page), action)
253
+ return true if allowed
254
+
255
+ prompt_login
256
+ flash[:permission_denied] = action
257
+ false
258
+ end
259
+
260
+ # show
261
+ respond_to :get do
262
+ with :html do
263
+ return unless user_allowed_to?(:view)
264
+ render_or_default(:html) { raise LayoutNotFound }
265
+ end
266
+
267
+ with :json do
268
+ return {success: false, unauthorised: true} unless user_allowed_to?(:view)
269
+ render_or_default(:json) do
270
+ to_json
271
+ end
272
+ end
273
+ end
274
+
275
+ # destroy
276
+ respond_to :delete do
277
+ with :html do
278
+ return unless user_allowed_to?(:delete)
279
+ response.redirect parent ? parent.path : '/'
280
+ destroy
281
+ end
282
+
283
+ with :json do
284
+ return {success: false, unauthorised: true} unless user_allowed_to?(:delete)
285
+ destroy
286
+ {success: true}
287
+ end
288
+ end
289
+
290
+ # update
291
+ respond_to :put do
292
+ with :html do
293
+ return unless user_allowed_to?(:update)
294
+
295
+ # update the page assuming a form created by to_form
296
+ path_was = path
297
+ success = from_json(params)
298
+
299
+ # updating the page can change its url
300
+ if success && (path != path_was)
301
+ flash[:performed_update] = true
302
+ flash[:update_successful] = success
303
+ response.redirect path
304
+ else
305
+ flash.now(:performed_update, true)
306
+ flash.now(:update_successful, success)
307
+ render_or_default(:html) { raise LayoutNotFound }
308
+ end
309
+ end
310
+
311
+ with :json do
312
+ return {success: false, unauthorised: true} unless user_allowed_to?(:update)
313
+ if params.key?('record')
314
+ values = JSON.parse(params['record'])
315
+ else
316
+ values = params
317
+ end
318
+
319
+ if from_json(values)
320
+ render_or_default(:json) do
321
+ {success: true, record: self}
322
+ end
323
+ else
324
+ render_or_default(:json) do
325
+ {success: false, errors: errors}
326
+ end
327
+ end
328
+ end
329
+ end
330
+
331
+ # DOC TIP: make sure a title is set or no path will be generated and this action will fail Rack::Lint
332
+
333
+ # create child
334
+ respond_to :post do
335
+ with :html do
336
+ return unless user_allowed_to?(:create)
337
+ new_page = model.default_child_model.new
338
+ new_page.parent = self
339
+
340
+ if new_page.from_json(params)
341
+ flash[:create_successful] = true
342
+ response.redirect new_page.path
343
+ else
344
+ if new_child_page
345
+ new_child_page.request = request
346
+ new_child_page.response = response
347
+ flash.now(:child_page, new_page)
348
+ new_child_page.respond_to_get_with_html
349
+ else
350
+ response.redirect request.referrer
351
+ end
352
+ end
353
+ end
354
+
355
+ with :json do
356
+ return {success: false, unauthorised: true} unless user_allowed_to?(:create)
357
+ new_page = model.default_child_model.new
358
+ new_page.parent = self
359
+
360
+ if params.key?('record')
361
+ values = JSON.parse(params['record'])
362
+ else
363
+ values = params
364
+ end
365
+
366
+ if new_page.from_json(values)
367
+ new_page.render_or_default(:json) do
368
+ {success: true, record: new_page}
369
+ end
370
+ else
371
+ new_page.render_or_default(:json) do
372
+ {success: false, errors: new_page.errors}
373
+ end
374
+ end
375
+ end
376
+ end
377
+
378
+ end