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,21 @@
1
+ class Section < Array
2
+ attr_reader :name
3
+
4
+ def initialize(name)
5
+ @name = name
6
+ super()
7
+ end
8
+
9
+ def display?
10
+ any? {|field| display_field?(field)}
11
+ end
12
+
13
+ def displayed_fields
14
+ select {|field| display_field?(field)}
15
+ end
16
+
17
+ private
18
+ def display_field?(field)
19
+ field.display? && field.default_input_type.present? && field.default_input_type != :embedded
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ require './record/mongo_record'
2
+ require './model/site_model'
3
+
4
+ class SiteRecord < MongoRecord
5
+ extend SiteModel
6
+ attr_reader :site
7
+
8
+ def initialize(site, values={}, new_record=true)
9
+ @site = site
10
+ super(values, new_record)
11
+ end
12
+
13
+ def site_id; @values['_site_id']; end
14
+
15
+ def default_values
16
+ super.merge({'_site_id' => site.id})
17
+ end
18
+
19
+ def inspect_hash
20
+ {site_id: site_id}.merge(super)
21
+ end
22
+
23
+ def perform_reload(params)
24
+ document = load_mongo_document(_id: params[:id])
25
+ initialize(params[:site], document)
26
+ end
27
+
28
+ def prepare_reload_params
29
+ super.tap {|vals| vals[:site] = site}
30
+ end
31
+ end
@@ -0,0 +1,52 @@
1
+ class Migration
2
+ def self.run_migrations(site)
3
+ Yodel.config.logger.info "Migrating #{site.name}"
4
+
5
+ each_migration_for(site) do |migration, file|
6
+ unless migration.nil?
7
+ next if site.migrations.include?(migration.name)
8
+ migration.up(site)
9
+
10
+ # newly created models are incomplete; reload the site
11
+ # to force complete versions to be generated for use
12
+ site.migrations << migration.name
13
+ site.save
14
+ site.reload
15
+
16
+ Yodel.config.logger.info "Migrated #{migration.name}"
17
+ else
18
+ raise MissingMigration.new(file)
19
+ end
20
+ end
21
+
22
+ Yodel.config.logger.info "Migrations for #{site.name} complete"
23
+ end
24
+
25
+ # As migration files are require'd this method will be triggered so
26
+ # we have a reference to the 'current' migration class being run
27
+ def self.inherited(child)
28
+ @migration = child
29
+ end
30
+
31
+
32
+ private
33
+ def self.each_migration_for(site, &block)
34
+ each_migration(File.join(site.migrations_directory, Yodel::YODEL_MIGRATIONS_DIRECTORY_NAME), &block)
35
+ each_migration(File.join(site.migrations_directory, Yodel::EXTENSION_MIGRATIONS_DIRECTORY_NAME), &block)
36
+ each_migration(File.join(site.migrations_directory, Yodel::SITE_MIGRATIONS_DIRECTORY_NAME), &block)
37
+ end
38
+
39
+ # Iterate over every migration and yield the migration class
40
+ # to the supplied block. Incorrect migration files may result
41
+ # in nil being yielded. The caller can respond appropriately.
42
+ # The current file (a string path) is also provided.
43
+ def self.each_migration(directory)
44
+ return unless File.directory?(directory)
45
+ Dir[File.join(directory, '**/*.rb')].sort.each do |file|
46
+ @migration = nil
47
+ load file
48
+ yield @migration, file
49
+ Object.send(:remove_const, @migration.name.to_sym) if @migration
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,61 @@
1
+ class Remote < MongoRecord
2
+ collection :remotes
3
+ field :name, :string, validations: {required: {}}
4
+ field :url, :string, validations: {required: {}}
5
+ field :username, :string, validations: {required: {}}
6
+ field :password, :password, validations: {required: {}}
7
+ many :sites, store: false
8
+
9
+ def site_list
10
+ perform_request('/sites.json', :get)
11
+ end
12
+
13
+ def create_site
14
+ perform_request('/sites.json', :post)
15
+ end
16
+
17
+ before_save :hash_password
18
+ def hash_password
19
+ return unless password_changed? && password?
20
+ self.password = Password.hashed_password(nil, password)
21
+ end
22
+
23
+ def host
24
+ URI.parse(url).host
25
+ end
26
+
27
+ def git_url(remote_id)
28
+ git_url = URI.parse(url).merge("/git/#{remote_id}")
29
+ git_url.user = CGI.escape(username)
30
+ git_url.password = password
31
+ git_url.to_s
32
+ end
33
+
34
+ private
35
+ def perform_request(request_path, method)
36
+ case method
37
+ when :get
38
+ klass = Net::HTTP::Get
39
+ when :post
40
+ klass = Net::HTTP::Post
41
+ else
42
+ raise "Unknown remote request type"
43
+ end
44
+ uri = URI.parse(url)
45
+
46
+ Net::HTTP.start(uri.host, uri.port) do |http|
47
+ request = klass.new(uri.merge(request_path).path, {'Content-Type' => 'application/json'})
48
+ request.basic_auth username, password
49
+ response = http.request(request, '')
50
+ if response.is_a?(Net::HTTPNotFound)
51
+ {'success' => false, 'reason' => 'Path or domain not found'}
52
+ elsif response.code == 302
53
+ {'success' => false, 'reason'=> 'Redirection not supported'}
54
+ else
55
+ JSON.parse(response.body)
56
+ end
57
+ end
58
+ rescue Errno::ECONNREFUSED, Errno::EHOSTDOWN, Errno::EHOSTUNREACH, Errno::ECONNRESET
59
+ {'success' => false, 'reason' => 'Remote host could not be contacted'}
60
+ end
61
+ end
@@ -0,0 +1,202 @@
1
+ class Site < MongoRecord
2
+ attr_reader :cached_records, :cached_models
3
+
4
+ collection :sites
5
+ field :name, :string
6
+ field :created_at, :time
7
+ field :root_directory, :string
8
+ field :remote_id, :string
9
+ field :model_plural_names, :hash
10
+ field :model_types, :hash
11
+ field :extensions, :array
12
+ field :migrations, :array
13
+ field :options, :hash
14
+ field :domains, :array
15
+ one :remote
16
+
17
+ def initialize(values={}, new_record=true)
18
+ super
19
+ @cached_records = {}
20
+ @cached_models = {}
21
+
22
+ # static models
23
+ @models = Model.scoped_for(self)
24
+ @cached_models['Model'] = @cached_models['models'] = @models
25
+ @cached_models['Trigger'] = @cached_models['triggers'] = Trigger.scoped_for(self)
26
+ @cached_models['Task'] = @cached_models['tasks'] = Task.scoped_for(self)
27
+ end
28
+
29
+
30
+ # ----------------------------------------
31
+ # Accessors
32
+ # ----------------------------------------
33
+ # TODO: a better interface is site.options.name.option; site.options.pages.permalink_character
34
+ def option(path)
35
+ component, option = path.split('.')
36
+ options[component].try(:fetch, option, nil).try(:fetch, 'value', nil)
37
+ end
38
+
39
+ def log
40
+ @log ||= Log.new(self)
41
+ end
42
+
43
+ def public_directory
44
+ @public_dir ||= File.join(root_directory, Yodel::PUBLIC_DIRECTORY_NAME)
45
+ end
46
+
47
+ def public_directories
48
+ @public_dirs ||= Yodel.config.public_directories + [public_directory]
49
+ end
50
+
51
+ def layouts_directory
52
+ @layouts_dir ||= File.join(root_directory, Yodel::LAYOUTS_DIRECTORY_NAME)
53
+ end
54
+
55
+ def layout_directories
56
+ @layout_dirs ||= Yodel.config.layout_directories + [layouts_directory]
57
+ end
58
+
59
+ def partials_directory
60
+ @partials_dir ||= File.join(root_directory, Yodel::PARTIALS_DIRECTORY_NAME)
61
+ end
62
+
63
+ def migrations_directory
64
+ @migrations_dir ||= File.join(root_directory, Yodel::MIGRATIONS_DIRECTORY_NAME)
65
+ end
66
+
67
+ def attachments_directory
68
+ @attachments_dir ||= File.join(root_directory, Yodel::ATTACHMENTS_DIRECTORY_NAME)
69
+ end
70
+
71
+ def site_yaml_path
72
+ File.join(root_directory, Yodel::SITE_YML_FILE_NAME)
73
+ end
74
+
75
+ def local_domain
76
+ domains.find {|domain| domain.end_with?('.yodel')}
77
+ end
78
+
79
+ def remote_domains
80
+ domains.select do |domain|
81
+ !domain.end_with?('.yodel') &&
82
+ !domain.end_with?('.local') &&
83
+ !domain.end_with?('.localhost') &&
84
+ !domain.start_with?('192.168.') &&
85
+ !domain.start_with?('10.') &&
86
+ !domain.start_with?('127.') &&
87
+ (!domain.start_with?('172.') || !(16..31).include?(domain.split('.')[1].to_i)) &&
88
+ domain != '0.0.0.0' &&
89
+ domain != '255.255.255.255' &&
90
+ domain != 'localhost' &&
91
+ domain != 'broadcasthost'
92
+ end
93
+ end
94
+
95
+ def latest_revision
96
+ Dir.chdir(root_directory) do
97
+ `git log -n1 --pretty=format:"%H"`
98
+ end
99
+ end
100
+
101
+ def latest_revision_date
102
+ Dir.chdir(root_directory) do
103
+ `git log -n1 --pretty=format:"%ai"`
104
+ end
105
+ end
106
+
107
+
108
+ # ----------------------------------------
109
+ # Life cycle
110
+ # ----------------------------------------
111
+ before_destroy :destroy_records
112
+ def destroy_records
113
+ Trigger.collection.remove(_site_id: id)
114
+ LogEntry.collection.remove(_site_id: id)
115
+ Record.collection.remove(_site_id: id)
116
+ Model.collection.remove(_site_id: id)
117
+ Task.collection.remove(_site_id: id)
118
+ end
119
+
120
+ after_destroy :destroy_directories
121
+ def destroy_directories
122
+ # root directory
123
+ FileUtils.remove_entry_secure(root_directory) if File.directory?(root_directory)
124
+
125
+ # domain symlinks in production
126
+ if Yodel.env.production?
127
+ domains.each do |domain|
128
+ path = File.join(Yodel.config.public_directory, domain)
129
+ FileUtils.remove_file(path, true) if File.symlink?(path)
130
+ end
131
+ end
132
+ end
133
+
134
+ after_save :update_site_yml
135
+ def update_site_yml
136
+ return unless Yodel.env.development?
137
+ File.open(site_yaml_path, 'w') do |file|
138
+ file.write(YAML.dump({
139
+ name: name.to_s,
140
+ extensions: extensions.to_a,
141
+ domains: remote_domains.to_a,
142
+ options: options.to_hash
143
+ }))
144
+ end
145
+ end
146
+
147
+ def reload_from_site_yaml
148
+ update(YAML.load_file(site_yaml_path))
149
+ end
150
+
151
+ def self.load_from_site_yaml(path)
152
+ Site.new(YAML.load_file(path)).tap do |site|
153
+ site.root_directory = File.dirname(path)
154
+ end
155
+ end
156
+
157
+
158
+ # ----------------------------------------
159
+ # Model Lookups
160
+ # ----------------------------------------
161
+ # Method missing is utilised to allows lookups of models by their plural name
162
+ # directly on a site object. site.models is equivalent to site.model('Model')
163
+ def method_missing(name, *args, &block)
164
+ # attempt to find the model in the cached_models hash
165
+ key = name.to_s
166
+ model = @cached_models[key]
167
+ return model unless model.nil?
168
+
169
+ # otherwise perform a lookup
170
+ model = model_by_plural_name(key)
171
+ return super if model.nil?
172
+ model
173
+ end
174
+
175
+ # Retrieve a model by its plural name ('models' as opposed to 'Model'). In general
176
+ # use the method missing functionality of site since it checks the cached_models
177
+ # hash before performing a lookup, whereas this method will always do a lookup.
178
+ def model_by_plural_name(name)
179
+ # ensure the site has a reference to a model by this name. get is required here
180
+ # instead calling 'model_types' explicitly as that relies on method_missing
181
+ # which in turn sometimes calls this method (creating infinite recursion)
182
+ model_id = get('model_types')[name]
183
+ return nil if model_id.nil?
184
+
185
+ # perform a lookup; nil will be returned if the model doesn't exist
186
+ model = @models.find(model_id)
187
+ @cached_models[name] = model
188
+ @cached_models[model.name] = model
189
+ @cached_records[model.id] = model
190
+ end
191
+
192
+ # Retrieve a model by its full name ('Model' as opposed to 'models')
193
+ def model(name)
194
+ # get is required here instead calling 'model_types' explicitly as that relies
195
+ # on method_missing which in turn sometimes calls this method (creating
196
+ # infinite recursion)
197
+ return nil if name.nil?
198
+ model = @cached_models[name]
199
+ return model unless model.nil?
200
+ model_by_plural_name(get('model_plural_names')[name])
201
+ end
202
+ end
@@ -0,0 +1,24 @@
1
+ class EmailAddressValidation < Validation
2
+ def self.validate(params, field, name, value, record, errors)
3
+ begin
4
+ # modified: http://my.rails-royce.org/2010/07/21/email-validation-in-ruby-on-rails-without-regexp/
5
+ address = Mail::Address.new(value)
6
+
7
+ # ensure there is a domain and the full parsed address is equivalent to the value
8
+ if address.domain.present? && address.address == value
9
+ # ensure the domain component is made up of more than just a TLD
10
+ domain_tree = address.send(:tree).domain
11
+ if domain_tree.dot_atom_text.elements.size > 1
12
+ return true
13
+ end
14
+ end
15
+ rescue
16
+ end
17
+
18
+ errors[field.name] << new(name)
19
+ end
20
+
21
+ def describe
22
+ "must be a valid email address"
23
+ end
24
+ end
@@ -0,0 +1,31 @@
1
+ class EmbeddedRecordsValidation < Validation
2
+ def initialize(params, errors)
3
+ super(params)
4
+ @errors = errors
5
+ end
6
+
7
+ def self.validate(field, records, record, errors)
8
+ # embedded record validations
9
+ records = [records] unless records.respond_to?(:to_a)
10
+ embedded_errors = Errors.new
11
+ records.to_a.each_with_index do |embedded_record, index|
12
+ embedded_errors[index] = embedded_record.valid? ? nil : embedded_record.errors
13
+ end
14
+
15
+ # field set validations
16
+ field.fields.each do |name, embedded_field|
17
+ next unless embedded_field.set_validations
18
+ set_value = records.to_a.collect {|embedded| embedded.get(name)}.uniq
19
+ embedded_field.set_validations.each do |type, params|
20
+ Validation.validate(type, params, embedded_field, name, set_value, record, embedded_errors)
21
+ end
22
+ end
23
+
24
+ errors[field.name] = embedded_errors unless embedded_errors.empty?
25
+ end
26
+
27
+ def describe
28
+ # FIXME: don't just call inspect here, format correctly using describe calls
29
+ "has these errors: #{@errors.inspect}}"
30
+ end
31
+ end
@@ -0,0 +1,51 @@
1
+ class Errors
2
+ def initialize
3
+ @errors = {}
4
+ end
5
+
6
+ def inspect
7
+ @errors.inspect
8
+ end
9
+
10
+ def [](name)
11
+ @errors[name] ||= []
12
+ @errors[name]
13
+ end
14
+
15
+ def []=(name, value)
16
+ return if value.nil?
17
+ @errors[name] = value
18
+ end
19
+
20
+ def key?(name)
21
+ @errors.key?(name)
22
+ end
23
+
24
+ def empty?
25
+ @errors.empty?
26
+ end
27
+
28
+ def clear
29
+ @errors.clear
30
+ @summary = nil
31
+ end
32
+
33
+ # errors on a collection
34
+ def <<(error)
35
+ self['_'] << error
36
+ end
37
+
38
+ def summarise
39
+ @summary ||= @errors.each_with_object({}) do |(field, errors), hash|
40
+ if errors.respond_to?(:summarise)
41
+ hash[field.to_s] = errors.summarise.values.to_sentence
42
+ else
43
+ hash[field.to_s] = "#{field.to_s.humanize} #{errors.collect(&:describe).to_sentence}"
44
+ end
45
+ end
46
+ end
47
+
48
+ def to_json(*a)
49
+ summarise.to_json(*a)
50
+ end
51
+ end