yodel 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +9 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +63 -0
- data/LICENSE +1 -0
- data/README.rdoc +20 -0
- data/Rakefile +1 -0
- data/bin/yodel +4 -0
- data/lib/yodel.rb +22 -0
- data/lib/yodel/application/application.rb +44 -0
- data/lib/yodel/application/extension.rb +59 -0
- data/lib/yodel/application/request_handler.rb +48 -0
- data/lib/yodel/application/yodel.rb +25 -0
- data/lib/yodel/command/command.rb +94 -0
- data/lib/yodel/command/deploy.rb +67 -0
- data/lib/yodel/command/dns_server.rb +16 -0
- data/lib/yodel/command/installer.rb +229 -0
- data/lib/yodel/config/config.rb +30 -0
- data/lib/yodel/config/environment.rb +16 -0
- data/lib/yodel/config/yodel.rb +21 -0
- data/lib/yodel/exceptions/destroyed_record.rb +2 -0
- data/lib/yodel/exceptions/domain_not_found.rb +16 -0
- data/lib/yodel/exceptions/duplicate_layout.rb +2 -0
- data/lib/yodel/exceptions/exceptions.rb +3 -0
- data/lib/yodel/exceptions/inconsistent_lock_state.rb +2 -0
- data/lib/yodel/exceptions/invalid_field.rb +2 -0
- data/lib/yodel/exceptions/invalid_index.rb +2 -0
- data/lib/yodel/exceptions/invalid_mixin.rb +2 -0
- data/lib/yodel/exceptions/invalid_model_field.rb +2 -0
- data/lib/yodel/exceptions/layout_not_found.rb +2 -0
- data/lib/yodel/exceptions/mass_assignment.rb +2 -0
- data/lib/yodel/exceptions/missing_migration.rb +2 -0
- data/lib/yodel/exceptions/missing_root_directory.rb +15 -0
- data/lib/yodel/exceptions/unable_to_acquire_lock.rb +2 -0
- data/lib/yodel/exceptions/unauthorised.rb +2 -0
- data/lib/yodel/exceptions/unknown_field.rb +2 -0
- data/lib/yodel/middleware/development_server.rb +180 -0
- data/lib/yodel/middleware/error_pages.rb +72 -0
- data/lib/yodel/middleware/public_assets.rb +78 -0
- data/lib/yodel/middleware/request.rb +16 -0
- data/lib/yodel/middleware/site_detector.rb +22 -0
- data/lib/yodel/mime_types/default_mime_set.rb +28 -0
- data/lib/yodel/mime_types/mime_type.rb +68 -0
- data/lib/yodel/mime_types/mime_type_set.rb +41 -0
- data/lib/yodel/mime_types/mime_types.rb +6 -0
- data/lib/yodel/mime_types/yodel.rb +15 -0
- data/lib/yodel/models/api/api.rb +1 -0
- data/lib/yodel/models/api/api_call.rb +87 -0
- data/lib/yodel/models/core/associations/association.rb +37 -0
- data/lib/yodel/models/core/associations/associations.rb +22 -0
- data/lib/yodel/models/core/associations/counts/many_association.rb +18 -0
- data/lib/yodel/models/core/associations/counts/one_association.rb +22 -0
- data/lib/yodel/models/core/associations/embedded/embedded_association.rb +47 -0
- data/lib/yodel/models/core/associations/embedded/embedded_record_array.rb +12 -0
- data/lib/yodel/models/core/associations/embedded/many_embedded_association.rb +62 -0
- data/lib/yodel/models/core/associations/embedded/one_embedded_association.rb +49 -0
- data/lib/yodel/models/core/associations/query/many_query_association.rb +10 -0
- data/lib/yodel/models/core/associations/query/one_query_association.rb +10 -0
- data/lib/yodel/models/core/associations/query/query_association.rb +64 -0
- data/lib/yodel/models/core/associations/record_association.rb +38 -0
- data/lib/yodel/models/core/associations/store/many_store_association.rb +32 -0
- data/lib/yodel/models/core/associations/store/one_store_association.rb +14 -0
- data/lib/yodel/models/core/associations/store/store_association.rb +51 -0
- data/lib/yodel/models/core/attachments/attachment.rb +73 -0
- data/lib/yodel/models/core/attachments/image.rb +38 -0
- data/lib/yodel/models/core/core.rb +15 -0
- data/lib/yodel/models/core/fields/alias_field.rb +32 -0
- data/lib/yodel/models/core/fields/array_field.rb +64 -0
- data/lib/yodel/models/core/fields/attachment_field.rb +42 -0
- data/lib/yodel/models/core/fields/boolean_field.rb +28 -0
- data/lib/yodel/models/core/fields/change_sensitive_array.rb +96 -0
- data/lib/yodel/models/core/fields/change_sensitive_hash.rb +53 -0
- data/lib/yodel/models/core/fields/color_field.rb +4 -0
- data/lib/yodel/models/core/fields/date_field.rb +35 -0
- data/lib/yodel/models/core/fields/decimal_field.rb +19 -0
- data/lib/yodel/models/core/fields/email_field.rb +10 -0
- data/lib/yodel/models/core/fields/enum_field.rb +33 -0
- data/lib/yodel/models/core/fields/field.rb +154 -0
- data/lib/yodel/models/core/fields/fields.rb +29 -0
- data/lib/yodel/models/core/fields/fields_field.rb +31 -0
- data/lib/yodel/models/core/fields/filter_mixin.rb +9 -0
- data/lib/yodel/models/core/fields/filtered_string_field.rb +5 -0
- data/lib/yodel/models/core/fields/filtered_text_field.rb +5 -0
- data/lib/yodel/models/core/fields/function_field.rb +28 -0
- data/lib/yodel/models/core/fields/hash_field.rb +54 -0
- data/lib/yodel/models/core/fields/html_field.rb +15 -0
- data/lib/yodel/models/core/fields/image_field.rb +11 -0
- data/lib/yodel/models/core/fields/integer_field.rb +25 -0
- data/lib/yodel/models/core/fields/password_field.rb +21 -0
- data/lib/yodel/models/core/fields/self_field.rb +27 -0
- data/lib/yodel/models/core/fields/string_field.rb +15 -0
- data/lib/yodel/models/core/fields/tags_field.rb +7 -0
- data/lib/yodel/models/core/fields/text_field.rb +7 -0
- data/lib/yodel/models/core/fields/time_field.rb +36 -0
- data/lib/yodel/models/core/functions/function.rb +471 -0
- data/lib/yodel/models/core/functions/functions.rb +2 -0
- data/lib/yodel/models/core/functions/trigger.rb +14 -0
- data/lib/yodel/models/core/log/log.rb +33 -0
- data/lib/yodel/models/core/log/log_entry.rb +12 -0
- data/lib/yodel/models/core/model/abstract_model.rb +59 -0
- data/lib/yodel/models/core/model/model.rb +460 -0
- data/lib/yodel/models/core/model/mongo_model.rb +25 -0
- data/lib/yodel/models/core/model/site_model.rb +17 -0
- data/lib/yodel/models/core/mongo/mongo.rb +3 -0
- data/lib/yodel/models/core/mongo/primary_key_factory.rb +12 -0
- data/lib/yodel/models/core/mongo/query.rb +68 -0
- data/lib/yodel/models/core/mongo/record_index.rb +89 -0
- data/lib/yodel/models/core/record/abstract_record.rb +411 -0
- data/lib/yodel/models/core/record/embedded_record.rb +47 -0
- data/lib/yodel/models/core/record/mongo_record.rb +83 -0
- data/lib/yodel/models/core/record/record.rb +386 -0
- data/lib/yodel/models/core/record/section.rb +21 -0
- data/lib/yodel/models/core/record/site_record.rb +31 -0
- data/lib/yodel/models/core/site/migration.rb +52 -0
- data/lib/yodel/models/core/site/remote.rb +61 -0
- data/lib/yodel/models/core/site/site.rb +202 -0
- data/lib/yodel/models/core/validations/email_address_validation.rb +24 -0
- data/lib/yodel/models/core/validations/embedded_records_validation.rb +31 -0
- data/lib/yodel/models/core/validations/errors.rb +51 -0
- data/lib/yodel/models/core/validations/excluded_from_validation.rb +10 -0
- data/lib/yodel/models/core/validations/excludes_combinations_validation.rb +18 -0
- data/lib/yodel/models/core/validations/format_validation.rb +10 -0
- data/lib/yodel/models/core/validations/included_in_validation.rb +10 -0
- data/lib/yodel/models/core/validations/includes_combinations_validation.rb +14 -0
- data/lib/yodel/models/core/validations/length_validation.rb +28 -0
- data/lib/yodel/models/core/validations/password_confirmation_validation.rb +11 -0
- data/lib/yodel/models/core/validations/required_validation.rb +9 -0
- data/lib/yodel/models/core/validations/unique_validation.rb +9 -0
- data/lib/yodel/models/core/validations/validation.rb +39 -0
- data/lib/yodel/models/core/validations/validations.rb +15 -0
- data/lib/yodel/models/email/email.rb +79 -0
- data/lib/yodel/models/migrations/01_record_model.rb +29 -0
- data/lib/yodel/models/migrations/02_page_model.rb +45 -0
- data/lib/yodel/models/migrations/03_layout_model.rb +38 -0
- data/lib/yodel/models/migrations/04_group_model.rb +61 -0
- data/lib/yodel/models/migrations/05_user_model.rb +24 -0
- data/lib/yodel/models/migrations/06_snippet_model.rb +13 -0
- data/lib/yodel/models/migrations/07_search_page_model.rb +32 -0
- data/lib/yodel/models/migrations/08_default_site_options.rb +21 -0
- data/lib/yodel/models/migrations/09_security_page_models.rb +36 -0
- data/lib/yodel/models/migrations/10_record_proxy_page_model.rb +17 -0
- data/lib/yodel/models/migrations/11_email_model.rb +28 -0
- data/lib/yodel/models/migrations/12_api_call_model.rb +23 -0
- data/lib/yodel/models/migrations/13_redirect_page_model.rb +13 -0
- data/lib/yodel/models/migrations/14_menu_model.rb +20 -0
- data/lib/yodel/models/models.rb +8 -0
- data/lib/yodel/models/pages/form_builder.rb +379 -0
- data/lib/yodel/models/pages/html_decorator.rb +132 -0
- data/lib/yodel/models/pages/layout.rb +120 -0
- data/lib/yodel/models/pages/menu.rb +32 -0
- data/lib/yodel/models/pages/page.rb +378 -0
- data/lib/yodel/models/pages/pages.rb +7 -0
- data/lib/yodel/models/pages/record_proxy_page.rb +188 -0
- data/lib/yodel/models/pages/redirect_page.rb +11 -0
- data/lib/yodel/models/search/search.rb +1 -0
- data/lib/yodel/models/search/search_page.rb +58 -0
- data/lib/yodel/models/security/facebook_login_page.rb +55 -0
- data/lib/yodel/models/security/group.rb +10 -0
- data/lib/yodel/models/security/guests_group.rb +5 -0
- data/lib/yodel/models/security/login_page.rb +20 -0
- data/lib/yodel/models/security/logout_page.rb +13 -0
- data/lib/yodel/models/security/noone_group.rb +5 -0
- data/lib/yodel/models/security/owner_group.rb +8 -0
- data/lib/yodel/models/security/password.rb +5 -0
- data/lib/yodel/models/security/password_reset_page.rb +47 -0
- data/lib/yodel/models/security/security.rb +10 -0
- data/lib/yodel/models/security/user.rb +33 -0
- data/lib/yodel/public/core/css/core.css +257 -0
- data/lib/yodel/public/core/css/reset.css +48 -0
- data/lib/yodel/public/core/images/cross.png +0 -0
- data/lib/yodel/public/core/images/spinner.gif +0 -0
- data/lib/yodel/public/core/images/tick.png +0 -0
- data/lib/yodel/public/core/images/yodel.png +0 -0
- data/lib/yodel/public/core/js/jquery.min.js +18 -0
- data/lib/yodel/public/core/js/json2.js +480 -0
- data/lib/yodel/public/core/js/yodel_jquery.js +238 -0
- data/lib/yodel/request/authentication.rb +76 -0
- data/lib/yodel/request/flash.rb +28 -0
- data/lib/yodel/request/request.rb +4 -0
- data/lib/yodel/requires.rb +47 -0
- data/lib/yodel/task_queue/queue_daemon.rb +33 -0
- data/lib/yodel/task_queue/queue_worker.rb +32 -0
- data/lib/yodel/task_queue/stats_thread.rb +27 -0
- data/lib/yodel/task_queue/task.rb +62 -0
- data/lib/yodel/task_queue/task_queue.rb +40 -0
- data/lib/yodel/types/date.rb +5 -0
- data/lib/yodel/types/object_id.rb +11 -0
- data/lib/yodel/types/time.rb +5 -0
- data/lib/yodel/version.rb +3 -0
- data/system/Library/LaunchDaemons/com.yodelcms.dns.plist +26 -0
- data/system/Library/LaunchDaemons/com.yodelcms.server.plist +26 -0
- data/system/etc/resolver/yodel +2 -0
- data/system/usr/local/bin/yodel_command_runner +2 -0
- data/system/usr/local/etc/yodel/development_settings.rb +28 -0
- data/system/usr/local/etc/yodel/production_settings.rb +27 -0
- data/system/var/log/yodel.log +0 -0
- data/test/helper.rb +18 -0
- data/test/test_yodel.rb +4 -0
- data/yodel.gemspec +47 -0
- 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
|