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,47 @@
|
|
|
1
|
+
require './record/abstract_record'
|
|
2
|
+
|
|
3
|
+
class EmbeddedRecord < AbstractRecord
|
|
4
|
+
attr_reader :embedded_field, :parent_record
|
|
5
|
+
|
|
6
|
+
def initialize(embedded_field, parent_record, values={}, new_record=true)
|
|
7
|
+
@embedded_field = embedded_field
|
|
8
|
+
@parent_record = parent_record
|
|
9
|
+
super(values, new_record)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def site
|
|
13
|
+
parent_record.site
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def set(name, value)
|
|
17
|
+
super
|
|
18
|
+
parent_record.changed!(embedded_field.name)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def set_raw(name, value)
|
|
22
|
+
super
|
|
23
|
+
parent_record.changed!(embedded_field.name)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def changed!(name)
|
|
27
|
+
super
|
|
28
|
+
parent_record.changed!(embedded_field.name)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def fields
|
|
32
|
+
embedded_field.fields
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def perform_save
|
|
36
|
+
embedded_field.save(self, parent_record)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def perform_destroy
|
|
40
|
+
embedded_field.destroy(self, parent_record)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def perform_reload(id)
|
|
44
|
+
# TODO: determine whether reloading the parent record will cause any problems
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
require './record/abstract_record'
|
|
2
|
+
require './model/mongo_model'
|
|
3
|
+
|
|
4
|
+
class MongoRecord < AbstractRecord
|
|
5
|
+
extend MongoModel
|
|
6
|
+
|
|
7
|
+
def fields
|
|
8
|
+
self.class.fields
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def collection
|
|
12
|
+
self.class.collection
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def id
|
|
16
|
+
@values['_id']
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def set_id(new_id)
|
|
20
|
+
@values['_id'] = new_id
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def default_values
|
|
24
|
+
super.merge({'_id' => PrimaryKeyFactory.pk})
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def inspect_hash
|
|
28
|
+
{id: id}.merge(super)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def perform_save
|
|
32
|
+
id = collection.save(@values, safe: true)
|
|
33
|
+
rescue
|
|
34
|
+
# TODO: write Yodel.db.get_last_error to the log or as a warning to the site
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def perform_destroy
|
|
39
|
+
result = collection.remove(_id: @values['_id'])
|
|
40
|
+
rescue
|
|
41
|
+
false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def perform_reload(params)
|
|
45
|
+
document = load_mongo_document(_id: params[:id])
|
|
46
|
+
initialize(document)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def load_mongo_document(scope)
|
|
50
|
+
collection.find_one(scope)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def load_from_mongo(scope)
|
|
54
|
+
@values = load_mongo_document(scope)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def increment!(name, value=1, conditions={})
|
|
58
|
+
name = name.to_s
|
|
59
|
+
|
|
60
|
+
# preconditions
|
|
61
|
+
raise DestroyedRecord if destroyed?
|
|
62
|
+
raise UnknownField, "Unknown field <#{name}>" unless field?(name)
|
|
63
|
+
return false if new?
|
|
64
|
+
|
|
65
|
+
increment_field = field(name)
|
|
66
|
+
raise InvalidField, "Field #{name} is not numeric" unless increment_field.numeric?
|
|
67
|
+
|
|
68
|
+
# atomic increment (amount can be negative)
|
|
69
|
+
conditions = {_id: id}.merge(Plucky::CriteriaHash.new(conditions).to_hash)
|
|
70
|
+
result = collection.update(conditions, {'$inc' => {name => value}}, safe: true)
|
|
71
|
+
succeeded = successful_result?(result)
|
|
72
|
+
|
|
73
|
+
# update the object cache, and indicate if the update was successful
|
|
74
|
+
new_value = (get(name) || 0) + value
|
|
75
|
+
@values[name] = @typecast[name] = new_value if succeeded
|
|
76
|
+
succeeded
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
def successful_result?(result)
|
|
81
|
+
result['n'] != 0
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
require './record/abstract_record'
|
|
2
|
+
require './record/embedded_record'
|
|
3
|
+
require './record/mongo_record'
|
|
4
|
+
require './record/site_record'
|
|
5
|
+
require './record/section'
|
|
6
|
+
require './model/model'
|
|
7
|
+
|
|
8
|
+
class Record < SiteRecord
|
|
9
|
+
collection :records
|
|
10
|
+
attr_reader :model_record, :model, :mixins
|
|
11
|
+
attr_accessor :real_record # reference to the 'real' record if this object is a mixin
|
|
12
|
+
|
|
13
|
+
def initialize(model, site, values={}, new_record=true)
|
|
14
|
+
@model_record = model
|
|
15
|
+
@site = site
|
|
16
|
+
@model = load_model(model, values)
|
|
17
|
+
@mixins = create_mixin_instances(values)
|
|
18
|
+
super(site, values, new_record)
|
|
19
|
+
|
|
20
|
+
# mixins have their db access methods delegated to the "real record"
|
|
21
|
+
# (the main object representing the mongo document). To maintain a
|
|
22
|
+
# transparency between objects, key instance variables in the mixin
|
|
23
|
+
# are changed to refer to the same instance variables in the real record.
|
|
24
|
+
delegate_mixins
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_str
|
|
28
|
+
"#<#{model_record.name}: #{id}>"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def default_values
|
|
32
|
+
super.merge({'model' => model.id})
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def collection
|
|
36
|
+
Record.collection
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def perform_reload(params)
|
|
40
|
+
document = load_mongo_document(_id: params[:id])
|
|
41
|
+
initialize(params[:model], params[:site], document)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def prepare_reload_params
|
|
45
|
+
super.tap {|vals| vals[:model] = @model}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ----------------------------------------
|
|
50
|
+
# Permissions
|
|
51
|
+
# ----------------------------------------
|
|
52
|
+
def user_allowed_to?(user, action)
|
|
53
|
+
model.user_allowed_to?(user, action, self)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def user_allowed_to_view?(user)
|
|
57
|
+
model.user_allowed_to?(user, :view, self)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def user_allowed_to_update?(user)
|
|
61
|
+
model.user_allowed_to?(user, :update, self)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def user_allowed_to_delete?(user)
|
|
65
|
+
model.user_allowed_to?(user, :delete, self)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def user_allowed_to_create?(user)
|
|
69
|
+
model.user_allowed_to?(user, :create, self)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ----------------------------------------
|
|
74
|
+
# Modelling
|
|
75
|
+
# ----------------------------------------
|
|
76
|
+
def fields
|
|
77
|
+
@fields ||= @model.all_record_fields
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def field_sections
|
|
81
|
+
if @sections.nil?
|
|
82
|
+
keyed_sections = Hash.new do |hash, key|
|
|
83
|
+
hash[key] = Section.new(key)
|
|
84
|
+
end
|
|
85
|
+
fields.each do |name, field|
|
|
86
|
+
keyed_sections[field.section] << field
|
|
87
|
+
end
|
|
88
|
+
@sections = keyed_sections.values
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
@sections
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def fields_for_section(section)
|
|
95
|
+
fields.select do |name, field|
|
|
96
|
+
field.display? && field.section == section && field.default_input_type.present? && field.default_input_type != :embedded
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def inspect_hash
|
|
101
|
+
{model: model, parent: parent, index: index}.merge(super)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def load_model(model, values)
|
|
105
|
+
return model if values['eigenmodel'].nil?
|
|
106
|
+
eigenmodel = site.models.find(values['eigenmodel'])
|
|
107
|
+
values['eigenmodel'] = nil if eigenmodel.nil?
|
|
108
|
+
eigenmodel || model
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def create_eigenmodel
|
|
112
|
+
return eigenmodel if eigenmodel?
|
|
113
|
+
new_eigenmodel = model.create_model("#{id}_eigenmodel")
|
|
114
|
+
self.eigenmodel = new_eigenmodel
|
|
115
|
+
@model = new_eigenmodel
|
|
116
|
+
save
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def remove_eigenmodel
|
|
120
|
+
eigenmodel.destroy if eigenmodel?
|
|
121
|
+
self.eigenmodel = nil
|
|
122
|
+
save
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def has_eigenmodel?
|
|
126
|
+
self.eigenmodel != nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def model_name
|
|
130
|
+
has_eigenmodel? ? self.eigenmodel.parent.name : self.model_record.name
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def create_mixin_instances(values)
|
|
134
|
+
return [] if @model.nil?
|
|
135
|
+
@model.mixins.collect do |mixin_model|
|
|
136
|
+
mixin_model.record_class.new(mixin_model, site, values)
|
|
137
|
+
end.compact
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def delegate_mixins
|
|
141
|
+
extend SingleForwardable
|
|
142
|
+
ancestors = self.class.ancestors
|
|
143
|
+
included_classes = []
|
|
144
|
+
|
|
145
|
+
mixins.each_with_index do |mixin, index|
|
|
146
|
+
# reassign the mixin object's instance vars
|
|
147
|
+
%w{@model @new @site @values @typecast @changed @errors @stash}.each do |var|
|
|
148
|
+
mixin.instance_variable_set(var, instance_variable_get(var))
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# delegate database access to the main object
|
|
152
|
+
mixin.extend SingleForwardable
|
|
153
|
+
mixin.real_record = self
|
|
154
|
+
mixin.def_delegators :@real_record, :save, :save_without_validation, :destroy, :update,
|
|
155
|
+
:reload, :fields
|
|
156
|
+
|
|
157
|
+
# delegate mixin instance methods (if custom classes are used) to the mixin
|
|
158
|
+
# so mixing in user to a page makes the page appear to have user methods
|
|
159
|
+
# such as :reset_password. Delegation of methods continues up the class
|
|
160
|
+
# hierarchy until the class ancestry of the main object and mixin converge
|
|
161
|
+
# (only unique classes are mixed in). So mixing a user subclass into a page
|
|
162
|
+
# would mixin the subclass, followed by user. We stop at record since
|
|
163
|
+
# both page and the user subclass inherit from it.
|
|
164
|
+
mixin.class.ancestors.each do |klass|
|
|
165
|
+
break if ancestors.include?(klass)
|
|
166
|
+
next if included_classes.include?(klass)
|
|
167
|
+
def_delegators "@mixins[#{index}]", *klass.instance_methods(false)
|
|
168
|
+
included_classes << klass
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ----------------------------------------
|
|
175
|
+
# Callbacks
|
|
176
|
+
# ----------------------------------------
|
|
177
|
+
# extend callbacks to work with mixins
|
|
178
|
+
Model::CALLBACKS.each do |callback|
|
|
179
|
+
Model::ORDERS.each do |order|
|
|
180
|
+
eval "
|
|
181
|
+
def run_#{order}_#{callback}_callbacks
|
|
182
|
+
#{order}_completed = self.class._#{order}_#{callback}_callbacks.dup
|
|
183
|
+
super
|
|
184
|
+
|
|
185
|
+
mixins.collect {|mixin| mixin.class._#{order}_#{callback}_callbacks}.flatten.each do |callback|
|
|
186
|
+
unless #{order}_completed.include?(callback)
|
|
187
|
+
send callback
|
|
188
|
+
#{order}_completed << callback
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
before_validation :run_record_before_validation_callbacks
|
|
197
|
+
def run_record_before_validation_callbacks
|
|
198
|
+
model.run_record_before_validation_callbacks(self)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
after_validation :run_record_after_validation_callbacks
|
|
202
|
+
def run_record_after_validation_callbacks
|
|
203
|
+
model.run_record_after_validation_callbacks(self)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
before_save :run_record_before_save_callbacks
|
|
207
|
+
def run_record_before_save_callbacks
|
|
208
|
+
model.run_record_before_save_callbacks(self)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
after_save :run_record_after_save_callbacks
|
|
212
|
+
def run_record_after_save_callbacks
|
|
213
|
+
model.run_record_after_save_callbacks(self)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
before_create :run_record_before_create_callbacks
|
|
217
|
+
def run_record_before_create_callbacks
|
|
218
|
+
model.run_record_before_create_callbacks(self)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
after_create :run_record_after_create_callbacks
|
|
222
|
+
def run_record_after_create_callbacks
|
|
223
|
+
model.run_record_after_create_callbacks(self)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
before_update :run_record_before_update_callbacks
|
|
227
|
+
def run_record_before_update_callbacks
|
|
228
|
+
model.run_record_before_update_callbacks(self)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
after_update :run_record_after_update_callbacks
|
|
232
|
+
def run_record_after_update_callbacks
|
|
233
|
+
model.run_record_after_update_callbacks(self)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
before_destroy :run_record_before_destroy_callbacks
|
|
237
|
+
def run_record_before_destroy_callbacks
|
|
238
|
+
model.run_record_before_destroy_callbacks(self)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
after_destroy :run_record_after_destroy_callbacks
|
|
242
|
+
def run_record_after_destroy_callbacks
|
|
243
|
+
model.run_record_after_destroy_callbacks(self)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# ----------------------------------------
|
|
249
|
+
# Hierarchical methods
|
|
250
|
+
# ----------------------------------------
|
|
251
|
+
# insertion and deletion to maintin the integrity of the 'index' field
|
|
252
|
+
before_validation :append_to_siblings
|
|
253
|
+
before_destroy :remove_from_siblings
|
|
254
|
+
before_destroy :destroy_children
|
|
255
|
+
|
|
256
|
+
def append_to_siblings
|
|
257
|
+
return unless new?
|
|
258
|
+
highest_index = siblings.last.try(:index) || 0
|
|
259
|
+
self.index = highest_index + 1
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# FIXME: these need to be atomic ops over the whole set of children
|
|
263
|
+
# FIXME: it also seems weird to perform increments on siblings, but leave the index change to this record unchanged
|
|
264
|
+
def insert_in_siblings(new_index)
|
|
265
|
+
original_parent = self.parent
|
|
266
|
+
remove_from_siblings if index
|
|
267
|
+
self.parent = original_parent
|
|
268
|
+
siblings.where(:index.gte => new_index).each do |sibling|
|
|
269
|
+
sibling.increment!(:index)
|
|
270
|
+
end
|
|
271
|
+
self.index = new_index
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def remove_from_siblings
|
|
275
|
+
siblings.where(:index.gte => index).each do |sibling|
|
|
276
|
+
sibling.increment!(:index, -1)
|
|
277
|
+
end
|
|
278
|
+
self.index = nil
|
|
279
|
+
self.parent = nil
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def destroy_children
|
|
283
|
+
children.each(&:destroy)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Siblings of this record (other records with the same parent)
|
|
287
|
+
def siblings
|
|
288
|
+
unless parent.nil?
|
|
289
|
+
model.unscoped.where(:parent => parent.try(:id), :_id.ne => id).order('index asc')
|
|
290
|
+
else
|
|
291
|
+
# A parent ID of nil indicates this record is the root of a tree. Since there
|
|
292
|
+
# are multiple trees (including the model tree), a sibling query makes no sense.
|
|
293
|
+
model.unscoped.where(:nonexistant_field => 'true')
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# All direct descendents of this record, and the record itself
|
|
298
|
+
def children_and_self
|
|
299
|
+
[self, children].flatten
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# All descendent children of this record, i.e children, grandchildren and so on.
|
|
303
|
+
def all_children
|
|
304
|
+
[self, children.collect(&:all_children)].flatten
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# An array of parent records all the way back to a root record. e.g calling on
|
|
308
|
+
# a page two levels deep would return: [page, parent, root]
|
|
309
|
+
def parents
|
|
310
|
+
[self, self.parent.try(:parents)].flatten.compact
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# True if this record has no parent
|
|
314
|
+
def root?
|
|
315
|
+
parent.nil?
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# True if record is a parent (ancestor) of this record
|
|
319
|
+
def parent?(record)
|
|
320
|
+
parents.include?(record)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Returns the first parent which is an instance of type
|
|
324
|
+
def first_parent(type, exact=false)
|
|
325
|
+
model = site.model_by_plural_name(type.to_s.downcase.pluralize)
|
|
326
|
+
|
|
327
|
+
if exact
|
|
328
|
+
match = parents.find {|record| record.model == model}
|
|
329
|
+
else
|
|
330
|
+
match = parents.find {|record| record.model.parents_and_mixins.include?(model)}
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
if block_given?
|
|
334
|
+
yield match
|
|
335
|
+
else
|
|
336
|
+
match
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Finds the first parent which can respond to 'message' and returns the
|
|
341
|
+
# result. Will return nil if no parents response to the message, however,
|
|
342
|
+
# keep in mind that nil may be a valid response to this message.
|
|
343
|
+
def first_response_to(message)
|
|
344
|
+
message = message.to_s
|
|
345
|
+
parents.find do |record|
|
|
346
|
+
return record.send(message) if record.respond_to?(message) || record.fields.keys.include?(message)
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def first_non_blank_response_to(message)
|
|
351
|
+
message = message.to_s
|
|
352
|
+
parents.find do |record|
|
|
353
|
+
if record.respond_to?(message) || record.fields.keys.include?(message)
|
|
354
|
+
value = record.send(message)
|
|
355
|
+
return value unless value.blank?
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
''
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# ----------------------------------------
|
|
363
|
+
# Rendering
|
|
364
|
+
# ----------------------------------------
|
|
365
|
+
def content
|
|
366
|
+
@content
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def set_content(content)
|
|
370
|
+
@content = content
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def get_binding
|
|
374
|
+
binding
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# ----------------------------------------
|
|
379
|
+
# Search
|
|
380
|
+
# ----------------------------------------
|
|
381
|
+
before_save :update_search_keywords
|
|
382
|
+
def update_search_keywords
|
|
383
|
+
return unless model.searchable?
|
|
384
|
+
self.search_keywords = search_terms
|
|
385
|
+
end
|
|
386
|
+
end
|