radiant-vhost-extension 2.1.0

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 (69) hide show
  1. data/.gitmodules +3 -0
  2. data/README +64 -0
  3. data/Rakefile +125 -0
  4. data/VERSION +1 -0
  5. data/app/controllers/admin/sites_controller.rb +16 -0
  6. data/app/models/site.rb +26 -0
  7. data/app/models/site_association_observer.rb +8 -0
  8. data/app/views/admin/sites/_form.html.haml +19 -0
  9. data/app/views/admin/sites/edit.html.haml +5 -0
  10. data/app/views/admin/sites/index.html.haml +24 -0
  11. data/app/views/admin/sites/new.html.haml +5 -0
  12. data/app/views/admin/sites/remove.html.haml +19 -0
  13. data/app/views/admin/users/_edit_sites.html.haml +4 -0
  14. data/app/views/admin/users/_site_admin_roles.html.haml +5 -0
  15. data/app/views/admin/users/_sites_td.html.haml +2 -0
  16. data/app/views/admin/users/_sites_th.html.haml +2 -0
  17. data/config/routes.rb +5 -0
  18. data/db/migrate/001_create_sites.rb +11 -0
  19. data/db/migrate/002_add_sites_users.rb +18 -0
  20. data/db/migrate/003_replace_snippet_name_unique_index.rb +10 -0
  21. data/db/migrate/004_add_site_admin_to_users.rb +9 -0
  22. data/db/templates/empty.yml +7 -0
  23. data/db/templates/simple-blog.yml +213 -0
  24. data/lib/bootstrap_with_site_id.rb +50 -0
  25. data/lib/radiant-vhost-extension.rb +0 -0
  26. data/lib/site_scope.rb +65 -0
  27. data/lib/tasks/add_site_columns.rb +44 -0
  28. data/lib/tasks/vhost_extension_tasks.rake +115 -0
  29. data/lib/vhost/admin_users_controller_extensions.rb +22 -0
  30. data/lib/vhost/admin_users_helper_extensions.rb +17 -0
  31. data/lib/vhost/application_controller_extensions.rb +36 -0
  32. data/lib/vhost/application_helper_extensions.rb +15 -0
  33. data/lib/vhost/controller_access_extensions.rb +16 -0
  34. data/lib/vhost/pages_controller_extensions.rb +11 -0
  35. data/lib/vhost/radiant_cache_extensions.rb +53 -0
  36. data/lib/vhost/site_scoped_model_extensions.rb +46 -0
  37. data/lib/vhost_default_config.yml +22 -0
  38. data/spec/controllers/admin/pages_controller_spec.rb +173 -0
  39. data/spec/controllers/admin/sites_controller_spec.rb +33 -0
  40. data/spec/controllers/site_controller_spec.rb +33 -0
  41. data/spec/datasets/site_home_pages_dataset.rb +76 -0
  42. data/spec/datasets/site_pages_dataset.rb +31 -0
  43. data/spec/datasets/site_users_dataset.rb +50 -0
  44. data/spec/datasets/sites_dataset.rb +10 -0
  45. data/spec/datasets/sites_site_users_and_site_pages_dataset.rb +8 -0
  46. data/spec/datasets/sites_site_users_dataset.rb +13 -0
  47. data/spec/fixtures/page_parts.yml +11 -0
  48. data/spec/fixtures/pages.yml +19 -0
  49. data/spec/fixtures/sites.yml +7 -0
  50. data/spec/fixtures/sites_users.yml +6 -0
  51. data/spec/fixtures/users.yml +35 -0
  52. data/spec/models/page_spec.rb +22 -0
  53. data/spec/models/site_spec.rb +19 -0
  54. data/spec/models/user_spec.rb +16 -0
  55. data/spec/spec.opts +6 -0
  56. data/spec/spec_helper.rb +42 -0
  57. data/test/fixtures/page_parts.yml +11 -0
  58. data/test/fixtures/pages.yml +19 -0
  59. data/test/fixtures/sites.yml +7 -0
  60. data/test/fixtures/sites_users.yml +6 -0
  61. data/test/fixtures/users.yml +35 -0
  62. data/test/functional/admin/pages_controller_test.rb +142 -0
  63. data/test/functional/admin/site_controller_test.rb +53 -0
  64. data/test/functional/vhost_extension_test.rb +37 -0
  65. data/test/helpers/page_part_test_helper.rb +49 -0
  66. data/test/test_helper.rb +17 -0
  67. data/test/unit/site_test.rb +26 -0
  68. data/vhost_extension.rb +154 -0
  69. metadata +167 -0
