yodel 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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