RefineryCMS & Rails 3.2 into your existing App


Why?

Refinery CMS provides a polished CMS to integrate seamlessly into your existing rails application.

How?

Refinery breaks its components into individual engines and we can pick and choose which engines we want in our application. Refineries authentication engine uses devise however we want to have complete control over Devise in our Rails application.

List of Engines:

  • Authentication
  • Core
  • Dashboard
  • Images
  • Pages
  • Resources

In order to integrate Refinery with your application we will specifically include all the the Refinery engines EXCEPT the Authentication engine.

Steps:

  1. Setup Rails Application, Devise and Refinery
  2. Emulate Refinery User Model
  3. Override and Initialize
  4. Test

Setup Rails Application, Devise and Refinery

We will now setup refinery and devise.

Update your Gemfile
#Devise
gem 'devise'
gem 'fog'

#Refinery
git 'git://github.com/refinery/refinerycms.git', :branch => '2-1-stable' do
  gem 'refinerycms-dashboard'
  gem 'refinerycms-images'
  gem 'refinerycms-pages'
  gem 'refinerycms-resources'
end
Install devise
rails generate devise:install
rails generate devise User
rails generate devise:views
Install Refinery
rails g refinery:cms --fresh-installation

If you ran rails g refinery:cms --fresh-installation and get the error 'uninitialized constant Refinery::AuthenticationGenerator.'

Steps to fix:

  1. Add export BUNDLER_EDITOR=vim to your .bashrc or .profile
  2. bundle open refinerycms-core
  3. Navigate to core/lib/generators/refinery/cms/cms_generator.rb
  4. Go to line 204 and edit this to reflect the pull request to RefineryCMS
  5. Run rails g refinery:cms --fresh-installation
`run_additional_generators!': uninitialized constant Refinery::AuthenticationGenerator (NameError)

I've issued a pull request to RefineryCMS which can be found here: View Here

bundle open refinerycms-core

Comment out the AuthenticationGenerator (Line 204). and run the command again. bundle open refinerycms-core

Emulate Refinery User Model

Take some time to look over the Refinery authentication User models View Here.

We will need to integrate this model with the model we generated with devise. Update your user model to look like the following.

class User < ActiveRecord::Base
  has_and_belongs_to_many :roles, :join_table => :roles_users
  has_many :plugins, :class_name => "UserPlugin", :order => "position ASC", :dependent => :destroy

  # Include default devise modules. Others available are:
  # :token_authenticatable, :confirmable,
  # :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  # Setup accessible (or protected) attributes for your model
  attr_accessible :email, :password, :password_confirmation, :remember_me, :plugins
  # attr_accessible :title, :body

  def plugins=(plugin_names)
    if persisted? # don't add plugins when the user_id is nil.
      UserPlugin.delete_all(:user_id => id)
      plugin_names.each_with_index do |plugin_name, index|
        plugins.create(:name => plugin_name, :position => index) if plugin_name.is_a?(String)
      end
    end
  end

  def authorized_plugins
    plugins.collect(&:name) | ::Refinery::Plugins.always_allowed.names
  end

  def can_delete?(user_to_delete = self)
    user_to_delete.persisted? &&
      !user_to_delete.has_role?(:superuser) &&
      ::Role[:refinery].users.any? &&
      id != user_to_delete.id
  end

  def can_edit?(user_to_edit = self)
    user_to_edit.persisted? && (
      user_to_edit == self ||
      self.has_role?(:superuser)
    )
  end

  def add_role(title)
    raise ArgumentException, "Role should be the title of the role not a role object." if title.is_a?(::Role)
    roles << ::Role[title] unless has_role?(title)
  end

  def has_role?(title)
    raise ArgumentException, "Role should be the title of the role not a role object." if title.is_a?(::Role)
    roles.any?{|r| r.title == title.to_s.camelize}
  end

  def create_first
    if valid?
      # first we need to save user
      save
      # add refinery role
      add_role(:refinery)
      # add superuser role
      add_role(:superuser) if ::Role[:refinery].users.count == 1
      # add plugins
      self.plugins = Refinery::Plugins.registered.in_menu.names
    end

    # return true/false based on validations
    valid?
  end

end

Refinery expects our user model to have certain methods. The best way to understand what Refinery expects is to look at their User model. Refinery User Model 2.0 Stable. Please take note of the relations that they specify.

has_and_belongs_to_many :roles, :join_table => :refinery_roles_users
has_many :plugins, :class_name => "UserPlugin", :order => "position ASC", :dependent => :destroy

This means we will need to add three new Models. Role, RoleUser and UserPlugin.

Add the Role Model

You can view the refinery role model Here

class Role < ActiveRecord::Base

  has_and_belongs_to_many :users, :join_table => :roles_users

  before_validation :camelize_title
  validates :title, :uniqueness => true

  def camelize_title(role_title = self.title)
    self.title = role_title.to_s.camelize
  end 

  def self.[](title)
    find_or_create_by_title(title.to_s.camelize)
  end 

end

Add the RoleUser Model

You can view the refinery Role User model Here

class RoleUser < ActiveRecord::Base

  belongs_to :role
  belongs_to :user

end

Add the UserPlugin Model

You can view the refinery User Plugin model Here

class UserPlugin < ActiveRecord::Base

  belongs_to :user
  attr_accessible :user_id, :name, :position

end

Add Migrations for these three new models

You can view the refinery Migrations Here ```ruby rails g migration CreateRefinerycmsAuthenticationSchema ``` ```ruby class CreateRefinerycmsAuthenticationSchema < ActiveRecord::Migration def change # Postgres apparently requires the roles_users table to exist before creating the roles table. create_table :roles_users, :id => false do |t| t.integer :user_id t.integer :role_id end add_index :roles_users, [:role_id, :user_id] add_index :roles_users, [:user_id, :role_id] create_table :roles do |t| t.string :title end create_table :user_plugins do |t| t.integer :user_id t.string :name t.integer :position end add_index :user_plugins, :name add_index :user_plugins, [:user_id, :name], :unique => true end end ```

