Overview

A simple to use Rails-Engine-Gem that offers an admin interface for trusted user.
Easy integratable and highly configurable and agnostic.
Works with ActiveRecord and Mongoid.


Install

  1. Make sure you have rails >= 4.0.0
  2. Add this to your Gemfile
    gem 'bhf'
  3. Add a bhf.rb to your config/initializers/ dir
    Bhf.configure do |config|
      config.on_login_fail = :login_url
    end

    (example)

  4. Mount the bhf-engine in your routes.rb
    mount Bhf::Engine, at: 'bhf'

    (example)

  5. Add a bhf.yml to your config/ dir
    pages:
    - my_page: # Page name, not really important for now
      - my_model: # replace this with whatever model name you already have in this project (lowcase)

    (example)

That's it!


Setup user authentication

Now http://localhost:3000/bhf is available, but you will be redirected back to the config.on_login_fail url. Because bhf doesn't trust you yet.

User authentication happens completely outside of bhf, this gives you the maximal flexibility to authentication process. The authlogic, device or the good-old http_basic_authenticate_with or anything else.

Let's keep it simple and use http authenticate, go to your routes.rb and add this line:

get 'admin', to: 'admin#login', as: :admin

(example)

create a admin_controller.rb and make sure users that are allowed to open bhf get the session[:is_admin] = true before they are redirected to bhf.root_url.

class AdminController < ApplicationController

  http_basic_authenticate_with name: 'admin', password: 'bhf'
  # somebody how doesn't know the login data can't open the login action

  def login
    # bhf checks for this session in a before_filter, if it's true user will pass
    session[:is_admin] = true
    redirect_to bhf.root_url
  end

end

(more advanced example with authlogic)

You can change the session name in the config/initializers/bhf.rb. Because you are in control of when this session variable is set, you are in control what authentication method you are using.

Now hit http://localhost:3000/bhf again and login!

Along with the standard approach in authentication, instead of defining the #login action in the Admin controller, you are able to redefine Bhf::ApplicationController, and its #check_admin_account method, to give to bhf the knowledge about weither current user is admin or not. Do it like follows:

class Bhf::ApplicationController < ActionController::Base
  protect_from_forgery
  before_filter :authenticate_user! # if devise authentication is used
  include Bhf::Extension::ApplicationController

  def check_admin_account
    # Here expression must be evaluated to `true` if user is admin.
    current_user.is_admin?
  end
end

Usage

After logging in you see the list of pages form bhf.yml, click one of them and you'll see the list of models (called platforms) this page contains. As you see platforms need only little configuration appear on a page and be ready to use. But of course we often need some basic business logic for example: hiding fields, adding custom fields, removing the delete button or using a curtain scope for the list of entries. All of this and much more can be setup via bhf.yml. This is how a typical bhf.yml looks like:

pages:

- statics: # Page Statics
  - statics: # bhf can work from this point on because there is a model named Static

- posts: # Page Posts
  - posts:
      table:
        # default_scope hides some posts so we need a different scope
        source: all_posts

        # columns to display
        display: [id, headline, subheadline, content, category, published, published_at]
      form:
        # fields to display
        display: [id, category, author, headline, subheadline, content, published, published_at]
        types:
          # change content field from a textarea to a markdown wysiwyg
          content: markdown

- settings: # Page Settings
  - category: # bhf can work from this point on because there is a model named Category

  - authors:
      table:
        # allow to sort the entries via drag and drop (make sure the default_scope order by position)
        sortable: position
        # hide the delete button
        hide_delete: true
      form:
        # fields to display
        display: [id, name, job_title, email]

Platform options

This are the keys you can add to a platform model, table, form, show, sortable_property, hooks, extend_abstract

Table

table: (hash) contains all the logic that you can see in the table view. This are the possible hash options:

  • scope: (string/array) name of the scope (read more about Rails scopes). default: all (source: is an alias)
  • user_scope: (string) name of the scope that will be applied on the user instance and not directly on the platform's model. default: false
  • display: (array) a list that will define the model instance getter and the order of the table rows. default: id + 5 more random keys. (columns: is an alias)
  • exclude: (array) a list of model instance getters that will not be shown (the opposite of display:). default: false
  • types: (hash) key is the model instance getter or a value defined in the display: array. value is the name of the variable type or a custom partial name. default: {} (example)
  • show_duplicate: (boolean) shows the duplicate entry button. default: false
  • sortable: (boolean) show the sort icon (drag and drop). default: false
  • hide_delete: (boolean) hides the delete button. default: false
  • hide_edit: (boolean) hides the edit possibilities in the table button. default: false
  • hide_create: (boolean) hides the create button. default: false
  • search: (string/boolean) name of the search method that will be used instead of bhf_default_search, if set to false it will remove the platform search form. default: nil
  • search_field: (boolean) show the search field. default: true
  • custom_search: (string) partial name/path used inside of the search form. default: false
  • custom_footer: (string) partial name/path used in the platform footer, this will not remove the pagination. default: false
  • entries_per_page: (int) number of entries per displayed on a page. default: false
  • custom_partial: (string) partial name that will be used instead of the standard table platform partial. default: false
  • quick_edit: (boolean) enables quick edit in the table view. default: false
  • custom_link: (string) changes the link of the entry. default: false
  • hide: (boolean) hides the platform. default: false

