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,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