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.
- 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
|