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.
Files changed (200) hide show
  1. data/.document +5 -0
  2. data/.gitignore +9 -0
  3. data/Gemfile +2 -0
  4. data/Gemfile.lock +63 -0
  5. data/LICENSE +1 -0
  6. data/README.rdoc +20 -0
  7. data/Rakefile +1 -0
  8. data/bin/yodel +4 -0
  9. data/lib/yodel.rb +22 -0
  10. data/lib/yodel/application/application.rb +44 -0
  11. data/lib/yodel/application/extension.rb +59 -0
  12. data/lib/yodel/application/request_handler.rb +48 -0
  13. data/lib/yodel/application/yodel.rb +25 -0
  14. data/lib/yodel/command/command.rb +94 -0
  15. data/lib/yodel/command/deploy.rb +67 -0
  16. data/lib/yodel/command/dns_server.rb +16 -0
  17. data/lib/yodel/command/installer.rb +229 -0
  18. data/lib/yodel/config/config.rb +30 -0
  19. data/lib/yodel/config/environment.rb +16 -0
  20. data/lib/yodel/config/yodel.rb +21 -0
  21. data/lib/yodel/exceptions/destroyed_record.rb +2 -0
  22. data/lib/yodel/exceptions/domain_not_found.rb +16 -0
  23. data/lib/yodel/exceptions/duplicate_layout.rb +2 -0
  24. data/lib/yodel/exceptions/exceptions.rb +3 -0
  25. data/lib/yodel/exceptions/inconsistent_lock_state.rb +2 -0
  26. data/lib/yodel/exceptions/invalid_field.rb +2 -0
  27. data/lib/yodel/exceptions/invalid_index.rb +2 -0
  28. data/lib/yodel/exceptions/invalid_mixin.rb +2 -0
  29. data/lib/yodel/exceptions/invalid_model_field.rb +2 -0
  30. data/lib/yodel/exceptions/layout_not_found.rb +2 -0
  31. data/lib/yodel/exceptions/mass_assignment.rb +2 -0
  32. data/lib/yodel/exceptions/missing_migration.rb +2 -0
  33. data/lib/yodel/exceptions/missing_root_directory.rb +15 -0
  34. data/lib/yodel/exceptions/unable_to_acquire_lock.rb +2 -0
  35. data/lib/yodel/exceptions/unauthorised.rb +2 -0
  36. data/lib/yodel/exceptions/unknown_field.rb +2 -0
  37. data/lib/yodel/middleware/development_server.rb +180 -0
  38. data/lib/yodel/middleware/error_pages.rb +72 -0
  39. data/lib/yodel/middleware/public_assets.rb +78 -0
  40. data/lib/yodel/middleware/request.rb +16 -0
  41. data/lib/yodel/middleware/site_detector.rb +22 -0
  42. data/lib/yodel/mime_types/default_mime_set.rb +28 -0
  43. data/lib/yodel/mime_types/mime_type.rb +68 -0
  44. data/lib/yodel/mime_types/mime_type_set.rb +41 -0
  45. data/lib/yodel/mime_types/mime_types.rb +6 -0
  46. data/lib/yodel/mime_types/yodel.rb +15 -0
  47. data/lib/yodel/models/api/api.rb +1 -0
  48. data/lib/yodel/models/api/api_call.rb +87 -0
  49. data/lib/yodel/models/core/associations/association.rb +37 -0
  50. data/lib/yodel/models/core/associations/associations.rb +22 -0
  51. data/lib/yodel/models/core/associations/counts/many_association.rb +18 -0
  52. data/lib/yodel/models/core/associations/counts/one_association.rb +22 -0
  53. data/lib/yodel/models/core/associations/embedded/embedded_association.rb +47 -0
  54. data/lib/yodel/models/core/associations/embedded/embedded_record_array.rb +12 -0
  55. data/lib/yodel/models/core/associations/embedded/many_embedded_association.rb +62 -0
  56. data/lib/yodel/models/core/associations/embedded/one_embedded_association.rb +49 -0
  57. data/lib/yodel/models/core/associations/query/many_query_association.rb +10 -0
  58. data/lib/yodel/models/core/associations/query/one_query_association.rb +10 -0
  59. data/lib/yodel/models/core/associations/query/query_association.rb +64 -0
  60. data/lib/yodel/models/core/associations/record_association.rb +38 -0
  61. data/lib/yodel/models/core/associations/store/many_store_association.rb +32 -0
  62. data/lib/yodel/models/core/associations/store/one_store_association.rb +14 -0
  63. data/lib/yodel/models/core/associations/store/store_association.rb +51 -0
  64. data/lib/yodel/models/core/attachments/attachment.rb +73 -0
  65. data/lib/yodel/models/core/attachments/image.rb +38 -0
  66. data/lib/yodel/models/core/core.rb +15 -0
  67. data/lib/yodel/models/core/fields/alias_field.rb +32 -0
  68. data/lib/yodel/models/core/fields/array_field.rb +64 -0
  69. data/lib/yodel/models/core/fields/attachment_field.rb +42 -0
  70. data/lib/yodel/models/core/fields/boolean_field.rb +28 -0
  71. data/lib/yodel/models/core/fields/change_sensitive_array.rb +96 -0
  72. data/lib/yodel/models/core/fields/change_sensitive_hash.rb +53 -0
  73. data/lib/yodel/models/core/fields/color_field.rb +4 -0
  74. data/lib/yodel/models/core/fields/date_field.rb +35 -0
  75. data/lib/yodel/models/core/fields/decimal_field.rb +19 -0
  76. data/lib/yodel/models/core/fields/email_field.rb +10 -0
  77. data/lib/yodel/models/core/fields/enum_field.rb +33 -0
  78. data/lib/yodel/models/core/fields/field.rb +154 -0
  79. data/lib/yodel/models/core/fields/fields.rb +29 -0
  80. data/lib/yodel/models/core/fields/fields_field.rb +31 -0
  81. data/lib/yodel/models/core/fields/filter_mixin.rb +9 -0
  82. data/lib/yodel/models/core/fields/filtered_string_field.rb +5 -0
  83. data/lib/yodel/models/core/fields/filtered_text_field.rb +5 -0
  84. data/lib/yodel/models/core/fields/function_field.rb +28 -0
  85. data/lib/yodel/models/core/fields/hash_field.rb +54 -0
  86. data/lib/yodel/models/core/fields/html_field.rb +15 -0
  87. data/lib/yodel/models/core/fields/image_field.rb +11 -0
  88. data/lib/yodel/models/core/fields/integer_field.rb +25 -0
  89. data/lib/yodel/models/core/fields/password_field.rb +21 -0
  90. data/lib/yodel/models/core/fields/self_field.rb +27 -0
  91. data/lib/yodel/models/core/fields/string_field.rb +15 -0
  92. data/lib/yodel/models/core/fields/tags_field.rb +7 -0
  93. data/lib/yodel/models/core/fields/text_field.rb +7 -0
  94. data/lib/yodel/models/core/fields/time_field.rb +36 -0
  95. data/lib/yodel/models/core/functions/function.rb +471 -0
  96. data/lib/yodel/models/core/functions/functions.rb +2 -0
  97. data/lib/yodel/models/core/functions/trigger.rb +14 -0
  98. data/lib/yodel/models/core/log/log.rb +33 -0
  99. data/lib/yodel/models/core/log/log_entry.rb +12 -0
  100. data/lib/yodel/models/core/model/abstract_model.rb +59 -0
  101. data/lib/yodel/models/core/model/model.rb +460 -0
  102. data/lib/yodel/models/core/model/mongo_model.rb +25 -0
  103. data/lib/yodel/models/core/model/site_model.rb +17 -0
  104. data/lib/yodel/models/core/mongo/mongo.rb +3 -0
  105. data/lib/yodel/models/core/mongo/primary_key_factory.rb +12 -0
  106. data/lib/yodel/models/core/mongo/query.rb +68 -0
  107. data/lib/yodel/models/core/mongo/record_index.rb +89 -0
  108. data/lib/yodel/models/core/record/abstract_record.rb +411 -0
  109. data/lib/yodel/models/core/record/embedded_record.rb +47 -0
  110. data/lib/yodel/models/core/record/mongo_record.rb +83 -0
  111. data/lib/yodel/models/core/record/record.rb +386 -0
  112. data/lib/yodel/models/core/record/section.rb +21 -0
  113. data/lib/yodel/models/core/record/site_record.rb +31 -0
  114. data/lib/yodel/models/core/site/migration.rb +52 -0
  115. data/lib/yodel/models/core/site/remote.rb +61 -0
  116. data/lib/yodel/models/core/site/site.rb +202 -0
  117. data/lib/yodel/models/core/validations/email_address_validation.rb +24 -0
  118. data/lib/yodel/models/core/validations/embedded_records_validation.rb +31 -0
  119. data/lib/yodel/models/core/validations/errors.rb +51 -0
  120. data/lib/yodel/models/core/validations/excluded_from_validation.rb +10 -0
  121. data/lib/yodel/models/core/validations/excludes_combinations_validation.rb +18 -0
  122. data/lib/yodel/models/core/validations/format_validation.rb +10 -0
  123. data/lib/yodel/models/core/validations/included_in_validation.rb +10 -0
  124. data/lib/yodel/models/core/validations/includes_combinations_validation.rb +14 -0
  125. data/lib/yodel/models/core/validations/length_validation.rb +28 -0
  126. data/lib/yodel/models/core/validations/password_confirmation_validation.rb +11 -0
  127. data/lib/yodel/models/core/validations/required_validation.rb +9 -0
  128. data/lib/yodel/models/core/validations/unique_validation.rb +9 -0
  129. data/lib/yodel/models/core/validations/validation.rb +39 -0
  130. data/lib/yodel/models/core/validations/validations.rb +15 -0
  131. data/lib/yodel/models/email/email.rb +79 -0
  132. data/lib/yodel/models/migrations/01_record_model.rb +29 -0
  133. data/lib/yodel/models/migrations/02_page_model.rb +45 -0
  134. data/lib/yodel/models/migrations/03_layout_model.rb +38 -0
  135. data/lib/yodel/models/migrations/04_group_model.rb +61 -0
  136. data/lib/yodel/models/migrations/05_user_model.rb +24 -0
  137. data/lib/yodel/models/migrations/06_snippet_model.rb +13 -0
  138. data/lib/yodel/models/migrations/07_search_page_model.rb +32 -0
  139. data/lib/yodel/models/migrations/08_default_site_options.rb +21 -0
  140. data/lib/yodel/models/migrations/09_security_page_models.rb +36 -0
  141. data/lib/yodel/models/migrations/10_record_proxy_page_model.rb +17 -0
  142. data/lib/yodel/models/migrations/11_email_model.rb +28 -0
  143. data/lib/yodel/models/migrations/12_api_call_model.rb +23 -0
  144. data/lib/yodel/models/migrations/13_redirect_page_model.rb +13 -0
  145. data/lib/yodel/models/migrations/14_menu_model.rb +20 -0
  146. data/lib/yodel/models/models.rb +8 -0
  147. data/lib/yodel/models/pages/form_builder.rb +379 -0
  148. data/lib/yodel/models/pages/html_decorator.rb +132 -0
  149. data/lib/yodel/models/pages/layout.rb +120 -0
  150. data/lib/yodel/models/pages/menu.rb +32 -0
  151. data/lib/yodel/models/pages/page.rb +378 -0
  152. data/lib/yodel/models/pages/pages.rb +7 -0
  153. data/lib/yodel/models/pages/record_proxy_page.rb +188 -0
  154. data/lib/yodel/models/pages/redirect_page.rb +11 -0
  155. data/lib/yodel/models/search/search.rb +1 -0
  156. data/lib/yodel/models/search/search_page.rb +58 -0
  157. data/lib/yodel/models/security/facebook_login_page.rb +55 -0
  158. data/lib/yodel/models/security/group.rb +10 -0
  159. data/lib/yodel/models/security/guests_group.rb +5 -0
  160. data/lib/yodel/models/security/login_page.rb +20 -0
  161. data/lib/yodel/models/security/logout_page.rb +13 -0
  162. data/lib/yodel/models/security/noone_group.rb +5 -0
  163. data/lib/yodel/models/security/owner_group.rb +8 -0
  164. data/lib/yodel/models/security/password.rb +5 -0
  165. data/lib/yodel/models/security/password_reset_page.rb +47 -0
  166. data/lib/yodel/models/security/security.rb +10 -0
  167. data/lib/yodel/models/security/user.rb +33 -0
  168. data/lib/yodel/public/core/css/core.css +257 -0
  169. data/lib/yodel/public/core/css/reset.css +48 -0
  170. data/lib/yodel/public/core/images/cross.png +0 -0
  171. data/lib/yodel/public/core/images/spinner.gif +0 -0
  172. data/lib/yodel/public/core/images/tick.png +0 -0
  173. data/lib/yodel/public/core/images/yodel.png +0 -0
  174. data/lib/yodel/public/core/js/jquery.min.js +18 -0
  175. data/lib/yodel/public/core/js/json2.js +480 -0
  176. data/lib/yodel/public/core/js/yodel_jquery.js +238 -0
  177. data/lib/yodel/request/authentication.rb +76 -0
  178. data/lib/yodel/request/flash.rb +28 -0
  179. data/lib/yodel/request/request.rb +4 -0
  180. data/lib/yodel/requires.rb +47 -0
  181. data/lib/yodel/task_queue/queue_daemon.rb +33 -0
  182. data/lib/yodel/task_queue/queue_worker.rb +32 -0
  183. data/lib/yodel/task_queue/stats_thread.rb +27 -0
  184. data/lib/yodel/task_queue/task.rb +62 -0
  185. data/lib/yodel/task_queue/task_queue.rb +40 -0
  186. data/lib/yodel/types/date.rb +5 -0
  187. data/lib/yodel/types/object_id.rb +11 -0
  188. data/lib/yodel/types/time.rb +5 -0
  189. data/lib/yodel/version.rb +3 -0
  190. data/system/Library/LaunchDaemons/com.yodelcms.dns.plist +26 -0
  191. data/system/Library/LaunchDaemons/com.yodelcms.server.plist +26 -0
  192. data/system/etc/resolver/yodel +2 -0
  193. data/system/usr/local/bin/yodel_command_runner +2 -0
  194. data/system/usr/local/etc/yodel/development_settings.rb +28 -0
  195. data/system/usr/local/etc/yodel/production_settings.rb +27 -0
  196. data/system/var/log/yodel.log +0 -0
  197. data/test/helper.rb +18 -0
  198. data/test/test_yodel.rb +4 -0
  199. data/yodel.gemspec +47 -0
  200. 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