Form

form: (hash) contains all the logic that you can see in the form view. This are the possible hash options:

  • display: (array) a list that will define the model instance getter and the order of the table rows. default: all model fields.
  • exclude: (array) same as table.exclude:
  • types: (hash) same as table.types:. See form field types for the available types or define your own.
  • links: (hash) key is the model instance getter or a value defined in the display: array. value is the name of the reflection platform linked to this key. Having false in the value will hide the link, because sometimes bhf is able to find reflection platform without your configuration. default: {}
  • multipart: (boolean) form is multipart. default: false

Show

show: (hash) contains all the logic that you can see in the show view. But first you need to link a entry to the show view.

pages:
- page:
  - authors:
      custom_link: entry_path

This are the possible hash options:

  • show_extra_fields: (array) appends custom fields at the end of the view. default: false
  • display: (array) a list that will define the model instance getter and the order of the table rows. default: all model fields. (definitions: is an alias)
  • exclude: (array) same as table.exclude:
  • types: (hash) same as table.types:

Model

model: (string) actual model name (e.g. User) it's often not needed because bhf tries to find the model via the platform name, but sometimes the have to be different.

pages:
- page:
  - best_authors:
      model: authors

Sortable property

sortable_property: (string) name of the property that needs to get updated via ajax on entries sort. default: position

Hooks

hooks: (hash) contains the hooks that will be used while saving the entry

  • after_load: (string) method name that is called on a entry instance right after was loaded. default: false
  • before_save: (string) method name that is call before saving entry instance with the Rails params hash. default: false
  • after_save: (string) method name that is call after saving entry instance with the Rails params hash. default: false

There are some ActiveRecord hooks that do the same but hey will not let you access the http params.

Abstract extend

extend_abstract: (string) name of the abstract platform. Read more about abstract platform settings.


Initializer configuration

Bhf.configure do |config|
end

This are the possible hash options:

  • config.css: (array) all the css file links. default: ['bhf/application']
  • config.js: (array) all the js file links. default: ['bhf/application']
  • config.abstract_settings: (array) link to abstract .yml files. default: []
  • config.on_login_fail: (symbol) url bhf redirects to if session[config.session_auth_name] isn't true default: :root_url
  • config.session_auth_name: (symbol) session key used to look up in user's session whether it's true or not default: :is_admin
  • config.session_account_id: (symbol) session key used to look up in user's id which will be passed to the config.account_model_find_method method default: :admin_account_id
  • config.account_model: (string) user's model name default: 'User'
  • config.account_model_find_method: (string) user's model method used to find the current user instance default: 'find'
  • config.logout_path: (symbol) link used in the footer if the user was found via config.account_model and config.account_model_find_method default: :logout_path
  • config.paperclip_image_types: (array) default: ['image/jpeg', 'image/pjpeg', 'image/jpg', 'image/png', 'image/tif', 'image/gif']

(example)


User roles

If you wish to display different pages or/and platforms to your users you can do that with roles support that is backed into bhf.

First of all you will have to provide a session[:admin_account_id] = @user_session.user.id. After finding the user bhf will try to retrieve user's roles. If the getter bhf_roles is defined bhf loops through the roles and collects "bhf/#{role.identifier}.yml" settings. So you no longer have the config/bhf.yml, instead you will have something like this: config/bhf/admin.yml, config/bhf/editor.yml. If you have same page names in each roles settings file defined, bhf will merge them. You can't define same names for platforms. If you need to reuse the settings form a platform consider using abstract settings.

schema.rb

create_table "roles", force: true do |t|
  t.string   "identifier"
  t.string   "name"
  t.datetime "created_at",             null: false
  t.datetime "updated_at",             null: false
end
create_table "roles_users", id: false, force: true do |t|
  t.integer "role_id"
  t.integer "user_id"
end

user.rb

class User < ActiveRecord::Base
  ...
  has_and_belongs_to_many :roles
  ...
  def bhf_roles
    roles
  end
end

Possible user model instance methods:

  • bhf_roles: returns: (array) a list of roles objects with at least a identifier key.

Abstract platform settings

