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,2 @@
1
+ require './functions/function'
2
+ require './functions/trigger'
@@ -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,12 @@
1
+ class LogEntry < SiteRecord
2
+ DEBUG = 0
3
+ INFO = 1
4
+ WARN = 2
5
+ ERROR = 3
6
+ FATAL = 4
7
+
8
+ collection :log
9
+ field :severity, :integer, default: INFO
10
+ field :created_at, :time
11
+ field :message, :string
12
+ 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