Override and Initialize

We're almost there! We just need to override methods that refinery uses for authentication (That live outside the authentication engine). There are the main files that contain methods we need to override.

  • core/lib/refinery/application_controller.rb
  • authentication/lib/refinery/authenticated_system.rb
  • authentication/app/models/refinery/user.rb

Add the following to config/initializers/refinery/user.rb

::Refinery::ApplicationController.module_eval do

  def self.included(base) # Extend controller
    base.helper_method :home_page?, :local_request?, :just_installed?,
                       :from_dialog?, :admin?, :login?, :current_refinery_user, :current_user_session,
                       :refinery_user_signed_in?, :refinery_user?

    base.protect_from_forgery # See ActionController::RequestForgeryProtection

    base.send :include, Refinery::Crud # basic create, read, update and delete methods

    if Refinery::Core.rescue_not_found
      base.rescue_from ActiveRecord::RecordNotFound,
                       ::AbstractController::ActionNotFound,
                       ActionView::MissingTemplate,
                       :with => :error_404
    end
  end

  def refinery_user_required?
    if just_installed? and controller_name != 'users'
      redirect_to main_app.new_user_registration_path
    end
  end

  # This just defines the devise method for after sign in to support
  # extension namespace isolation...
  def after_sign_in_path_for(resource_or_scope)
    scope = Devise::Mapping.find_scope!(resource_or_scope)
    home_path = "#{scope}_root_path"
    respond_to?(home_path, true) ? refinery.send(home_path) : refinery.admin_root_path
  end

  def after_sign_out_path_for(resource_or_scope)
    refinery.root_path
  end

  def refinery_user_signed_in?
    user_signed_in?
  end

  def authenticate_refinery_user!
    authenticate_user!
  end

  def current_refinery_user
    current_user
  end

    def admin?
      %r{^admin/} === controller_name
    end

    def error_404(exception=nil)
      # fallback to the default 404.html page.
      file = Rails.root.join 'public', '404.html'
      file = Refinery.roots(:'refinery/core').join('public', '404.html') unless file.exist?
      render :file => file.cleanpath.to_s.gsub(%r{#{file.extname}$}, ''),
             :layout => false, :status => 404, :formats => [:html]
      return false
    end

    def from_dialog?
      params[:dialog] == 'true' or params[:modal] == 'true'
    end

    def home_page?
      %r{^#{Regexp.escape(request.path)}} === refinery.root_path
    end

    def just_installed?
      false #Disable redirecting to Authentication engine
    end

    def local_request?
      Rails.env.development? || /(::1)|(127.0.0.1)|((192.168).*)/ === request.remote_ip
    end

    def login?
      (/^(user|session)(|s)/ === controller_name && !admin?) || just_installed?
    end

  protected
    def refinery_user?
      refinery_user_signed_in? && current_refinery_user.has_role?(:refinery)
    end

   # Redirect to the URI stored by the most recent store_location call or
    # to the passed default.
    def redirect_back_or_default(default)
      redirect_to(session[:return_to] || default)
      session[:return_to] = nil
    end

    def store_location
      session[:return_to] = request.fullpath.sub("//", "/")
    end

    # use a different model for the meta information.
    def present(model)
      @meta = presenter_for(model).new(model)
    end

end

Override refinery site_bar

We need to first override the refinery site bar and then rename refinery.destroy_refinery_user_session_path to main_app.destroy_user_session_path

rake refinery:override view=refinery/_site_bar*

Edit app/views/refinery/_site_bar.html.erb.

<%= link_to t('.log_out', site_bar_translate_locale_args), main_app.destroy_user_session_path, :id => 'logout', :method => :delete %>

Create User Roles

Refinery by default uses two user roles. These roles are Refinery and Superuser.

#Create User Roles
Role.create(:title => 'Refinery')
Role.create(:title => 'Superuser')
#Assign user roles
user = User.first
user.roles << Role.find_by_title('Refinery')
user.roles << Role.find_by_title('Superuser')
user.save

Known Issues

  1. Redirect loop when no user exists that has the refinery role. Temporary solution is creating a user with the Refinery Role.
  2. If you ran rails g refinery:cms --fresh-installation and get the following error then edit core/lib/generators/refinery/cms/cms_generator.rb on line 204. Run 'bundle open refinerycms-core'. Comment out the AuthenticationGenerator and run the command again. Special thanks to @parndt!

Testing

I would HIGHLY recommend writting tests for the above code. The good news is the guys at refinerycms have done most of the work for you! You can look at the available tests for authentication Here!

Download The Example

You can download a working example of this blog post Here!

Resources

Thanks to the fantastic developers working on Refinery CMS! I would also like to source this article for giving the inspiration to write this one!


comments powered by Disqus