yodel 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +9 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +63 -0
- data/LICENSE +1 -0
- data/README.rdoc +20 -0
- data/Rakefile +1 -0
- data/bin/yodel +4 -0
- data/lib/yodel.rb +22 -0
- data/lib/yodel/application/application.rb +44 -0
- data/lib/yodel/application/extension.rb +59 -0
- data/lib/yodel/application/request_handler.rb +48 -0
- data/lib/yodel/application/yodel.rb +25 -0
- data/lib/yodel/command/command.rb +94 -0
- data/lib/yodel/command/deploy.rb +67 -0
- data/lib/yodel/command/dns_server.rb +16 -0
- data/lib/yodel/command/installer.rb +229 -0
- data/lib/yodel/config/config.rb +30 -0
- data/lib/yodel/config/environment.rb +16 -0
- data/lib/yodel/config/yodel.rb +21 -0
- data/lib/yodel/exceptions/destroyed_record.rb +2 -0
- data/lib/yodel/exceptions/domain_not_found.rb +16 -0
- data/lib/yodel/exceptions/duplicate_layout.rb +2 -0
- data/lib/yodel/exceptions/exceptions.rb +3 -0
- data/lib/yodel/exceptions/inconsistent_lock_state.rb +2 -0
- data/lib/yodel/exceptions/invalid_field.rb +2 -0
- data/lib/yodel/exceptions/invalid_index.rb +2 -0
- data/lib/yodel/exceptions/invalid_mixin.rb +2 -0
- data/lib/yodel/exceptions/invalid_model_field.rb +2 -0
- data/lib/yodel/exceptions/layout_not_found.rb +2 -0
- data/lib/yodel/exceptions/mass_assignment.rb +2 -0
- data/lib/yodel/exceptions/missing_migration.rb +2 -0
- data/lib/yodel/exceptions/missing_root_directory.rb +15 -0
- data/lib/yodel/exceptions/unable_to_acquire_lock.rb +2 -0
- data/lib/yodel/exceptions/unauthorised.rb +2 -0
- data/lib/yodel/exceptions/unknown_field.rb +2 -0
- data/lib/yodel/middleware/development_server.rb +180 -0
- data/lib/yodel/middleware/error_pages.rb +72 -0
- data/lib/yodel/middleware/public_assets.rb +78 -0
- data/lib/yodel/middleware/request.rb +16 -0
- data/lib/yodel/middleware/site_detector.rb +22 -0
- data/lib/yodel/mime_types/default_mime_set.rb +28 -0
- data/lib/yodel/mime_types/mime_type.rb +68 -0
- data/lib/yodel/mime_types/mime_type_set.rb +41 -0
- data/lib/yodel/mime_types/mime_types.rb +6 -0
- data/lib/yodel/mime_types/yodel.rb +15 -0
- data/lib/yodel/models/api/api.rb +1 -0
- data/lib/yodel/models/api/api_call.rb +87 -0
- data/lib/yodel/models/core/associations/association.rb +37 -0
- data/lib/yodel/models/core/associations/associations.rb +22 -0
- data/lib/yodel/models/core/associations/counts/many_association.rb +18 -0
- data/lib/yodel/models/core/associations/counts/one_association.rb +22 -0
- data/lib/yodel/models/core/associations/embedded/embedded_association.rb +47 -0
- data/lib/yodel/models/core/associations/embedded/embedded_record_array.rb +12 -0
- data/lib/yodel/models/core/associations/embedded/many_embedded_association.rb +62 -0
- data/lib/yodel/models/core/associations/embedded/one_embedded_association.rb +49 -0
- data/lib/yodel/models/core/associations/query/many_query_association.rb +10 -0
- data/lib/yodel/models/core/associations/query/one_query_association.rb +10 -0
- data/lib/yodel/models/core/associations/query/query_association.rb +64 -0
- data/lib/yodel/models/core/associations/record_association.rb +38 -0
- data/lib/yodel/models/core/associations/store/many_store_association.rb +32 -0
- data/lib/yodel/models/core/associations/store/one_store_association.rb +14 -0
- data/lib/yodel/models/core/associations/store/store_association.rb +51 -0
- data/lib/yodel/models/core/attachments/attachment.rb +73 -0
- data/lib/yodel/models/core/attachments/image.rb +38 -0
- data/lib/yodel/models/core/core.rb +15 -0
- data/lib/yodel/models/core/fields/alias_field.rb +32 -0
- data/lib/yodel/models/core/fields/array_field.rb +64 -0
- data/lib/yodel/models/core/fields/attachment_field.rb +42 -0
- data/lib/yodel/models/core/fields/boolean_field.rb +28 -0
- data/lib/yodel/models/core/fields/change_sensitive_array.rb +96 -0
- data/lib/yodel/models/core/fields/change_sensitive_hash.rb +53 -0
- data/lib/yodel/models/core/fields/color_field.rb +4 -0
- data/lib/yodel/models/core/fields/date_field.rb +35 -0
- data/lib/yodel/models/core/fields/decimal_field.rb +19 -0
- data/lib/yodel/models/core/fields/email_field.rb +10 -0
- data/lib/yodel/models/core/fields/enum_field.rb +33 -0
- data/lib/yodel/models/core/fields/field.rb +154 -0
- data/lib/yodel/models/core/fields/fields.rb +29 -0
- data/lib/yodel/models/core/fields/fields_field.rb +31 -0
- data/lib/yodel/models/core/fields/filter_mixin.rb +9 -0
- data/lib/yodel/models/core/fields/filtered_string_field.rb +5 -0
- data/lib/yodel/models/core/fields/filtered_text_field.rb +5 -0
- data/lib/yodel/models/core/fields/function_field.rb +28 -0
- data/lib/yodel/models/core/fields/hash_field.rb +54 -0
- data/lib/yodel/models/core/fields/html_field.rb +15 -0
- data/lib/yodel/models/core/fields/image_field.rb +11 -0
- data/lib/yodel/models/core/fields/integer_field.rb +25 -0
- data/lib/yodel/models/core/fields/password_field.rb +21 -0
- data/lib/yodel/models/core/fields/self_field.rb +27 -0
- data/lib/yodel/models/core/fields/string_field.rb +15 -0
- data/lib/yodel/models/core/fields/tags_field.rb +7 -0
- data/lib/yodel/models/core/fields/text_field.rb +7 -0
- data/lib/yodel/models/core/fields/time_field.rb +36 -0
- data/lib/yodel/models/core/functions/function.rb +471 -0
- data/lib/yodel/models/core/functions/functions.rb +2 -0
- data/lib/yodel/models/core/functions/trigger.rb +14 -0
- data/lib/yodel/models/core/log/log.rb +33 -0
- data/lib/yodel/models/core/log/log_entry.rb +12 -0
- data/lib/yodel/models/core/model/abstract_model.rb +59 -0
- data/lib/yodel/models/core/model/model.rb +460 -0
- data/lib/yodel/models/core/model/mongo_model.rb +25 -0
- data/lib/yodel/models/core/model/site_model.rb +17 -0
- data/lib/yodel/models/core/mongo/mongo.rb +3 -0
- data/lib/yodel/models/core/mongo/primary_key_factory.rb +12 -0
- data/lib/yodel/models/core/mongo/query.rb +68 -0
- data/lib/yodel/models/core/mongo/record_index.rb +89 -0
- data/lib/yodel/models/core/record/abstract_record.rb +411 -0
- data/lib/yodel/models/core/record/embedded_record.rb +47 -0
- data/lib/yodel/models/core/record/mongo_record.rb +83 -0
- data/lib/yodel/models/core/record/record.rb +386 -0
- data/lib/yodel/models/core/record/section.rb +21 -0
- data/lib/yodel/models/core/record/site_record.rb +31 -0
- data/lib/yodel/models/core/site/migration.rb +52 -0
- data/lib/yodel/models/core/site/remote.rb +61 -0
- data/lib/yodel/models/core/site/site.rb +202 -0
- data/lib/yodel/models/core/validations/email_address_validation.rb +24 -0
- data/lib/yodel/models/core/validations/embedded_records_validation.rb +31 -0
- data/lib/yodel/models/core/validations/errors.rb +51 -0
- data/lib/yodel/models/core/validations/excluded_from_validation.rb +10 -0
- data/lib/yodel/models/core/validations/excludes_combinations_validation.rb +18 -0
- data/lib/yodel/models/core/validations/format_validation.rb +10 -0
- data/lib/yodel/models/core/validations/included_in_validation.rb +10 -0
- data/lib/yodel/models/core/validations/includes_combinations_validation.rb +14 -0
- data/lib/yodel/models/core/validations/length_validation.rb +28 -0
- data/lib/yodel/models/core/validations/password_confirmation_validation.rb +11 -0
- data/lib/yodel/models/core/validations/required_validation.rb +9 -0
- data/lib/yodel/models/core/validations/unique_validation.rb +9 -0
- data/lib/yodel/models/core/validations/validation.rb +39 -0
- data/lib/yodel/models/core/validations/validations.rb +15 -0
- data/lib/yodel/models/email/email.rb +79 -0
- data/lib/yodel/models/migrations/01_record_model.rb +29 -0
- data/lib/yodel/models/migrations/02_page_model.rb +45 -0
- data/lib/yodel/models/migrations/03_layout_model.rb +38 -0
- data/lib/yodel/models/migrations/04_group_model.rb +61 -0
- data/lib/yodel/models/migrations/05_user_model.rb +24 -0
- data/lib/yodel/models/migrations/06_snippet_model.rb +13 -0
- data/lib/yodel/models/migrations/07_search_page_model.rb +32 -0
- data/lib/yodel/models/migrations/08_default_site_options.rb +21 -0
- data/lib/yodel/models/migrations/09_security_page_models.rb +36 -0
- data/lib/yodel/models/migrations/10_record_proxy_page_model.rb +17 -0
- data/lib/yodel/models/migrations/11_email_model.rb +28 -0
- data/lib/yodel/models/migrations/12_api_call_model.rb +23 -0
- data/lib/yodel/models/migrations/13_redirect_page_model.rb +13 -0
- data/lib/yodel/models/migrations/14_menu_model.rb +20 -0
- data/lib/yodel/models/models.rb +8 -0
- data/lib/yodel/models/pages/form_builder.rb +379 -0
- data/lib/yodel/models/pages/html_decorator.rb +132 -0
- data/lib/yodel/models/pages/layout.rb +120 -0
- data/lib/yodel/models/pages/menu.rb +32 -0
- data/lib/yodel/models/pages/page.rb +378 -0
- data/lib/yodel/models/pages/pages.rb +7 -0
- data/lib/yodel/models/pages/record_proxy_page.rb +188 -0
- data/lib/yodel/models/pages/redirect_page.rb +11 -0
- data/lib/yodel/models/search/search.rb +1 -0
- data/lib/yodel/models/search/search_page.rb +58 -0
- data/lib/yodel/models/security/facebook_login_page.rb +55 -0
- data/lib/yodel/models/security/group.rb +10 -0
- data/lib/yodel/models/security/guests_group.rb +5 -0
- data/lib/yodel/models/security/login_page.rb +20 -0
- data/lib/yodel/models/security/logout_page.rb +13 -0
- data/lib/yodel/models/security/noone_group.rb +5 -0
- data/lib/yodel/models/security/owner_group.rb +8 -0
- data/lib/yodel/models/security/password.rb +5 -0
- data/lib/yodel/models/security/password_reset_page.rb +47 -0
- data/lib/yodel/models/security/security.rb +10 -0
- data/lib/yodel/models/security/user.rb +33 -0
- data/lib/yodel/public/core/css/core.css +257 -0
- data/lib/yodel/public/core/css/reset.css +48 -0
- data/lib/yodel/public/core/images/cross.png +0 -0
- data/lib/yodel/public/core/images/spinner.gif +0 -0
- data/lib/yodel/public/core/images/tick.png +0 -0
- data/lib/yodel/public/core/images/yodel.png +0 -0
- data/lib/yodel/public/core/js/jquery.min.js +18 -0
- data/lib/yodel/public/core/js/json2.js +480 -0
- data/lib/yodel/public/core/js/yodel_jquery.js +238 -0
- data/lib/yodel/request/authentication.rb +76 -0
- data/lib/yodel/request/flash.rb +28 -0
- data/lib/yodel/request/request.rb +4 -0
- data/lib/yodel/requires.rb +47 -0
- data/lib/yodel/task_queue/queue_daemon.rb +33 -0
- data/lib/yodel/task_queue/queue_worker.rb +32 -0
- data/lib/yodel/task_queue/stats_thread.rb +27 -0
- data/lib/yodel/task_queue/task.rb +62 -0
- data/lib/yodel/task_queue/task_queue.rb +40 -0
- data/lib/yodel/types/date.rb +5 -0
- data/lib/yodel/types/object_id.rb +11 -0
- data/lib/yodel/types/time.rb +5 -0
- data/lib/yodel/version.rb +3 -0
- data/system/Library/LaunchDaemons/com.yodelcms.dns.plist +26 -0
- data/system/Library/LaunchDaemons/com.yodelcms.server.plist +26 -0
- data/system/etc/resolver/yodel +2 -0
- data/system/usr/local/bin/yodel_command_runner +2 -0
- data/system/usr/local/etc/yodel/development_settings.rb +28 -0
- data/system/usr/local/etc/yodel/production_settings.rb +27 -0
- data/system/var/log/yodel.log +0 -0
- data/test/helper.rb +18 -0
- data/test/test_yodel.rb +4 -0
- data/yodel.gemspec +47 -0
- metadata +501 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
require './model/abstract_model'
|
2
|
+
|
3
|
+
module MongoModel
|
4
|
+
include AbstractModel
|
5
|
+
extend Forwardable
|
6
|
+
def_delegators :scoped, :where, :limit, :skip, :sort, :count,
|
7
|
+
:last, :first, :all, :paginate, :find,
|
8
|
+
:find!, :exists?, :exist?, :find_each
|
9
|
+
|
10
|
+
def scoped(scope={})
|
11
|
+
Query.new(self, nil, collection, scope)
|
12
|
+
end
|
13
|
+
|
14
|
+
def load(values)
|
15
|
+
new(values, false)
|
16
|
+
end
|
17
|
+
|
18
|
+
def collection(*name)
|
19
|
+
if name.size == 1
|
20
|
+
@collection = Yodel.db.collection(name.first, pk: PrimaryKeyFactory)
|
21
|
+
else
|
22
|
+
@collection
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require './model/mongo_model'
|
2
|
+
|
3
|
+
module SiteModel
|
4
|
+
include MongoModel
|
5
|
+
|
6
|
+
def scoped_for(site, scope={})
|
7
|
+
scoped(site, self, scope.merge({_site_id: site.id}))
|
8
|
+
end
|
9
|
+
|
10
|
+
def scoped(site, constructor, scope={})
|
11
|
+
Query.new(constructor, site, collection, scope)
|
12
|
+
end
|
13
|
+
|
14
|
+
def load(site, values)
|
15
|
+
new(site, values)
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module PrimaryKeyFactory
|
2
|
+
# The default mongo primary key factory (BSON::ObjectId) creates ids
|
3
|
+
# with symbol keys. Yodel uses string keys (since records are retrieved
|
4
|
+
# with string keys) so Yodel mongo collections use this pk factory instead.
|
5
|
+
def self.create_pk(doc)
|
6
|
+
doc.has_key?('_id') ? doc : doc.merge!('_id' => self.pk)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.pk
|
10
|
+
BSON::ObjectId.new
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
class Query < Plucky::Query
|
2
|
+
attr_reader :constructor, :site
|
3
|
+
|
4
|
+
# construct a default scope for queries on a resource
|
5
|
+
def initialize(constructor, site, collection, scope={})
|
6
|
+
@site = site
|
7
|
+
@constructor = constructor
|
8
|
+
super(collection, scope)
|
9
|
+
end
|
10
|
+
|
11
|
+
def distinct(key)
|
12
|
+
record = collection.distinct(key, criteria.to_hash)
|
13
|
+
end
|
14
|
+
|
15
|
+
# TODO: we only cache queries where _id, _site_id, and model are present; _id on
|
16
|
+
# its own is a strong enough restriction, so why can't we cache all queries with id?
|
17
|
+
# the query may not match (extra restrictions), but for quries that do match, we
|
18
|
+
# should save id-> recrd in cached_records
|
19
|
+
# TODO: cache any new records, so they can be matched by single lookups in the future
|
20
|
+
def find_each(opts={})
|
21
|
+
if @site
|
22
|
+
super.collect do |values|
|
23
|
+
record = @site.cached_records[values['_id']]
|
24
|
+
record ? record : @constructor.load(@site, values)
|
25
|
+
end
|
26
|
+
else
|
27
|
+
super.collect {|values| @constructor.load(values)}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# TODO: extract this out to a collection sub class; yodel collection subclass
|
32
|
+
# will override 'find' itself, so it doesn't need to be done here
|
33
|
+
|
34
|
+
# override find_one to find objects via an identity hash
|
35
|
+
def find_one(opts={})
|
36
|
+
unless @site
|
37
|
+
document = super
|
38
|
+
return nil if document.nil?
|
39
|
+
@constructor.load(document)
|
40
|
+
else
|
41
|
+
query = clone.amend(opts)
|
42
|
+
|
43
|
+
# construct the criteria hash, and remove the keys allowed by a cacheable lookup
|
44
|
+
criteria_hash = query.criteria.to_hash
|
45
|
+
id = criteria_hash[:_id]
|
46
|
+
keys = criteria_hash.keys
|
47
|
+
keys -= [:_id, :_site_id, :model]
|
48
|
+
|
49
|
+
# queries are cacheable if they are looking for a single ID
|
50
|
+
cacheable = !id.nil? && id.is_a?(BSON::ObjectId) && keys.empty?
|
51
|
+
|
52
|
+
# lookup the record in the cache
|
53
|
+
if cacheable
|
54
|
+
record = @site.cached_records[id]
|
55
|
+
return record unless record.nil?
|
56
|
+
end
|
57
|
+
|
58
|
+
# lookup failed, so perform a query
|
59
|
+
record = query.collection.find_one(criteria_hash, query.options.to_hash)
|
60
|
+
if record
|
61
|
+
record = @constructor.load(@site, record)
|
62
|
+
@site.cached_records[id] = record if cacheable
|
63
|
+
end
|
64
|
+
|
65
|
+
record
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require './record/mongo_record'
|
2
|
+
require './record/record'
|
3
|
+
require './model/mongo_model'
|
4
|
+
|
5
|
+
class RecordIndex < MongoRecord
|
6
|
+
extend MongoModel
|
7
|
+
collection :record_indexes
|
8
|
+
field :spec, :array, of: :strings, required: true
|
9
|
+
field :references, :array, of: :strings, required: true
|
10
|
+
field :name, :string, required: true
|
11
|
+
|
12
|
+
# ----------------------------------------
|
13
|
+
# Index creation is handled by RecordIndex
|
14
|
+
# ----------------------------------------
|
15
|
+
def self.add_index(model_reference, spec)
|
16
|
+
name = index_name(spec)
|
17
|
+
index = self.scoped.where(name: name).first
|
18
|
+
|
19
|
+
if index.nil?
|
20
|
+
index = new
|
21
|
+
index.spec = spec
|
22
|
+
index.name = name
|
23
|
+
end
|
24
|
+
|
25
|
+
index.references << model_reference
|
26
|
+
index.save
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.remove_index(model_reference)
|
30
|
+
index = self.scoped.where(references: model_reference).first
|
31
|
+
return false if index.nil?
|
32
|
+
index.references.delete(model_reference)
|
33
|
+
|
34
|
+
if index.references.empty?
|
35
|
+
index.destroy
|
36
|
+
else
|
37
|
+
index.save
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
# ----------------------------------------
|
43
|
+
# Helper methods
|
44
|
+
# ----------------------------------------
|
45
|
+
def self.add_index_for_field(model, field)
|
46
|
+
name = model_index_name(model, field.name)
|
47
|
+
spec = [[field.name, Mongo::ASCENDING]]
|
48
|
+
add_index(name, spec)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.add_index_for_model(model, name, spec)
|
52
|
+
name = model_index_name(model, name)
|
53
|
+
add_index(name, spec)
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.remove_index_for_field(model, field)
|
57
|
+
name = model_index_name(model, field.name)
|
58
|
+
remove_index(name)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.remove_index_for_model(model, name)
|
62
|
+
name = model_index_name(model, name)
|
63
|
+
remove_index(name)
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
# ----------------------------------------
|
68
|
+
# Actual index construction/deletion
|
69
|
+
# ----------------------------------------
|
70
|
+
after_destroy :remove_index
|
71
|
+
def remove_index
|
72
|
+
Record.collection.drop_index(name)
|
73
|
+
end
|
74
|
+
|
75
|
+
before_create :create_index
|
76
|
+
def create_index
|
77
|
+
Record.collection.create_index(spec, name: name, background: true)
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
private
|
82
|
+
def self.model_index_name(model, name)
|
83
|
+
"#{model.site.id.to_s}_#{model.name}_#{name}"
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.index_name(spec)
|
87
|
+
spec.collect {|field| field.collect(&:to_s).join('_')}.join('_')
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,411 @@
|
|
1
|
+
# Record objects must implement these methods:
|
2
|
+
# fields
|
3
|
+
# perform_save
|
4
|
+
# perform_destroy
|
5
|
+
# perform_reload
|
6
|
+
|
7
|
+
class AbstractRecord
|
8
|
+
attr_reader :values, :typecast, :changed, :errors, :stash
|
9
|
+
|
10
|
+
def initialize(values={}, new_record=true)
|
11
|
+
@values = default_values.merge(values.stringify_keys) # FIXME: don't merge here; default || values
|
12
|
+
@typecast = {} # typecast versions of original document values
|
13
|
+
@changed = {} # typecast versions of changed values
|
14
|
+
@stash = {} # values of unknown fields set by from_json
|
15
|
+
@errors = Errors.new
|
16
|
+
@new = new_record
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
# ----------------------------------------
|
21
|
+
# Equality
|
22
|
+
# ----------------------------------------
|
23
|
+
def eql?(other)
|
24
|
+
other.respond_to?(:id) && other.id == self.id &&
|
25
|
+
other.is_a?(AbstractRecord)
|
26
|
+
end
|
27
|
+
|
28
|
+
alias :== :eql?
|
29
|
+
|
30
|
+
def hash
|
31
|
+
id.hash
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
# ----------------------------------------
|
36
|
+
# Modelling
|
37
|
+
# ----------------------------------------
|
38
|
+
def fields
|
39
|
+
{}
|
40
|
+
end
|
41
|
+
|
42
|
+
def field(name)
|
43
|
+
fields[name]
|
44
|
+
end
|
45
|
+
|
46
|
+
def field?(name)
|
47
|
+
fields.key?(name)
|
48
|
+
end
|
49
|
+
|
50
|
+
def default_values
|
51
|
+
fields.each_with_object({}) do |(name, field), defaults|
|
52
|
+
default_value = field.default
|
53
|
+
unless default_value.nil? && field.strip_nil?
|
54
|
+
defaults[name] = default_value
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
# ----------------------------------------
|
61
|
+
# Representations
|
62
|
+
# ----------------------------------------
|
63
|
+
def inspect_hash
|
64
|
+
fields.each_with_object({}) do |(name, field), hash|
|
65
|
+
hash[name] = get(name)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def inspect
|
70
|
+
values = inspect_hash.collect do |name, value|
|
71
|
+
if value.is_a?(Array) || value.is_a?(ChangeSensitiveArray)
|
72
|
+
value = "[#{value.collect {|element| inspect_value(element)}.join(', ')}]"
|
73
|
+
elsif value.is_a?(Hash) || value.is_a?(ChangeSensitiveHash)
|
74
|
+
value = "{#{value.to_hash.collect {|key, value| "#{key.to_s}: #{inspect_value(value)}"}.join(', ')}}"
|
75
|
+
else
|
76
|
+
value = inspect_value(value)
|
77
|
+
end
|
78
|
+
"#{name}: #{value}"
|
79
|
+
end
|
80
|
+
"#<#{self.class.name} #{values.join(', ')}>"
|
81
|
+
end
|
82
|
+
|
83
|
+
def inspect_value(value)
|
84
|
+
value.respond_to?(:to_str) && !value.is_a?(String) ? value.to_str : value.inspect.to_s
|
85
|
+
end
|
86
|
+
|
87
|
+
def to_str
|
88
|
+
"#<#{self.class.name}: #{id}>"
|
89
|
+
end
|
90
|
+
|
91
|
+
alias :to_s :to_str
|
92
|
+
|
93
|
+
def to_json(*a)
|
94
|
+
@values.to_json(*a)
|
95
|
+
end
|
96
|
+
|
97
|
+
def from_json(values)
|
98
|
+
values.each do |name, value|
|
99
|
+
if field?(name)
|
100
|
+
current_field = field(name)
|
101
|
+
raise MassAssignment, "Cannot mass assign #{field}" if current_field.protected?
|
102
|
+
else
|
103
|
+
@stash[name] = value
|
104
|
+
next
|
105
|
+
end
|
106
|
+
|
107
|
+
# action hashes allow operations on fields such as append, increment
|
108
|
+
if value.is_a?(Hash) && value.key?('_action')
|
109
|
+
current_field.json_action(value.delete('_action'), value.delete('_value'), self)
|
110
|
+
else
|
111
|
+
catch :ignore_value do
|
112
|
+
processed_value = current_field.from_json(value, self)
|
113
|
+
set(name, processed_value)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
save
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
# ----------------------------------------
|
123
|
+
# Accessors
|
124
|
+
# ----------------------------------------
|
125
|
+
def id
|
126
|
+
object_id
|
127
|
+
end
|
128
|
+
|
129
|
+
def clear_key(name)
|
130
|
+
@changed.delete(name)
|
131
|
+
@typecast.delete(name)
|
132
|
+
end
|
133
|
+
|
134
|
+
def get(name)
|
135
|
+
ensure_field_is_valid(name)
|
136
|
+
return @changed[name] if @changed.key?(name)
|
137
|
+
return @typecast[name] if @typecast.key?(name)
|
138
|
+
typecast_value(name)
|
139
|
+
end
|
140
|
+
|
141
|
+
def get_raw(name)
|
142
|
+
ensure_field_is_valid(name)
|
143
|
+
@values[name]
|
144
|
+
end
|
145
|
+
|
146
|
+
def get_meta(name)
|
147
|
+
@values[name]
|
148
|
+
end
|
149
|
+
|
150
|
+
def set(name, value)
|
151
|
+
ensure_field_is_valid(name)
|
152
|
+
@changed[name] = value
|
153
|
+
end
|
154
|
+
|
155
|
+
def set_raw(name, value)
|
156
|
+
ensure_field_is_valid(name)
|
157
|
+
@values[name] = value
|
158
|
+
@changed.delete(name)
|
159
|
+
@typecast.delete(name)
|
160
|
+
end
|
161
|
+
|
162
|
+
def set_meta(name, value)
|
163
|
+
@values[name] = value
|
164
|
+
end
|
165
|
+
|
166
|
+
def present?(name)
|
167
|
+
# FIXME: this doesn't work for many/one store: false
|
168
|
+
ensure_field_is_valid(name)
|
169
|
+
!get(name).blank?
|
170
|
+
end
|
171
|
+
|
172
|
+
def changed?(name)
|
173
|
+
ensure_field_is_valid(name)
|
174
|
+
@changed.key?(name)
|
175
|
+
end
|
176
|
+
|
177
|
+
def changed!(name)
|
178
|
+
ensure_field_is_valid(name)
|
179
|
+
return if @changed.key?(name)
|
180
|
+
@changed[name] = get(name).dup
|
181
|
+
end
|
182
|
+
|
183
|
+
def field_was(name)
|
184
|
+
ensure_field_is_valid(name)
|
185
|
+
if @typecast.key?(name)
|
186
|
+
@typecast[name]
|
187
|
+
else
|
188
|
+
typecast_value(name)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def increment!(name, value=1)
|
193
|
+
ensure_field_is_valid(name)
|
194
|
+
current = get(name)
|
195
|
+
set(name, current + value)
|
196
|
+
save_without_validation
|
197
|
+
end
|
198
|
+
|
199
|
+
def method_missing(name, *args, &block)
|
200
|
+
# Catch a "fun" ruby 1.9 implemention detail. Calls to flatten blindly call
|
201
|
+
# to_ary on items in an array rather than checking it they really support
|
202
|
+
# the method with respond_to? Catch, and raise the expected exception.
|
203
|
+
raise NoMethodError if name == :to_ary
|
204
|
+
field_name = name.to_s
|
205
|
+
|
206
|
+
if field_name.end_with?('_changed?')
|
207
|
+
changed?(field_name[0...-9])
|
208
|
+
elsif field_name.end_with?('=')
|
209
|
+
set(field_name[0...-1], args.first)
|
210
|
+
elsif field_name.end_with?('?')
|
211
|
+
present?(field_name[0...-1])
|
212
|
+
elsif field_name.end_with?('_was')
|
213
|
+
field_was(field_name[0...-4])
|
214
|
+
else
|
215
|
+
get(field_name)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
|
220
|
+
# ----------------------------------------
|
221
|
+
# Persistence
|
222
|
+
# ----------------------------------------
|
223
|
+
def new?
|
224
|
+
!!@new
|
225
|
+
end
|
226
|
+
|
227
|
+
def destroyed?
|
228
|
+
!!@destroyed
|
229
|
+
end
|
230
|
+
|
231
|
+
def save
|
232
|
+
valid? ? save_without_validation : false
|
233
|
+
end
|
234
|
+
|
235
|
+
def save_without_validation
|
236
|
+
raise DestroyedRecord if destroyed?
|
237
|
+
callback = "run_#{new? ? 'create' : 'update'}_callbacks"
|
238
|
+
succeeded = false
|
239
|
+
|
240
|
+
run_save_callbacks do
|
241
|
+
send(callback) do
|
242
|
+
|
243
|
+
# untypecast all changed values to construct an up to date values hash
|
244
|
+
changed.each do |name, value|
|
245
|
+
changed_field = field(name)
|
246
|
+
untypecast_value = changed_field.untypecast(value, self)
|
247
|
+
if untypecast_value.nil? && changed_field.strip_nil?
|
248
|
+
values.delete(name)
|
249
|
+
else
|
250
|
+
values[name] = untypecast_value
|
251
|
+
end
|
252
|
+
typecast[name] = value
|
253
|
+
end
|
254
|
+
succeeded = perform_save
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
if succeeded
|
259
|
+
@new = false
|
260
|
+
@changed.clear
|
261
|
+
@stash.clear
|
262
|
+
end
|
263
|
+
succeeded
|
264
|
+
end
|
265
|
+
|
266
|
+
def destroy
|
267
|
+
return if new? || destroyed?
|
268
|
+
succeeded = false
|
269
|
+
run_destroy_callbacks do
|
270
|
+
succeeded = perform_destroy
|
271
|
+
end
|
272
|
+
@destroyed = succeeded
|
273
|
+
end
|
274
|
+
|
275
|
+
def update(values, do_save=true)
|
276
|
+
raise DestroyedRecord if destroyed?
|
277
|
+
values.stringify_keys!
|
278
|
+
values.each do |name, value|
|
279
|
+
ensure_field_is_valid(name)
|
280
|
+
if field(name).protected?
|
281
|
+
raise MassAssignment, "Cannot mass assign #{field}"
|
282
|
+
else
|
283
|
+
set(name, value)
|
284
|
+
end
|
285
|
+
end
|
286
|
+
save if do_save
|
287
|
+
end
|
288
|
+
|
289
|
+
def reload
|
290
|
+
return if new? || destroyed?
|
291
|
+
reload_params = prepare_reload_params
|
292
|
+
|
293
|
+
# remove all instance variables and re-initialise
|
294
|
+
instance_variables.each {|var| remove_instance_variable(var)}
|
295
|
+
perform_reload(reload_params)
|
296
|
+
end
|
297
|
+
|
298
|
+
def prepare_reload_params
|
299
|
+
{id: id}
|
300
|
+
end
|
301
|
+
|
302
|
+
|
303
|
+
# ----------------------------------------
|
304
|
+
# Callbacks & Validation
|
305
|
+
# ----------------------------------------
|
306
|
+
CALLBACKS = %w{save create update destroy validation}
|
307
|
+
ORDERS = %w{before after}
|
308
|
+
CALLBACKS.each do |callback|
|
309
|
+
ORDERS.each do |order|
|
310
|
+
eval "
|
311
|
+
@_#{order}_#{callback}_callbacks = []
|
312
|
+
|
313
|
+
def self._#{order}_#{callback}_callbacks
|
314
|
+
@_#{order}_#{callback}_callbacks
|
315
|
+
end
|
316
|
+
|
317
|
+
def self.#{order}_#{callback}(*callbacks)
|
318
|
+
@_#{order}_#{callback}_callbacks += callbacks
|
319
|
+
end
|
320
|
+
|
321
|
+
def run_#{order}_#{callback}_callbacks
|
322
|
+
self.class._#{order}_#{callback}_callbacks.each {|method| send method}
|
323
|
+
end
|
324
|
+
"
|
325
|
+
end
|
326
|
+
|
327
|
+
eval "
|
328
|
+
def run_#{callback}_callbacks(&block)
|
329
|
+
run_before_#{callback}_callbacks
|
330
|
+
yield if block_given?
|
331
|
+
run_after_#{callback}_callbacks
|
332
|
+
end
|
333
|
+
"
|
334
|
+
end
|
335
|
+
|
336
|
+
def self.inherited(child)
|
337
|
+
super(child)
|
338
|
+
CALLBACKS.each do |callback|
|
339
|
+
ORDERS.each do |order|
|
340
|
+
callbacks = instance_variable_get("@_#{order}_#{callback}_callbacks")
|
341
|
+
child.instance_variable_set("@_#{order}_#{callback}_callbacks", callbacks)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
def valid?
|
347
|
+
# validate all fields for new records; we know saved records should be
|
348
|
+
# valid so we can limit testing to the set of changed fields only
|
349
|
+
run_validation_callbacks do
|
350
|
+
@errors.clear
|
351
|
+
unless new?
|
352
|
+
@changed.each {|name, value| field(name).validate(self, @errors)}
|
353
|
+
else
|
354
|
+
fields.each {|name, field| field.validate(self, @errors)}
|
355
|
+
end
|
356
|
+
end
|
357
|
+
@errors.empty?
|
358
|
+
end
|
359
|
+
|
360
|
+
def errors?
|
361
|
+
!@errors.blank?
|
362
|
+
end
|
363
|
+
|
364
|
+
# Field callbacks
|
365
|
+
FIELD_CALLBACKS = %w{save create update destroy}
|
366
|
+
FIELD_CALLBACKS.each do |callback|
|
367
|
+
ORDERS.each do |order|
|
368
|
+
eval "
|
369
|
+
#{order}_#{callback} :trigger_field_#{order}_#{callback}_callbacks
|
370
|
+
def trigger_field_#{order}_#{callback}_callbacks
|
371
|
+
trigger_field_callback(:#{order}, :#{callback})
|
372
|
+
end
|
373
|
+
"
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
def trigger_field_callback(order, action)
|
378
|
+
method = "#{order}_#{action}"
|
379
|
+
fields.each do |name, field|
|
380
|
+
field.send(method, self) if field.respond_to?(method)
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
|
385
|
+
# ----------------------------------------
|
386
|
+
# Search
|
387
|
+
# ----------------------------------------
|
388
|
+
def search_terms
|
389
|
+
search_terms = Set.new
|
390
|
+
|
391
|
+
fields.each do |name, field|
|
392
|
+
# TODO: we should cache somewhere which types do and do not contain the search_terms_set
|
393
|
+
# method; this can also be used to automatically populate the searchable option on fields
|
394
|
+
next unless field.searchable? && field.respond_to?(:search_terms_set)
|
395
|
+
search_terms.merge(field.search_terms_set(self).collect(&:downcase))
|
396
|
+
end
|
397
|
+
|
398
|
+
search_terms.to_a
|
399
|
+
end
|
400
|
+
|
401
|
+
|
402
|
+
private
|
403
|
+
def ensure_field_is_valid(name)
|
404
|
+
raise UnknownField, "Unknown field <#{name}>" unless field?(name)
|
405
|
+
end
|
406
|
+
|
407
|
+
def typecast_value(name)
|
408
|
+
value = field(name).typecast(@values[name], self)
|
409
|
+
@typecast[name] = value
|
410
|
+
end
|
411
|
+
end
|