Reusing platform settings is simple. Add config.abstract_settings = ['abstract'] to your bhf initializer and add config/bhf/abstract.yml. Define the abstract platform settings the way you normal would, all the pages and platforms wont be displayed. Use the extend_abstract: platform's setting to link the platform to an abstract platform.

Example of a abstract settings .yml file and how it can be used.


Overriding bhf

Field types

Table:

Available types for model columns:

  • array (string)
  • boolean (string)
  • date (string)
  • extern_link (link to string)
  • toggle (boolean) provides a link for an instant change of the boolean value
  • file (link to file)
  • hash (string)
  • image (image_tag)
  • number (string)
  • paperclip (file name string or image_tag)
  • carrierwave (image_tag)
  • type (string)
  • primary_key (strong_tag)
  • string (string)
  • text (string)

Available types for belongs_to, has_many, has_one, embeds_many,
embeds_one and has_and_belongs_to_many:

  • default (string)

Form:

Available types for model columns:

  • array (multiple text_fields)
  • boolean (check_box)
  • date (date- and/or time-picker)
  • hash (string)
  • mappin (google-maps map with a pin)
  • markdown (markdown editor)
  • multiple_fields (multiple text_fields)
  • number (text_field)
  • paperclip (file_field)
  • password (password_field)
  • static (string)
  • string (text_field)
  • type (select_tag)
  • image_file (file_filed with a image preview)
  • text (text_area)
  • wysiwyg (wysiwyg html editor)

Available types for reflection belongs_to:

  • select (select_tag)
  • radio (radio_button)
  • static (string)

Available types for has_and_belongs_to_many and has_many :through =>:

  • check_box (check_box)
  • static (string)

Available types for has_many, has_one, embeds_many and embeds_one:

  • static (string)

This a example of how to set up a markdown editor.

Replacing the “bhf” logo with your own

Add a logo_bhf.png or logo_bhf.svg file to your app/assets/images/ directory.

Models

bhf includes the following methods into ActiveRecord::Base and Mongoid::Document

Instance methods:

  • to_bhf_s returns: (string) Finds a good looking title for a instance, e.g.: Post.first.to_bhf_s #=> 'Koala bears are happy again' # title attribute found or Roster.first.to_bhf_s # => 'Roster ID: 33' # title attribute not found override this method in your models when needed.