@@ -0,0 +1,50 @@
1
+ module BootstrapWithSiteId
2
+ def load_database_template_with_site_id(filename)
3
+ template = nil
4
+ if filename
5
+ name = find_template_in_path(filename)
6
+ unless name
7
+ announce "Invalid template name: #{filename}"
8
+ filename = nil
9
+ else
10
+ template = load_template_file(name)
11
+ end
12
+ end
13
+ unless filename
14
+ templates = find_and_load_templates("#{RAILS_ROOT}/vendor/extensions/vhost/db/templates/*.yml")
15
+ templates.concat find_and_load_templates("#{RADIANT_ROOT}/config/extensions/vhost/db/templates/*.yml")
16
+ templates.concat find_and_load_templates("#{RAILS_ROOT}/db/templates/*.yml") if RADIANT_ROOT != RAILS_ROOT
17
+ choose do |menu|
18
+ menu.header = "\nSelect a database template"
19
+ menu.prompt = "[1-#{templates.size}]: "
20
+ menu.select_by = :index
21
+ templates.each { |t| menu.choice(t['name']) { template = t } }
22
+ end
23
+ end
24
+ # If there are sites defined create them first as there are many to many
25
+ # relationships from users to sites that need to be created and will fail
26
+ # if the site isn't created first.
27
+ if !template['records']['Sites'].nil?
28
+ site_template = {}
29
+ site_template['records'] = {}
30
+ site_template['records']['Sites'] = template['records']['Sites']
31
+ create_records(site_template)
32
+ site_template['records'].delete('Sites')
33
+ end
34
+ create_records(template)
35
+ end
36
+
37
+ def find_template_in_path_with_site_id(filename)
38
+ [
39
+ filename,
40
+ "#{RAILS_ROOT}/vendor/extensions/vhost/db/templates/#{filename}",
41
+ "#{RADIANT_ROOT}/config/extensions/vhost/db/templates/#{filename}",
42
+ "#{RADIANT_ROOT}/#{filename}",
43
+ "#{RADIANT_ROOT}/db/templates/#{filename}",
44
+ "#{RAILS_ROOT}/#{filename}",
45
+ "#{RAILS_ROOT}/db/templates/#{filename}",
46
+ "#{Dir.pwd}/#{filename}",
47
+ "#{Dir.pwd}/db/templates/#{filename}",
48
+ ].find { |name| File.file?(name) }
49
+ end
50
+ end
File without changes
data/lib/site_scope.rb ADDED
@@ -0,0 +1,65 @@
1
+ module SiteScope
2
+
3
+ def self.included(base)
4
+ base.send :helper_method, :current_site
5
+ end
6
+
7
+ def current_site
8
+ return @current_site unless @current_site.nil?
9
+ # For testing we won't have a request.host so we're going to use a class
10
+ # variable (VhostExtension.HOST) in those cases.
11
+ host ||= VhostExtension.HOST || request.host
12
+ # Remove the 'www.' from the site so we don't have to always include a www.
13
+ # in addition to the regular domain name.
14
+ host.gsub!(/^www\./, '')
15
+ @current_site ||= Site.find_by_hostname(host) || Site.find_by_hostname('*') || Site.find(:first, :conditions => ["hostname LIKE ?", "%#{host}%"])
16
+ raise "No site found to match #{host}." unless @current_site
17
+ Thread.current[:current_site_id] = @current_site.id
18
+ @current_site
19
+ end
20
+
21
+ protected
22
+ # This is the key method that forks in the additional conditions that will
23
+ # be used to fetch the site-scoped models. It also defines how the site-scoped
24
+ # models will be saved with a site_id.
25
+ def site_scope
26
+ @site_scope ||= {
27
+ :find => { :conditions => ["site_id = ?", current_site.id]},
28
+ :create => { :site_id => current_site.id }
29
+ }
30
+ end
31
+
32
+ def users_site_scope
33
+ @users_site_scope = {}
34
+ # Only do the user site scoping if it's a site_admin. We don't want the admin to be restricted.
35
+ if current_user && current_user.site_admin?
36
+ @users_site_scope = {
37
+ :find => { :joins => "JOIN sites_users AS scoped_sites_users ON scoped_sites_users.user_id = id", :conditions => ["scoped_sites_users.site_id = ?", current_site.id]},
38
+ # Make sure admin is always false - wouldn't want someone trying to set it to true through some html magic
39
+ :create => { :site_ids => [current_site.id], :site_admin => false }
40
+ }
41
+ end
42
+ return @users_site_scope
43
+ end
44
+
45
+ # Should this really be here? Shouldn't we be calling this regardless of if we go
46
+ # through the ApplicationControllers :before_filter?
47
+ def set_site_scope_in_models
48
+ set_model_current_site = lambda {|model|
49
+ model.constantize.send :cattr_accessor, :current_site
50
+ model.constantize.current_site = self.current_site
51
+ }
52
+ VhostExtension.MODELS.each do |model|
53
+ model_config = VhostExtension.read_config[:model_uniqueness_validations][model]
54
+ if model_config['sti_classes']
55
+ model_config['sti_classes'].each do |klass|
56
+ set_model_current_site.call(klass)
57
+ end
58
+ end
59
+ set_model_current_site.call(model)
60
+ end
61
+ Site.send :cattr_accessor, :current_site
62
+ Site.current_site = self.current_site
63
+ end
64
+
65
+ end
@@ -0,0 +1,44 @@
1
+ class AddSiteColumns < ActiveRecord::Migration
2
+
3
+ config = VhostExtension.read_config
4
+
5
+ MODELS = config[:models]
6
+
7
+ # Declare the models so we can use them.
8
+ MODELS.each do |model|
9
+ eval "class #{model} < ActiveRecord::Base; end"
10
+ end
11
+
12
+ def self.up
13
+ MODELS.each do |model|
14
+ begin
15
+ add_column model.tableize, :site_id, :integer
16
+ model.constantize.update_all "site_id = 1"
17
+ # Special case for Snippets to add a proper index
18
+ if model == 'Snippet'
19
+ add_index :snippets, [:name, :site_id], :unique => true
20
+ end
21
+ rescue
22
+ # Ignore errors here, they're going to happen when the user
23
+ # does a 'remigrate'
24
+ end
25
+ end
26
+ add_index :snippets, [:name, :site_id] rescue nil
27
+ end
28
+
29
+ def self.down
30
+ MODELS.each do |model|
31
+ begin
32
+ # Special case for Snippets to remove index
33
+ if model == 'Snippet'
34
+ remove_index :snippets, [:name, :site_id]
35
+ end
36
+ remove_column model.tableize, :site_id
37
+ rescue
38
+ # Ignore errors here, they're going to happen when the user
39
+ # does a 'remigrate'
40
+ end
41
+ end
42
+ remove_index :snippets, :column => [:name, :site_id] rescue nil
43
+ end
44
+ end
@@ -0,0 +1,115 @@
1
+ namespace :radiant do
2
+ namespace :extensions do
3
+ namespace :vhost do
4
+
5
+ desc "Prepares your database for Vhost"
6
+ task :install => [:environment, :migrate, :apply_site_scoping]
7
+
8
+ desc "Runs the migration of the Vhost extension"
9
+ task :migrate => :environment do
10
+ require 'radiant/extension_migrator'
11
+ if ENV["VERSION"]
12
+ VhostExtension.migrator.migrate(ENV["VERSION"].to_i)
13
+ else
14
+ VhostExtension.migrator.migrate
15
+ end
16
+ end
17
+
18
+
19
+ # @todo the following method should accept a direction. We should also
20
+ # rename it something like 'apply_site_scoping' and 'reset_site_scoping'
21
+ # or something...
22
+ desc "Initializes site scoping. "
23
+ task :apply_site_scoping => :environment do
24
+ require "#{File.dirname(__FILE__)}/add_site_columns"
25
+ AddSiteColumns.up
26
+ end
27
+
28
+ desc "Reinitializes site scoping in the event a new model needs to be site scoped."
29
+ task :destroy_site_scoping => :environment do
30
+ require 'highline/import'
31
+ if agree("This task will destroy any model to site relationships in the database. Are you sure \nyou want to continue? [yn] ")
32
+ AddSiteColumns.up
33
+ AddSiteColumns.down
34
+ end
35
+ end
36
+
37
+ end
38
+ end
39
+ end
40
+
41
+ Rake::TaskManager.class_eval do
42
+ def remove_task(task_name)
43
+ @tasks.delete(task_name.to_s)
44
+ end
45
+ end
46
+
47
+ def remove_task(task_name)
48
+ Rake.application.remove_task(task_name)
49
+ end
50
+
51
+ # We need the bootstrap task to use site_ids
52
+ remove_task "db:bootstrap"
53
+ remove_task "db:remigrate"
54
+ namespace :db do
55
+ desc "Bootstrap your database for Radiant."
56
+ task :bootstrap => :remigrate do
57
+ require 'radiant/setup'
58
+ require File.join(File.dirname(__FILE__), '../bootstrap_with_site_id')
59
+ Radiant::Setup.send :include, BootstrapWithSiteId
60
+ Radiant::Setup.send :alias_method_chain, :load_database_template, :site_id
61
+ Radiant::Setup.send :alias_method_chain, :find_template_in_path, :site_id
62
+
63
+ Radiant::Setup.bootstrap(
64
+ :admin_name => ENV['ADMIN_NAME'],
65
+ :admin_username => ENV['ADMIN_USERNAME'],
66
+ :admin_password => ENV['ADMIN_PASSWORD'],
67
+ :database_template => ENV['DATABASE_TEMPLATE']
68
+ )
69
+
70
+ Rake::Task["radiant:extensions:vhost:apply_site_scoping"].invoke
71
+
72
+ end
73
+
74
+ desc "Migrate schema to version 0 and back up again. WARNING: Destroys all data in tables!!"
75
+ task :remigrate => :environment do
76
+ require 'highline/import'
77
+ require 'radiant/extension_migrator'
78
+ if ENV['OVERWRITE'].to_s.downcase == 'true' or
79
+ agree("This task will destroy any data in the database. Are you sure you want to \ncontinue? [yn] ")
80
+
81
+ # Migrate extensions downward
82
+ Radiant::ExtensionLoader.instance.extensions.each do |ext|
83
+ # The first time you bootstrap you'll always encounter exceptions
84
+ # so be sure to ignore them here.
85
+ begin
86
+ ext.migrator.migrate(0)
87
+ rescue
88
+ puts "An error occurred while migrating the #{ext} extension downward: #{$!}"
89
+ end
90
+ end
91
+
92
+ # Migrate downward
93
+ ActiveRecord::Migrator.migrate("#{RADIANT_ROOT}/db/migrate/", 0)
94
+
95
+ # Migrate upward
96
+ Rake::Task["db:migrate"].invoke
97
+
98
+ # Migrate extensions upward
99
+ Radiant::ExtensionLoader.instance.extensions.each do |ext|
100
+ # The first time you bootstrap you'll always encounter exceptions
101
+ # so be sure to ignore them here.
102
+ ext.migrator.migrate
103
+ end
104
+
105
+ # Remigrate the extensions to catch any new site scoped extensions added
106
+ Rake::Task["radiant:extensions:vhost:apply_site_scoping"].invoke
107
+
108
+ # Dump the schema
109
+ Rake::Task["db:schema:dump"].invoke
110
+ else
111
+ say "Task cancelled."
112
+ exit
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,22 @@
1
+ module Vhost::AdminUsersControllerExtensions
2
+ def self.included(receiver)
3
+ receiver.send :only_allow_access_to, :index, :show, :new, :create, :edit, :update, :remove, :destroy,
4
+ :when => [:admin, :site_admin],
5
+ :denied_url => { :controller => 'pages', :action => 'index' },
6
+ :denied_message => 'You must have administrative privileges to perform this action.'
7
+
8
+ receiver.class_eval {
9
+ def load_model
10
+ self.model = if params[:id]
11
+ model_class.find(params[:id], :readonly => false)
12
+ else
13
+ model_class.new
14
+ end
15
+ end
16
+
17
+ def load_models
18
+ self.models = current_site.users.paginate(pagination_parameters)
19
+ end
20
+ }
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ module Vhost::AdminUsersHelperExtensions
2
+ def self.included(receiver)
3
+ receiver.send :alias_method_chain, :roles, :site_admin
4
+ receiver.send :define_method, :sites do |user|
5
+ sites = user.sites.collect{|site| site.hostname}
6
+ sites.join("<br/>")
7
+ end
8
+ end
9
+
10
+ def roles_with_site_admin(user)
11
+ roles = []
12
+ roles << 'Admin' if user.admin?
13
+ roles << 'Site Admin' if user.site_admin?
14
+ roles << 'Designer' if user.designer?
15
+ roles.join(', ')
16
+ end
17
+ end
@@ -0,0 +1,36 @@
1
+ module Vhost::ApplicationControllerExtensions
2
+ def self.included(base)
3
+ base.class_eval {
4
+ prepend_before_filter :redirect_to_primary_site
5
+
6
+ helper_method :primary_site_url
7
+
8
+ def redirect_to_primary_site
9
+ if VhostExtension.REDIRECT_TO_PRIMARY_SITE
10
+ site = current_site
11
+ return if site.nil? || site.hostname.include?("*")
12
+ primary_host = site.hostname.split(',')[0].strip
13
+ redirect_to(primary_site_url + request.request_uri) if request.host != primary_host
14
+ end
15
+ end
16
+
17
+ def primary_site_url
18
+ site = current_site
19
+ return nil if site.nil? || site.hostname.include?("*")
20
+
21
+ # Rebuild the current URL. Check if it matches the URL of the
22
+ # primary site and redirect if it does not.
23
+ prefix = request.ssl? ? "https://" : "http://"
24
+ host = request.host
25
+ port = request.port_string
26
+
27
+ # Primary site is the first site
28
+ primary_host = site.hostname.split(',')[0].strip
29
+
30
+ # Return the concatenation
31
+ prefix+primary_host+port
32
+ end
33
+ }
34
+ end
35
+ end
36
+
@@ -0,0 +1,15 @@
1
+ module Vhost::ApplicationHelperExtensions
2
+ def self.included(receiver)
3
+ # This swaps out the 'subtitle' method for the 'site_hostname'
4
+ # method to show the hostname in the subtitle in admin...
5
+ receiver.send :alias_method_chain, :subtitle, :site_hostname
6
+
7
+ receiver.send :define_method, :site_admin? do
8
+ current_user and current_user.site_admin?
9
+ end
10
+ end
11
+
12
+ def subtitle_with_site_hostname
13
+ current_site.hostname
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ module Vhost::ControllerAccessExtensions
2
+ def self.included(receiver)
3
+ receiver.send :before_filter, :ensure_user_has_site_access
4
+ end
5
+
6
+ def ensure_user_has_site_access
7
+ unless current_site.allow_access_for(current_user)
8
+ cookies[:session_token] = { :expires => 1.day.ago }
9
+ self.current_user.forget_me if self.current_user
10
+ self.current_user = nil
11
+ flash[:error] = 'Access denied.'
12
+ redirect_to login_url
13
+ end
14
+ end
15
+ end
16
+
@@ -0,0 +1,11 @@
1
+ module Vhost::PagesControllerExtensions
2
+ def self.included(receiver)
3
+ receiver.send :alias_method_chain, :clear_model_cache, :site_specificity
4
+ end
5
+
6
+ def clear_model_cache_with_site_specificity
7
+ url_to_expire = "#{request.host}#{@page.url}"
8
+ Radiant::Cache.clear(url_to_expire) if defined?(Radiant::Cache)
9
+ end
10
+ end
11
+
@@ -0,0 +1,53 @@
1
+ # Alter Radiant's site controller to add the domain to the cache files.
2
+ # Also store requests for later use.
3
+ # See radiant/app/controllers/site_controller.rb
4
+ module Vhost::RadiantCacheExtensions
5
+ module RadiantCache
6
+ def self.included(base)
7
+ base.class_eval {
8
+ # This sets up the cache - the entitystore and metastore 'cache/entity' and 'cache/meta' sets up the folder
9
+ # structure for storing the cache items.
10
+ def self.new(app, options={})
11
+ self.use_x_sendfile = options.delete(:use_x_sendfile) if options[:use_x_sendfile]
12
+ self.use_x_accel_redirect = options.delete(:use_x_accel_redirect) if options[:use_x_accel_redirect]
13
+ Rack::Cache.new(app, {
14
+ :entitystore => "radiant:tmp/cache/entity",
15
+ :metastore => "radiant:tmp/cache/meta",
16
+ :verbose => false}.merge(options))
17
+ end
18
+ def self.clear(host_and_url = nil)
19
+ meta_stores.each {|ms| ms.clear(host_and_url) }
20
+ entity_stores.each {|es| es.clear }
21
+ end
22
+ }
23
+ end
24
+ end
25
+
26
+ module MetaStore
27
+ def self.included(base)
28
+ base.class_eval {
29
+ def initialize(root="#{Rails.root}/tmp/cache/meta")
30
+ super
31
+ Radiant::Cache.meta_stores << self
32
+ end
33
+
34
+ def clear(host_and_url = nil)
35
+ if host_and_url.nil?
36
+ Dir[File.join(self.root, "*")].each {|file| FileUtils.rm_rf(file) }
37
+ else
38
+ FileUtils.rm_rf(key_path("#{host_and_url}"))
39
+ end
40
+ end
41
+
42
+ # cache_key should, by default, include the host as well the query string
43
+ # def cache_key(request)
44
+ # "#{request.host}#{request.path_info}"
45
+ # end
46
+ }
47
+ end
48
+ end
49
+
50
+ # FIXME - Add support for EntityStore - MetaStore is the most important caching
51
+ # mechanism to be able to clear by site but EntityStore would be great too.
52
+
53
+ end
@@ -0,0 +1,46 @@
1
+ require 'yaml'
2
+ module Vhost::SiteScopedModelExtensions
3
+ module InstanceMethods
4
+ def self.included(base)
5
+ base.class_eval {
6
+ Rails.logger.debug("Applying SiteScope to '"+self.name+"'")
7
+
8
+ self.clear_callbacks_by_calling_method_name(:validate, :validates_uniqueness_of)
9
+ validates_presence_of :site_id
10
+ belongs_to :site
11
+
12
+ # Parse the model_uniqueness_validations config and set any necessary validations
13
+ # If the current class name matches an entry in the config then process it
14
+ config = VhostExtension.MODEL_UNIQUENESS_VALIDATIONS[self.name]
15
+ unless config.nil?
16
+ config.each_pair do |attr, params|
17
+ unless attr == 'sti_classes'
18
+ validates_uniqueness_of attr.to_sym, params.symbolize_keys
19
+ end
20
+ end
21
+ end
22
+ }
23
+ end
24
+ end
25
+ module ClassMethods
26
+ def clear_callbacks_by_calling_method_name(kind, calling_method_name)
27
+ calling_method_name = calling_method_name.to_s
28
+ # Callbacks are stored by kind as instance variables named @<kind>_callbacks.
29
+ # Fetch them so we can kick out the matching items.
30
+ callback_chain = eval("@#{kind.to_s}_callbacks")
31
+ unless callback_chain.nil?
32
+ callback_chain.reject! do |callback|
33
+ method = callback.method
34
+ if method.is_a?(Proc)
35
+ # Returns the symbol for the method the proc was declared in
36
+ current_calling_method_name = eval("caller[0] =~ /`([^']*)'/ and $1", method.binding).to_s rescue nil
37
+ current_calling_method_name == calling_method_name
38
+ else
39
+ false
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+
@@ -0,0 +1,22 @@
1
+ redirect_to_primary_site: false
2
+ models:
3
+ Layout:
4
+ name:
5
+ message:
6
+ 'name already in use'
7
+ scope:
8
+ site_id
9
+ Page:
10
+ slug:
11
+ scope:
12
+ - parent_id
13
+ - site_id
14
+ message:
15
+ 'slug already in use for child of parent'
16
+ Snippet:
17
+ name:
18
+ message:
19
+ 'name already in use'
20
+ scope:
21
+ site_id
22
+