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,14 @@
|
|
|
1
|
+
class Trigger < SiteRecord
|
|
2
|
+
collection :triggers
|
|
3
|
+
field :source, :string
|
|
4
|
+
field :instructions, :array
|
|
5
|
+
|
|
6
|
+
before_save :compile_function
|
|
7
|
+
def compile_function
|
|
8
|
+
self.instructions = Function.new(source).instructions
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def run(record)
|
|
12
|
+
Function.new(instructions).execute(record)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require './log/log_entry'
|
|
2
|
+
|
|
3
|
+
class Log
|
|
4
|
+
def initialize(site)
|
|
5
|
+
@site = site
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def debug(message)
|
|
9
|
+
build_log_entry(LogEntry::DEBUG, message)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def info(message)
|
|
13
|
+
build_log_entry(LogEntry::INFO, message)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def warn(message)
|
|
17
|
+
build_log_entry(LogEntry::WARN, message)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def error(message)
|
|
21
|
+
build_log_entry(LogEntry::ERROR, message)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def fatal(message)
|
|
25
|
+
build_log_entry(LogEntry::FATAL, message)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
def build_log_entry(severity, message)
|
|
30
|
+
entry = LogEntry.new(@site)
|
|
31
|
+
entry.update(severity: severity, message: message)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module AbstractModel
|
|
2
|
+
def fields
|
|
3
|
+
@fields ||= {}
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def field(name, type, options={})
|
|
7
|
+
type = type.to_s
|
|
8
|
+
options = deep_stringify_keys({'type' => type}.merge(options))
|
|
9
|
+
fields[name.to_s] = Field.field_from_type(type).new(name.to_s, options)
|
|
10
|
+
end
|
|
11
|
+
alias :add_field :field
|
|
12
|
+
|
|
13
|
+
def modify_field(name, options={})
|
|
14
|
+
field = fields[name.to_s]
|
|
15
|
+
field.options = field.options.dup.merge(deep_stringify_keys(options))
|
|
16
|
+
field.instance_exec(field, &block) if block_given?
|
|
17
|
+
changed!('record_fields') if respond_to?(:changed!)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def remove_field(name)
|
|
21
|
+
fields.delete(name.to_s)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def embed_many(name, options={}, &block)
|
|
25
|
+
embedded_field = field(name, 'many_embedded', options)
|
|
26
|
+
embedded_field.instance_exec(embedded_field, &block) if block_given?
|
|
27
|
+
end
|
|
28
|
+
alias :add_embed_many :embed_many
|
|
29
|
+
|
|
30
|
+
def embed_one(name, options={}, &block)
|
|
31
|
+
embedded_field = field(name, 'one_embedded', options)
|
|
32
|
+
embedded_field.instance_exec(embedded_field, &block) if block_given?
|
|
33
|
+
end
|
|
34
|
+
alias :add_embed_one :embed_one
|
|
35
|
+
|
|
36
|
+
def many(name, options={})
|
|
37
|
+
type = query_association?(options) ? 'many_query' : 'many_store'
|
|
38
|
+
field(name, type, options)
|
|
39
|
+
end
|
|
40
|
+
alias :add_many :many
|
|
41
|
+
|
|
42
|
+
def one(name, options={})
|
|
43
|
+
type = query_association?(options) ? 'one_query' : 'one_store'
|
|
44
|
+
field(name, type, options)
|
|
45
|
+
end
|
|
46
|
+
alias :add_one :one
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
protected
|
|
50
|
+
def query_association?(options)
|
|
51
|
+
options[:store] == false || [:foreign_key, :extends, :through].any? {|opt| options[opt].present?}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def deep_stringify_keys(hash)
|
|
55
|
+
hash.each_with_object({}) do |(key, value), new_hash|
|
|
56
|
+
new_hash[key.to_s] = (value.respond_to?(:to_hash) ? deep_stringify_keys(value) : value)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
require './record/site_record'
|
|
2
|
+
require './model/abstract_model'
|
|
3
|
+
require './model/mongo_model'
|
|
4
|
+
require './model/site_model'
|
|
5
|
+
|
|
6
|
+
class Model < SiteRecord
|
|
7
|
+
attr_reader :unscoped, :record_class
|
|
8
|
+
collection :models
|
|
9
|
+
|
|
10
|
+
# ----------------------------------------
|
|
11
|
+
# Fields
|
|
12
|
+
# ----------------------------------------
|
|
13
|
+
field :name, :string, validations: {required: {}}
|
|
14
|
+
field :record_fields, :fields, validations: {required: {}}
|
|
15
|
+
field :triggers, :array
|
|
16
|
+
field :functions, :hash
|
|
17
|
+
field :icon, :string, inherited: true
|
|
18
|
+
field :menu_root, :boolean, default: false
|
|
19
|
+
field :hide_in_admin, :boolean, default: false
|
|
20
|
+
field :record_class_name, :string, default: 'Record', inherited: true
|
|
21
|
+
field :searchable, :boolean, default: true, inherited: true
|
|
22
|
+
field :indexes, :array, of: :strings
|
|
23
|
+
field :record_before_validation_callbacks, :array, of: :strings, inherited: true
|
|
24
|
+
field :record_after_validation_callbacks, :array, of: :strings, inherited: true
|
|
25
|
+
field :record_before_save_callbacks, :array, of: :strings, inherited: true
|
|
26
|
+
field :record_after_save_callbacks, :array, of: :strings, inherited: true
|
|
27
|
+
field :record_before_create_callbacks, :array, of: :strings, inherited: true
|
|
28
|
+
field :record_after_create_callbacks, :array, of: :strings, inherited: true
|
|
29
|
+
field :record_before_update_callbacks, :array, of: :strings, inherited: true
|
|
30
|
+
field :record_after_update_callbacks, :array, of: :strings, inherited: true
|
|
31
|
+
field :record_before_destroy_callbacks, :array, of: :strings, inherited: true
|
|
32
|
+
field :record_after_destroy_callbacks, :array, of: :strings, inherited: true
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ----------------------------------------
|
|
36
|
+
# Associations
|
|
37
|
+
# ----------------------------------------
|
|
38
|
+
many :mixins, model: :model
|
|
39
|
+
many :descendants, model: :model
|
|
40
|
+
many :allowed_children, model: :model, inherited: true
|
|
41
|
+
many :allowed_parents, model: :model, inherited: true
|
|
42
|
+
one :parent, model: :model
|
|
43
|
+
one :view_group, model: :group, inherited: true
|
|
44
|
+
one :update_group, model: :group, inherited: true
|
|
45
|
+
one :delete_group, model: :group, inherited: true
|
|
46
|
+
one :create_group, model: :group, inherited: true
|
|
47
|
+
many :children, model: :model, foreign_key: 'parent'
|
|
48
|
+
one :default_child_model, model: :model, inherited: true
|
|
49
|
+
one :new_child_page, model: :page, inherited: true
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def initialize(site, values={})
|
|
53
|
+
@cached_records_by_name = {}
|
|
54
|
+
super
|
|
55
|
+
@unscoped = Record.scoped(site, self)
|
|
56
|
+
@scope = Record.scoped(site, self, 'model' => get_raw('descendants'))
|
|
57
|
+
@record_class = Object.module_eval(get_raw('record_class_name'))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_str
|
|
61
|
+
"#<Model: #{name}>"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ----------------------------------------
|
|
66
|
+
# Callbacks
|
|
67
|
+
# ----------------------------------------
|
|
68
|
+
# TODO: use loops like in abstract record to write these functions
|
|
69
|
+
def run_record_before_validation_callbacks(record)
|
|
70
|
+
record_before_validation_callbacks.each {|fn| Function.new(fn).execute(record)}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def run_record_after_validation_callbacks(record)
|
|
74
|
+
record_after_validation_callbacks.each {|fn| Function.new(fn).execute(record)}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def run_record_before_save_callbacks(record)
|
|
78
|
+
record_before_save_callbacks.each {|fn| Function.new(fn).execute(record)}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def run_record_after_save_callbacks(record)
|
|
82
|
+
record_after_save_callbacks.each {|fn| Function.new(fn).execute(record)}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def run_record_before_create_callbacks(record)
|
|
86
|
+
record_before_create_callbacks.each {|fn| Function.new(fn).execute(record)}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def run_record_after_create_callbacks(record)
|
|
90
|
+
record_after_create_callbacks.each {|fn| Function.new(fn).execute(record)}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def run_record_before_update_callbacks(record)
|
|
94
|
+
record_before_update_callbacks.each {|fn| Function.new(fn).execute(record)}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def run_record_after_update_callbacks(record)
|
|
98
|
+
record_after_update_callbacks.each {|fn| Function.new(fn).execute(record)}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def run_record_before_destroy_callbacks(record)
|
|
102
|
+
record_before_destroy_callbacks.each {|fn| Function.new(fn).execute(record)}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def run_record_after_destroy_callbacks(record)
|
|
106
|
+
record_after_destroy_callbacks.each {|fn| Function.new(fn).execute(record)}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ----------------------------------------
|
|
111
|
+
# Records
|
|
112
|
+
# ----------------------------------------
|
|
113
|
+
extend Forwardable
|
|
114
|
+
def_delegators :@scope, :where, :limit, :skip, :sort, :count,
|
|
115
|
+
:last, :first, :all, :paginate, :find,
|
|
116
|
+
:find!, :exists?, :exist?, :find_each
|
|
117
|
+
|
|
118
|
+
# Load a record from a mongo document. If this model is not the model
|
|
119
|
+
# of the record, the appropriate model is found and used instead.
|
|
120
|
+
def load(site, values)
|
|
121
|
+
return nil if values.nil?
|
|
122
|
+
if values['model'] != id
|
|
123
|
+
site.models.find(values['model']).load(site, values)
|
|
124
|
+
else
|
|
125
|
+
record_class.new(self, site, values, false)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def new(values={})
|
|
130
|
+
record_class.new(self, site).tap {|record| record.update(values, false)}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Scope to retrieve all root records of a model type under a site, e.g
|
|
134
|
+
# Groups.roots(site). Returns all records with a nil parent.
|
|
135
|
+
def roots
|
|
136
|
+
self.where(parent: nil).order('index asc')
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Scope to retrieve the first (or only) root record of a model under a
|
|
140
|
+
# site, e.g Page.root(site) will retrieve the root page of a site
|
|
141
|
+
def root
|
|
142
|
+
self.where(parent: nil).order('index asc').first
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Simple lookup operator for models that have records with unique names.
|
|
146
|
+
# Used as if the model object was a hash: site.emails['name']
|
|
147
|
+
def [](name)
|
|
148
|
+
unless @cached_records_by_name.key?(name)
|
|
149
|
+
record = self.where(name: name).first
|
|
150
|
+
@cached_records_by_name[name] = record
|
|
151
|
+
site.cached_records[record.id] = record unless record.nil?
|
|
152
|
+
end
|
|
153
|
+
@cached_records_by_name[name]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ----------------------------------------
|
|
158
|
+
# Hierarchy
|
|
159
|
+
# ----------------------------------------
|
|
160
|
+
def ancestors
|
|
161
|
+
next_parent = self
|
|
162
|
+
Enumerator.new do |models|
|
|
163
|
+
while next_parent
|
|
164
|
+
models.yield next_parent
|
|
165
|
+
next_parent = next_parent.parent
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Combine the full set of parents and mixins in a way that doesn't duplicate models
|
|
171
|
+
# if mixins would cause a duplicate, and maintains the correct position of mixins in
|
|
172
|
+
# the inheritance tree for this model, any parents, and any mixins (and their mixins)
|
|
173
|
+
def parents_and_mixins
|
|
174
|
+
models = parent.try(:parents_and_mixins) || []
|
|
175
|
+
mixins.each do |mixin_model|
|
|
176
|
+
models |= mixin_model.parents_and_mixins
|
|
177
|
+
end
|
|
178
|
+
models << self
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def all_record_fields
|
|
182
|
+
parents_and_mixins.each_with_object({}) do |ancestor, fields|
|
|
183
|
+
fields.merge! ancestor.record_fields # FIXME: should this be record_fields or all_record_fields?
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ----------------------------------------
|
|
189
|
+
# Admin interface
|
|
190
|
+
# ----------------------------------------
|
|
191
|
+
def allowed_children_and_descendants
|
|
192
|
+
allowed_children.collect(&:descendants).flatten.uniq
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def allowed_child?(other_model)
|
|
196
|
+
allowed_children_and_descendants.include?(other_model)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Based on the list of allowed parents, returns true if the supplied
|
|
200
|
+
# model is a descendant of a valid parent of this model.
|
|
201
|
+
def allowed_parent?(other_model)
|
|
202
|
+
other_model_ancestors = other_model.ancestors.to_a
|
|
203
|
+
allowed_parents.any? {|parent| other_model_ancestors.include?(parent)}
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Returns an array of all allowed children and descendants of those
|
|
207
|
+
# children. This list respects both allowed_children and allowed_parents
|
|
208
|
+
# restrictions, so Page (which allows children that are
|
|
209
|
+
# descendants of Page) won't include Article which can only
|
|
210
|
+
# exist under a Blog page, even though Article is a
|
|
211
|
+
# descendant of Page.
|
|
212
|
+
def valid_children
|
|
213
|
+
allowed_children_and_descendants.select {|child| child.allowed_parent?(self)}
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def valid_child?(other_model)
|
|
217
|
+
valid_children.include?(other_model)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# ----------------------------------------
|
|
222
|
+
# Permissions
|
|
223
|
+
# ----------------------------------------
|
|
224
|
+
def user_allowed_to?(user, action, record)
|
|
225
|
+
case action
|
|
226
|
+
when :view
|
|
227
|
+
group = view_group
|
|
228
|
+
when :update
|
|
229
|
+
group = update_group
|
|
230
|
+
when :delete
|
|
231
|
+
group = delete_group
|
|
232
|
+
when :create
|
|
233
|
+
group = create_group
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
return true if group.nil?
|
|
237
|
+
group.permitted?(user, record)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def user_allowed_to_view?(user, record)
|
|
241
|
+
user_allowed_to?(user, :view, record)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def user_allowed_to_update?(user, record)
|
|
245
|
+
user_allowed_to?(user, :update, record)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def user_allowed_to_delete?(user, record)
|
|
249
|
+
user_allowed_to?(user, :delete, record)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def user_allowed_to_create?(user, record)
|
|
253
|
+
user_allowed_to?(user, :create, record)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# ----------------------------------------
|
|
258
|
+
# Migrations
|
|
259
|
+
# ----------------------------------------
|
|
260
|
+
# Convenience method for migrations, so modifications can be specified with
|
|
261
|
+
# site.model_name.modify { field ... etc. }
|
|
262
|
+
def modify(&block)
|
|
263
|
+
instance_eval &block
|
|
264
|
+
save
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# TODO: ensure field name != a public method name
|
|
268
|
+
|
|
269
|
+
def add_field(name, type, options={})
|
|
270
|
+
name = name.to_s
|
|
271
|
+
|
|
272
|
+
# preconditions
|
|
273
|
+
raise InvalidModelField.new("Duplicate field name") if record_fields.key?(name)
|
|
274
|
+
raise InvalidModelField.new("Type must be a known yodel field type") unless valid_type?(type)
|
|
275
|
+
raise InvalidModelField.new("Field name cannot start with an underscore") if name.start_with?('_')
|
|
276
|
+
|
|
277
|
+
# add the field to the model and subclasses
|
|
278
|
+
field_type = Field.field_from_type(type.to_s)
|
|
279
|
+
field = field_type.new(name, deep_stringify_keys(options.merge(type: type.to_s)))
|
|
280
|
+
RecordIndex.add_index_for_field(self, field) if field.index?
|
|
281
|
+
record_fields[name] = field
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def remove_field(name)
|
|
285
|
+
field = record_fields.delete(name.to_s)
|
|
286
|
+
raise InvalidModelField.new("Unknown field name") if field.nil?
|
|
287
|
+
RecordIndex.remove_index_for_field(self, field) if field.index?
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def modify_field(name, options={}, &block)
|
|
291
|
+
field = record_fields[name.to_s]
|
|
292
|
+
field.options = field.options.dup.merge(deep_stringify_keys(options))
|
|
293
|
+
field.instance_exec(field, &block) if block_given?
|
|
294
|
+
changed!('record_fields')
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# TODO: remove copy of this method when abstract_model is mixed in
|
|
298
|
+
def deep_stringify_keys(hash)
|
|
299
|
+
hash.each_with_object({}) do |(key, value), new_hash|
|
|
300
|
+
new_hash[key.to_s] = (value.respond_to?(:to_hash) ? deep_stringify_keys(value) : value)
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# TODO: modify versions of the association methods
|
|
306
|
+
|
|
307
|
+
def add_embed_many(name, options={}, &block)
|
|
308
|
+
embedded_field = add_field(name, 'many_embedded', options)
|
|
309
|
+
embedded_field.instance_exec(embedded_field, &block) if block_given?
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def remove_embed_many(name)
|
|
313
|
+
remove_field(name)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def add_embed_one(name, options={}, &block)
|
|
317
|
+
embedded_field = add_field(name, 'one_embedded', options)
|
|
318
|
+
embedded_field.instance_exec(embedded_field, &block) if block_given?
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def remove_embed_one(name)
|
|
322
|
+
remove_field(name)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def add_many(name, options={})
|
|
326
|
+
type = query_association?(options) ? 'many_query' : 'many_store'
|
|
327
|
+
add_field(name, type, options)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def remove_many(name)
|
|
331
|
+
remove_field(name)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def add_one(name, options={})
|
|
335
|
+
type = query_association?(options) ? 'one_query' : 'one_store'
|
|
336
|
+
add_field(name, type, options)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def remove_one(name)
|
|
340
|
+
remove_field(name)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def query_association?(options)
|
|
344
|
+
options[:store] == false || [:foreign_key, :extends, :through].any? {|opt| options[opt].present?}
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def add_index(name, *fields)
|
|
348
|
+
raise InvalidIndex, 'Indexes must be built on at least one field' if fields.empty?
|
|
349
|
+
spec = fields.collect do |field|
|
|
350
|
+
if field.is_a?(Array)
|
|
351
|
+
[field.first.to_s, (field.last == :desc) ? Mongo::DESCENDING : Mongo::ASCENDING]
|
|
352
|
+
else
|
|
353
|
+
[field.to_s, Mongo::ASCENDING]
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
RecordIndex.add_index_for_model(self, name, spec)
|
|
357
|
+
indexes << name
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def remove_index(name)
|
|
361
|
+
RecordIndex.remove_index_for_model(self, name)
|
|
362
|
+
indexes.delete(name)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Create a new model which inherits from the current model. If supplied, a block
|
|
366
|
+
# is run and passed a reference to the new model.
|
|
367
|
+
def create_model(name, &block)
|
|
368
|
+
name = name.to_s.tableize
|
|
369
|
+
raise "Model name '#{name}' is not unique" if site.model_types.key?(name)
|
|
370
|
+
|
|
371
|
+
# create a new instance of model
|
|
372
|
+
child = self.class.new(site)
|
|
373
|
+
child.name = name.camelcase.singularize
|
|
374
|
+
child.parent = self
|
|
375
|
+
|
|
376
|
+
# inherited fields
|
|
377
|
+
fields.each do |name, field|
|
|
378
|
+
child.set(name, get(name)) if field.inherited?
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# insert the model in to the site models list
|
|
382
|
+
class_name = name.classify
|
|
383
|
+
site.model_types[name] = child.id
|
|
384
|
+
site.model_plural_names[class_name] = name
|
|
385
|
+
site.save
|
|
386
|
+
|
|
387
|
+
# append the model to ancestor descendant lists (these are used in queries to
|
|
388
|
+
# restrict the type of records returned, e.g pages.all => _model: ['Page', ...]
|
|
389
|
+
child.tap do |child|
|
|
390
|
+
child.add_descendant(child)
|
|
391
|
+
child.instance_exec(child, &block) if block_given?
|
|
392
|
+
child.save
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Add a new mixin to this model
|
|
397
|
+
def add_mixin(model)
|
|
398
|
+
raise InvalidMixin.new("#{model.name} already mixed in to this model") if mixins.include?(model)
|
|
399
|
+
raise InvalidMixin.new("Mixin cannot be a parent") if ancestors.include?(model)
|
|
400
|
+
|
|
401
|
+
# for all intents and purposes, by mixing in a model, we are a subtype of that model
|
|
402
|
+
model.add_descendant(self)
|
|
403
|
+
mixins << model
|
|
404
|
+
save
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Remove a mixin from this model
|
|
408
|
+
def remove_mixin(model)
|
|
409
|
+
model.remove_descendant(self)
|
|
410
|
+
mixins.delete(model)
|
|
411
|
+
save
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# Destroys all records which are instances of this model, removes a reference to
|
|
415
|
+
# the model from the parent site, and repeats for any child models of the model.
|
|
416
|
+
def destroy
|
|
417
|
+
# remove this model from the model tree
|
|
418
|
+
parent.try(:remove_descendant, self)
|
|
419
|
+
mixins.each {|mixin| mixin.remove_descendant(self)}
|
|
420
|
+
|
|
421
|
+
# destroy model subclasses, and all record instances
|
|
422
|
+
children.each(&:destroy)
|
|
423
|
+
all.each(&:destroy)
|
|
424
|
+
|
|
425
|
+
# remove the association between the site and this model
|
|
426
|
+
site.model_types.delete(name.underscore.pluralize)
|
|
427
|
+
site.model_plural_names.delete(name)
|
|
428
|
+
site.save
|
|
429
|
+
|
|
430
|
+
# remove any remaining indexes
|
|
431
|
+
indexes.each do |name|
|
|
432
|
+
RecordIndex.remove_index_for_model(self, name)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
record_fields.each do |name, field|
|
|
436
|
+
RecordIndex.remove_index_for_field(self, field) if field.index?
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# destroy the model record
|
|
440
|
+
super
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
protected
|
|
445
|
+
def valid_type?(type)
|
|
446
|
+
Field.field_from_type(type.to_s).present?
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def add_descendant(model)
|
|
450
|
+
parent.try(:add_descendant, model)
|
|
451
|
+
descendants << model unless descendants.include?(model)
|
|
452
|
+
save
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def remove_descendant(model)
|
|
456
|
+
parent.try(:remove_descendant, model)
|
|
457
|
+
descendants.delete(model)
|
|
458
|
+
save
|
|
459
|
+
end
|
|
460
|
+
end
|