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.
- data/.document +5 -0
- data/.gitignore +9 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +63 -0
- data/LICENSE +1 -0
- data/README.rdoc +20 -0
- data/Rakefile +1 -0
- data/bin/yodel +4 -0
- data/lib/yodel.rb +22 -0
- data/lib/yodel/application/application.rb +44 -0
- data/lib/yodel/application/extension.rb +59 -0
- data/lib/yodel/application/request_handler.rb +48 -0
- data/lib/yodel/application/yodel.rb +25 -0
- data/lib/yodel/command/command.rb +94 -0
- data/lib/yodel/command/deploy.rb +67 -0
- data/lib/yodel/command/dns_server.rb +16 -0
- data/lib/yodel/command/installer.rb +229 -0
- data/lib/yodel/config/config.rb +30 -0
- data/lib/yodel/config/environment.rb +16 -0
- data/lib/yodel/config/yodel.rb +21 -0
- data/lib/yodel/exceptions/destroyed_record.rb +2 -0
- data/lib/yodel/exceptions/domain_not_found.rb +16 -0
- data/lib/yodel/exceptions/duplicate_layout.rb +2 -0
- data/lib/yodel/exceptions/exceptions.rb +3 -0
- data/lib/yodel/exceptions/inconsistent_lock_state.rb +2 -0
- data/lib/yodel/exceptions/invalid_field.rb +2 -0
- data/lib/yodel/exceptions/invalid_index.rb +2 -0
- data/lib/yodel/exceptions/invalid_mixin.rb +2 -0
- data/lib/yodel/exceptions/invalid_model_field.rb +2 -0
- data/lib/yodel/exceptions/layout_not_found.rb +2 -0
- data/lib/yodel/exceptions/mass_assignment.rb +2 -0
- data/lib/yodel/exceptions/missing_migration.rb +2 -0
- data/lib/yodel/exceptions/missing_root_directory.rb +15 -0
- data/lib/yodel/exceptions/unable_to_acquire_lock.rb +2 -0
- data/lib/yodel/exceptions/unauthorised.rb +2 -0
- data/lib/yodel/exceptions/unknown_field.rb +2 -0
- data/lib/yodel/middleware/development_server.rb +180 -0
- data/lib/yodel/middleware/error_pages.rb +72 -0
- data/lib/yodel/middleware/public_assets.rb +78 -0
- data/lib/yodel/middleware/request.rb +16 -0
- data/lib/yodel/middleware/site_detector.rb +22 -0
- data/lib/yodel/mime_types/default_mime_set.rb +28 -0
- data/lib/yodel/mime_types/mime_type.rb +68 -0
- data/lib/yodel/mime_types/mime_type_set.rb +41 -0
- data/lib/yodel/mime_types/mime_types.rb +6 -0
- data/lib/yodel/mime_types/yodel.rb +15 -0
- data/lib/yodel/models/api/api.rb +1 -0
- data/lib/yodel/models/api/api_call.rb +87 -0
- data/lib/yodel/models/core/associations/association.rb +37 -0
- data/lib/yodel/models/core/associations/associations.rb +22 -0
- data/lib/yodel/models/core/associations/counts/many_association.rb +18 -0
- data/lib/yodel/models/core/associations/counts/one_association.rb +22 -0
- data/lib/yodel/models/core/associations/embedded/embedded_association.rb +47 -0
- data/lib/yodel/models/core/associations/embedded/embedded_record_array.rb +12 -0
- data/lib/yodel/models/core/associations/embedded/many_embedded_association.rb +62 -0
- data/lib/yodel/models/core/associations/embedded/one_embedded_association.rb +49 -0
- data/lib/yodel/models/core/associations/query/many_query_association.rb +10 -0
- data/lib/yodel/models/core/associations/query/one_query_association.rb +10 -0
- data/lib/yodel/models/core/associations/query/query_association.rb +64 -0
- data/lib/yodel/models/core/associations/record_association.rb +38 -0
- data/lib/yodel/models/core/associations/store/many_store_association.rb +32 -0
- data/lib/yodel/models/core/associations/store/one_store_association.rb +14 -0
- data/lib/yodel/models/core/associations/store/store_association.rb +51 -0
- data/lib/yodel/models/core/attachments/attachment.rb +73 -0
- data/lib/yodel/models/core/attachments/image.rb +38 -0
- data/lib/yodel/models/core/core.rb +15 -0
- data/lib/yodel/models/core/fields/alias_field.rb +32 -0
- data/lib/yodel/models/core/fields/array_field.rb +64 -0
- data/lib/yodel/models/core/fields/attachment_field.rb +42 -0
- data/lib/yodel/models/core/fields/boolean_field.rb +28 -0
- data/lib/yodel/models/core/fields/change_sensitive_array.rb +96 -0
- data/lib/yodel/models/core/fields/change_sensitive_hash.rb +53 -0
- data/lib/yodel/models/core/fields/color_field.rb +4 -0
- data/lib/yodel/models/core/fields/date_field.rb +35 -0
- data/lib/yodel/models/core/fields/decimal_field.rb +19 -0
- data/lib/yodel/models/core/fields/email_field.rb +10 -0
- data/lib/yodel/models/core/fields/enum_field.rb +33 -0
- data/lib/yodel/models/core/fields/field.rb +154 -0
- data/lib/yodel/models/core/fields/fields.rb +29 -0
- data/lib/yodel/models/core/fields/fields_field.rb +31 -0
- data/lib/yodel/models/core/fields/filter_mixin.rb +9 -0
- data/lib/yodel/models/core/fields/filtered_string_field.rb +5 -0
- data/lib/yodel/models/core/fields/filtered_text_field.rb +5 -0
- data/lib/yodel/models/core/fields/function_field.rb +28 -0
- data/lib/yodel/models/core/fields/hash_field.rb +54 -0
- data/lib/yodel/models/core/fields/html_field.rb +15 -0
- data/lib/yodel/models/core/fields/image_field.rb +11 -0
- data/lib/yodel/models/core/fields/integer_field.rb +25 -0
- data/lib/yodel/models/core/fields/password_field.rb +21 -0
- data/lib/yodel/models/core/fields/self_field.rb +27 -0
- data/lib/yodel/models/core/fields/string_field.rb +15 -0
- data/lib/yodel/models/core/fields/tags_field.rb +7 -0
- data/lib/yodel/models/core/fields/text_field.rb +7 -0
- data/lib/yodel/models/core/fields/time_field.rb +36 -0
- data/lib/yodel/models/core/functions/function.rb +471 -0
- data/lib/yodel/models/core/functions/functions.rb +2 -0
- data/lib/yodel/models/core/functions/trigger.rb +14 -0
- data/lib/yodel/models/core/log/log.rb +33 -0
- data/lib/yodel/models/core/log/log_entry.rb +12 -0
- data/lib/yodel/models/core/model/abstract_model.rb +59 -0
- data/lib/yodel/models/core/model/model.rb +460 -0
- data/lib/yodel/models/core/model/mongo_model.rb +25 -0
- data/lib/yodel/models/core/model/site_model.rb +17 -0
- data/lib/yodel/models/core/mongo/mongo.rb +3 -0
- data/lib/yodel/models/core/mongo/primary_key_factory.rb +12 -0
- data/lib/yodel/models/core/mongo/query.rb +68 -0
- data/lib/yodel/models/core/mongo/record_index.rb +89 -0
- data/lib/yodel/models/core/record/abstract_record.rb +411 -0
- data/lib/yodel/models/core/record/embedded_record.rb +47 -0
- data/lib/yodel/models/core/record/mongo_record.rb +83 -0
- data/lib/yodel/models/core/record/record.rb +386 -0
- data/lib/yodel/models/core/record/section.rb +21 -0
- data/lib/yodel/models/core/record/site_record.rb +31 -0
- data/lib/yodel/models/core/site/migration.rb +52 -0
- data/lib/yodel/models/core/site/remote.rb +61 -0
- data/lib/yodel/models/core/site/site.rb +202 -0
- data/lib/yodel/models/core/validations/email_address_validation.rb +24 -0
- data/lib/yodel/models/core/validations/embedded_records_validation.rb +31 -0
- data/lib/yodel/models/core/validations/errors.rb +51 -0
- data/lib/yodel/models/core/validations/excluded_from_validation.rb +10 -0
- data/lib/yodel/models/core/validations/excludes_combinations_validation.rb +18 -0
- data/lib/yodel/models/core/validations/format_validation.rb +10 -0
- data/lib/yodel/models/core/validations/included_in_validation.rb +10 -0
- data/lib/yodel/models/core/validations/includes_combinations_validation.rb +14 -0
- data/lib/yodel/models/core/validations/length_validation.rb +28 -0
- data/lib/yodel/models/core/validations/password_confirmation_validation.rb +11 -0
- data/lib/yodel/models/core/validations/required_validation.rb +9 -0
- data/lib/yodel/models/core/validations/unique_validation.rb +9 -0
- data/lib/yodel/models/core/validations/validation.rb +39 -0
- data/lib/yodel/models/core/validations/validations.rb +15 -0
- data/lib/yodel/models/email/email.rb +79 -0
- data/lib/yodel/models/migrations/01_record_model.rb +29 -0
- data/lib/yodel/models/migrations/02_page_model.rb +45 -0
- data/lib/yodel/models/migrations/03_layout_model.rb +38 -0
- data/lib/yodel/models/migrations/04_group_model.rb +61 -0
- data/lib/yodel/models/migrations/05_user_model.rb +24 -0
- data/lib/yodel/models/migrations/06_snippet_model.rb +13 -0
- data/lib/yodel/models/migrations/07_search_page_model.rb +32 -0
- data/lib/yodel/models/migrations/08_default_site_options.rb +21 -0
- data/lib/yodel/models/migrations/09_security_page_models.rb +36 -0
- data/lib/yodel/models/migrations/10_record_proxy_page_model.rb +17 -0
- data/lib/yodel/models/migrations/11_email_model.rb +28 -0
- data/lib/yodel/models/migrations/12_api_call_model.rb +23 -0
- data/lib/yodel/models/migrations/13_redirect_page_model.rb +13 -0
- data/lib/yodel/models/migrations/14_menu_model.rb +20 -0
- data/lib/yodel/models/models.rb +8 -0
- data/lib/yodel/models/pages/form_builder.rb +379 -0
- data/lib/yodel/models/pages/html_decorator.rb +132 -0
- data/lib/yodel/models/pages/layout.rb +120 -0
- data/lib/yodel/models/pages/menu.rb +32 -0
- data/lib/yodel/models/pages/page.rb +378 -0
- data/lib/yodel/models/pages/pages.rb +7 -0
- data/lib/yodel/models/pages/record_proxy_page.rb +188 -0
- data/lib/yodel/models/pages/redirect_page.rb +11 -0
- data/lib/yodel/models/search/search.rb +1 -0
- data/lib/yodel/models/search/search_page.rb +58 -0
- data/lib/yodel/models/security/facebook_login_page.rb +55 -0
- data/lib/yodel/models/security/group.rb +10 -0
- data/lib/yodel/models/security/guests_group.rb +5 -0
- data/lib/yodel/models/security/login_page.rb +20 -0
- data/lib/yodel/models/security/logout_page.rb +13 -0
- data/lib/yodel/models/security/noone_group.rb +5 -0
- data/lib/yodel/models/security/owner_group.rb +8 -0
- data/lib/yodel/models/security/password.rb +5 -0
- data/lib/yodel/models/security/password_reset_page.rb +47 -0
- data/lib/yodel/models/security/security.rb +10 -0
- data/lib/yodel/models/security/user.rb +33 -0
- data/lib/yodel/public/core/css/core.css +257 -0
- data/lib/yodel/public/core/css/reset.css +48 -0
- data/lib/yodel/public/core/images/cross.png +0 -0
- data/lib/yodel/public/core/images/spinner.gif +0 -0
- data/lib/yodel/public/core/images/tick.png +0 -0
- data/lib/yodel/public/core/images/yodel.png +0 -0
- data/lib/yodel/public/core/js/jquery.min.js +18 -0
- data/lib/yodel/public/core/js/json2.js +480 -0
- data/lib/yodel/public/core/js/yodel_jquery.js +238 -0
- data/lib/yodel/request/authentication.rb +76 -0
- data/lib/yodel/request/flash.rb +28 -0
- data/lib/yodel/request/request.rb +4 -0
- data/lib/yodel/requires.rb +47 -0
- data/lib/yodel/task_queue/queue_daemon.rb +33 -0
- data/lib/yodel/task_queue/queue_worker.rb +32 -0
- data/lib/yodel/task_queue/stats_thread.rb +27 -0
- data/lib/yodel/task_queue/task.rb +62 -0
- data/lib/yodel/task_queue/task_queue.rb +40 -0
- data/lib/yodel/types/date.rb +5 -0
- data/lib/yodel/types/object_id.rb +11 -0
- data/lib/yodel/types/time.rb +5 -0
- data/lib/yodel/version.rb +3 -0
- data/system/Library/LaunchDaemons/com.yodelcms.dns.plist +26 -0
- data/system/Library/LaunchDaemons/com.yodelcms.server.plist +26 -0
- data/system/etc/resolver/yodel +2 -0
- data/system/usr/local/bin/yodel_command_runner +2 -0
- data/system/usr/local/etc/yodel/development_settings.rb +28 -0
- data/system/usr/local/etc/yodel/production_settings.rb +27 -0
- data/system/var/log/yodel.log +0 -0
- data/test/helper.rb +18 -0
- data/test/test_yodel.rb +4 -0
- data/yodel.gemspec +47 -0
- 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
|