yodel 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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