Possible instance methods:

  • before_bhf_duplicate(old_object): returns: (doesn't matter) Is called right before the duplicated of an entry happens. This method is called on the new instance and sends the old instance as an argument.
  • after_bhf_duplicate(old_object): returns: (doesn't matter) Same as before_bhf_duplicate(old_object) only that it's called after save on the new instance.
  • bhf_can_edit?: returns: (boolean) Finds out whether the shortcut edit link can be displayed for the current user. Called inside the bhf_edit view helper method.
  • to_bhf_hash returns: (hash) Hash that gets merged with the default hash of the model attributes after a quick edit successfully returns a json which will be used to update the html.

Class methods:

  • bhf_default_search(search_params) returns: (ActiveRecord::Relation) This method is used to get search results for the search form. Override this method to perform better searches with your business logic.
  • bhf_attribute_method?(column_name) returns: (boolean) Checks if column_name is a column.
  • bhf_primary_key returns: (string) Key name of the primary key attribute, relevant for mongoid.
  • bhf_embedded? returns: (boolean) Is this instance embedded, relevant for mongoid.

Views

bhf is a Rails-Engine this means you can use hierarchical overrides, simple include a view in your app in the same directory and bhf will render this view instead of it's own. Check out bhf's views directory and this example where the footer partial is overridden. You can also override the layout or use it in your own controllers (e.g. for login).

It's very easy to add new custom views and assets or to overwrite the existing ones with your own.

Adding view files

Notice the demo_checkbox string inside the display: array:

pages:
- posts: # Page Posts
  - posts:
      form:
        display: [id, name, demo_checkbox]

Add a partial to views/bhf/form/column/_demo_checkbox.haml or .erb (not _demo_checkbox.html.haml)

Use the node view helper to create a label and a div holder for your form elements

= node f, field do
  = f.hidden_field :test
  = field.name

Adding stylesheet files

include this line to your config/initializers/bhf.rb

config.css << 'my_custom_bhf'

add this file to your asset pipeline directory and add it to your config.assets.precompile array

That's it, you can now work on your css to overwrite bhf default styling or add new styling for your custom views.

Adding javascript files

include this line to your config/initializers/bhf.rb

config.js << 'my_custom_bhf'

add this file to your asset pipeline directory and add it to your config.assets.precompile array

Always use the 'bhfDomChunkReady' not just 'domready'. Never try to grab DOM elements from the document directly, use scope as your element scope. Often times only small parts of the DOM get updated via ajax. bhf uses the mootools javascript framework, if you need jQuery feel free to require it via //= require jquery. Also bhf uses Turbolinks, be aware of that.

window.addEvent('bhfDomChunkReady', function(scope){
  console.log(scope);
});
Examples

Look at this examples to get a better understanding: Javascript, View)

Controllers

bhf is not designed to be flexible when it comes to controller logic. While for saving the data to your model you can use simple, yet powerful hooks: hash settings in the .yml file for some additional logic you will have to do everything on your own. Here is an example of how you could inject your own controller.

Make sure you have a stadium and a tournaments platform defined in your settings .yml.

Add this code to the top of your routes.rb:

Bhf::Engine.routes.draw do
  patch 'stadium/entries/:id', to: 'tournaments#stadium_update', defaults: {platform: 'stadium'}
  get 'tournaments/entries/new', to: 'tournaments#new', defaults: {platform: 'tournaments'}
end

Add a app/controllers/bhf/tournament_controller.rb file:

class Bhf::TournamentsController < Bhf::EntriesController
  def new
    @object.save
    redirect_to edit_entry_path(@platform.name, @object)
  end
  def stadium_update
    ...
  end
end

Look at the Bhf::EntriesController file and try to reuse as much as possible in your controller.


Internationalization

bhf right now offers I18n support for those languages, you can easily add more languages translating this yaml file and this javascript file (please submit it also as a fork if you have added a language). Make sure you have a fallback included in application.rb if you don't use en as your main language. For the translation of the model attributes, bhf heavily relies upon build-in Rails I18n model API.

config.i18n.default_locale = :de
config.i18n.fallbacks = [:en]

Time zones

Make sure a default time_zone is set in application.rb otherwise you are saving dates in GMT.

config.time_zone = 'Berlin'

Read about why this is so important.


Understanding quick_edit

quick_edit is a sidebar view that contains a form to create or edit a entry. E.g. if you see a <select> element for a belongs_to relation you are also presented with buttons to edit the current selected entry or to create a new one, without leaving the current form. This happens if bhf manages to link the relations name to a platform.

Check out the quick_edit features in this demo app (username: "admin" password: "bhf").

If the platform exists and you still don't see the quick_edit features setup a links: hash in your .yml settings.

pages:
- settings:
  - authors:
      form:
        display: [_id, name, job_title, email, categories]
        links:
          categories: best_categories
  - best_categories:
      model: Category

Areas

Areas divide the tabs you can see at the top into different page parts. Having multiple areas is useful if you want to have several totally different navigations and/or platforms in one bhf project. Areas behave a little like roles, if bhf finds a getter bhf_areas on the user it will loop through the areas.

routes.rb:

mount Bhf::Engine, at: 'bhf/:bhf_area', as: :bhf, defaults: { bhf_area: 'main' }
get 'bhf', to: 'application#redirect_to_users_bhf_area', as: :bhf_help

schema.rb:

create_table "areas", force: true do |t|
  t.string   "identifier"
  t.string   "name"
  # link to your main app e.g.: 
  # this area is about oranges and your site has a url http://myfruits.com/oranges ,
  # a good link would be '/oranges'
  t.string   "link"
  t.datetime "created_at",             null: false
  t.datetime "updated_at",             null: false
end
create_table "areas_users", id: false, force: true do |t|
  t.integer "area_id"
  t.integer "user_id"
end

user.rb:

class User < ActiveRecord::Base
  ...
  has_and_belongs_to_many :areas
  ...
  def bhf_areas
    areas
    # You also can use OpenStruct here if you want to avoid an Area model
    # [
    #   OpenStruct.new(identifier: 'main'), 
    #  (OpenStruct.new(identifier: 'admin') if roles.include?(Role.find(1)))
    # ]
  end
end

application_controller.rb:

class ApplicationController < ActionController::Base
  # this simply makes sure you still can use /bhf as an url
  def redirect_to_users_bhf_area
    r = if current_user
      current_user.bhf_areas.first.identifier
    else
      'main'
    end
    redirect_to bhf.root_url(bhf_area: r)
  end
end

Possible user model instance methods:

  • bhf_areas: returns: (array) a list of area objects with at least a identifier key.
  • bhf_area_roles(current_area): returns: (array) a helper to remove roles for current_area. Return a array of roles objects that you want to use in this area.

Plugins

It's possible to write your own plugins for bhf. Here is an example of how to do it. If you have multiple projects running with bhf, writing a plugin to keep your code DRY makes great sens. Also please let me know about your plugin, so I can list it here.

List of available bhf plugins:

  • ksk used for blog/website management

Support

My name is Anton Pawlik, @antpaw, anton.pawlik@gmail.com.
Feel free to ask questions, contact me for support or donate to the project.

Click here to lend your support to: bhf and make a donation at pledgie.com !