Basepack


Project maintained by lksv Hosted on GitHub Pages — Theme by mattgraham

Introduction

This tutorial is designed for learning basepack which is based on Ruby on Rails framework. So previous experience with Ruby on Rails is necessary but you do not need to be an expert. There is the README for shorter introduction of basepack and there is a demo application. But in order to learn basepack you would need to build such application yourself and that is exactly what this tutorial will walk you through.

Basepack is a collection of gems (which are useful in any application) tied together with additional features on top of it. Installing basepack's gem really enhances the power of Ruby on Rails. And remember you can still use everything what development in Ruby on Rails provides. For example use basepack's features only in some parts of application.

For easy start it is recommended that you are familiar with gems below (you have used them at least in one application) because these gems are essential part of basepack. Although there is no need to worry if you do not know them yet. You will be supplied with additional links to related sections during all tutorial.

And being familiar with these may also help:

The basepack requires rails 4 and ruby 2 or higher, these versions were used in this tutorial.

rails -v
ruby -v

Rails 4.0.0
ruby 2.0.0p247 (2013-06-27 revision 41674) [x86_64-linux]

First application with basepack

Without further ado let's jump in and start building issue tracking application. Create new rails application in your console.

rails new basepack_tutorial
cd basepack_tutorial

Add basepack gem to the application and run bundle to install it with all its dependancies.

Gemfile:

gem 'basepack'
bundle install

This will create a new basepack generator for us let's use it right away.

rails g basepack:install

The basepack generator notices the absence of cancan and devise files and offers you running generator for them as well.

Then it asks for confirmation to remove application.html.erb in order to use own application.html.haml with related partials (header, navigation, etc.). You can see how this basepack's template looks like in demo application. You can without fear type yes to confirm all suggestions.

Here is the summary of what generator did:

Last step in installation is to define inital ability in app/models/ability.rb. For now you can allow anybody to perform any action and then migrate database.

app/models/ability.rb:

can :manage, :all
rake db:migrate

Everything is set up and ready to be used but it has no data to work with. So our application needs to track projects, which will have a lot of issues (has_many). Use scaffolding to create these resources. Functionality of scaffolding is now modified by basepack.

rails g scaffold Project name description:text start:date finish:date
rails g scaffold Issue name description:text status:string resolution:string project:references user:references

Then migrate the database and run server.

rake db:migrate
rails s

In comparision with basic scaffolding it includes importing and exporting of resources. Records in index action are paginated by default. You can perform complex searching and save frequent searches as filters or sort by individual columns. It displays breadcrumb (links with path e.g. Users / Projects / firs_project) and many more.

After scaffolding your resources, you can customize fields used in individual actions by Railsdmin DSL. This domain specific language allows you to quickly customize how your views look like. Take a time and really browse through its configuring possibilities (visibility, ordering, labels, formatting, ...).

app/models/project.rb:

class Project < ActiveRecord::Base
  has_many :issues, inverse_of: :project

  rails_admin do
    list do
      # only these fields(name, finish) will be displayed in this order
      field :name
      field :finish
    end

    edit do
      field :name
      # add rich text editor as a field type
      field :description, :wysihtml5
      field :start
      field :finish
    end 

    show do
      field :name
      field :description
      field :start
      field :finish
    end 
  end
end

app/models/issue.rb:


  class Issue < ActiveRecord::Base
    belongs_to :project, inverse_of: :issues
  end
  

Make sure that you define inverse_of option on has_one, has_many and belongs_to associations. It is necessary for correct functioning of basepack, see Rails documentation for explaination.

Notice that files for views are not generated (directories appp/views/projects and appp/views/issues are empty), but all RESTful actions(index, show, new, create, edit, update, destroy) are working correctly. It is because views iherit default look and you can easily override these defaults by creating appropriate files.

Another difference is controllers which inherit from ResourcesController. Full inheritance hierarchy looks this way:

ProjectsController < ResourcesController < Basepack::BaseController < InheritedResources::Base

If you are not familiar with InheritedResources, now is a time to take a look at it.

Basepack::BaseController adds to inherited resources:

It will be disscussed later in tutorial except defining strong parameters. You just neeed to know that you don't have to define permitted parameters anymore. It is defined by RailsAdmin DSL, more precisely by what you set as visible in edit action.

app/models/project.rb:

class Project < ActiveRecord::Base
  ...
    edit do
      field :name
      field :description, :wysihtml5
      field :start
      field :finish
     end 
  ...
end

This implicitly sets permitted params which could be written as follows:

def permitted_params
  params.permit(:project => [:name, :description, :start, :finish])
end

You can override these implicit settings by creating this method in case you want it.

Nested Resource

As you can read in RailsGuides nested resource is a way how to express parent-children relationship in routes. In our application it make sense to nest issues to projects because they are logical children of projects.

config/routes.rb:

resources :projects, concerns: :resourcable do
  resources :issues, concerns: :resourcable
end

You can use belongs_to to reflect this relationship in controller. And it ensures that:

For example when an user enters url /projects/1/issues/2. In that case issue with id 2 must belong to project with id 1. And it checks user permissions for project with id 1 because it could be a security vulnerability if user could read issues on project he has no permitted access.

Option optional allows to display issues independantly of parent resource.

app/controllers/issues_controller.rb:

class IssuesController < ResourcesController
  belongs_to :project, optional: true
end

There is one more thing related to nested resource. When you are creating new project you may also want to create new issues. You need to explicitly allow this with accepts_nested_attributes_for method.

app/models/project.rb:

class Project < ActiveRecord::Base
  accepts_nested_attributes_for :issues
  ...
    edit do
      field :name
      field :description, :wysihtml5
      field :start
      field :finish
      field :issues # display nested resource
     end 
  ...
end

Project show page

Suppose I want to see on project show page all issues associated with that project. To do that you can customize default show page by creating appropriate view file.

You can use @project instance variable to output project's information or better render partials which will display project as it was configured in rails_admin's show block.

app/views/projects/show.html.erb:

<%= render 'header' %>
<%= render 'show' %>

Usually you want to display content the way it is configerd in rails_admin sections (e.g. show, list, edit, ...). There are defined helper methods for some of these sections which will use configuration from rails_admin block.

edit_form_for(resource_or_chain, options = {})
show_form_for(resource_or_chain)
list_form_for(query_form)
query_form_for(class_or_chain, scope, options = {})
...

These methods return content of section for specific resource let's call it instnce of form. Actually it is a instance of Basepack::Forms class.

Resource chain is a sequence (array) of resources in case of nested resources. You can see this sequence in a breadcrumb and you have also access to association_chain which is an empty array in project's show page. But if projects were nested resource for users e.g. url organization/3/users/2/projects/1 than association_chain would contain third organization object and second user.

In your views, you will get the following helpers:

resource                       #=> @project
collection                     #=> @projects (with pagination)
collection_without_pagination  #=> @projects (without pagination)
resource_class                 #=> Project
association_chain              #=> array of resources

As you might expect collection and collection_without_pagination (or @projects instance variable) is available only on the index action.

That means you can use show_form_for to get the same content as in show page of every issue in current project. For displaying you have to use form_render because render does not know how to render instances of basepack forms (in this case Basepack::Forms::Show object).

app/views/projects/show.html.erb:


<%= render 'header' %>
<%= render 'show' %>

<h3>Issues</h3>
<% resource.issues.each do |issue| %>
  <%= form_render show_form_for(issue) %>
<% end %>

Furthermore these helpers for retrieving content of forms have pair variant without _for suffix which already contains retrieved content in appropriate actions. For example in projects show page show_form is equivalent to show_form_for(resource).


edit_form
show_form
list_form
query_for
...

Default query

Imagine that there were many (hundred) projects especially these which already ended and every time you enter project index page you see these passed projects and you have to search within them. It would be better to change default query which is currently just display all projects in order as they are in database (most of the time it is order in which they were created).

And if you are familiar with ransack it is really simple to change this default query. You just need to call method default_query_params in appropriate controller, which returns a hash of parameters for search. Basepack uses ransack under hood, but you have to use f as a default param key for search (instead of q) and for searching use f[s].

app/controllers/projects_controller.rb:

class ProjectsController < ResourcesController
  def default_query_params do
    { 
      # display projects which haven't ended yet (end today or later)
      "f[end_gteq_null]" => Date.today,
      # sorted by start of project 
      "f[s]" => 'start asc'
    }

end end

In case you have not worked with ransack you can watch railscast episode by Ryan Bates for great introduction.

Tagged issues

In this chapter you will give user possibility to add tags to issues with acts-as-taggable-on gem so first step is straightforward.

Gemfile:

gem "acts-as-taggable-on"
  

Then follow post installation instructions and create migration by generator and migrate the database:

rails g acts_as_taggable_on:migration
rake db:migrate

The created model is located in different namespace which means basepack will not detect it automatically and you have to include it manually in the rails_admin configuration file.

config/initializers/rails_admin.rb:

config.included_models = Basepack::Utils.detect_models + ['ActsAsTaggableOn::Tag']

Mark issues model as taggabale with acts_as_taggable which will provide new methods for working with tags as specified in usage section.

Especially interesting is method tag_list= for setting tags and tag_list for retrieving tags. It is enough just to display tag_list field and you are done. Actually almost done because associations added by acts_as_taggable to make it work are also displayed as fields (Tags, Base list) so you can exclude them at beginning.

app/models/issues.rb

class Issue < ActiveRecord::Base
  ...
  acts_as_taggable

  rails_admin do
    exclude_fields :base_tags, :tags

    # display tags in show actions
    show do
      include_fields :tag_list
    end

    edit do
      # tags entered in comma separated syntax
      field :tag_list
    end

  end
end

To make it even better render the tag_list field with basepack's partial which uses select2 and provides autocompletion by making ajax calls to taggings method of particular controller (IssuesController in this case)

app/models/issues.rb

    edit do
      # tags entered in comma separated syntax
      field :tag_list
    end
  

If you want you can enable actions (new, edit, show, ...) for ActsAsTaggableOn::Tag model just add routes for these actions.

routes.rb

  resources :acts_as_taggable_on_tags, :filters, concerns: :resourcable

And create controller to properly handle all routes.

app/controllers/acts_as_taggable_on_tags_controller.rb

class ActsAsTaggableOnTagsController < ResourcesController
  # set default model (Tag is in different namespace) see https://github.com/josevalim/inherited_resources#overwriting-defaults
  defaults resource_class: ActsAsTaggableOn::Tag
end

Dynamically showed and hidden attributes

Sometimes you need to hide or display field depending on the entered value of other field in new / edit form. When you take a look at this table of issue types you will see that it is exactly what you need right now. Because resolution can be added only to closed bugs (with resolved or verified status) as you can see in this table.

Status

Resolution

Open bugs
Unconfirmed
New
Assigned
Reopened
Ready
No resolution yet.
Closed bugs
Resolved
Verified
Fixed
Invalid
Wontfix
Duplicate
Worksforme
Incomplete

First of all, you need to display status and resoultion field as a select box to limit possible values. You can use enum field type and than define model method with array of values which will be displayed as select box options. Check out rails_admin's :enum field type wiki page for more.

app/models/issues

class Issue < ActiveRecord::Base
...
  rails_admin do
    # show ...
    # list ...

    edit do
      field :name
      field :description
      field :project
      # set type of status and resolution to enem
      field :status, :enum
      field :resolution, :enum
    end
  end

# method for retrieving potencial values for status field
  def status_enum
    ["Unconfirmed", "New", "Assigned", "Reopened", "Ready", "Resolved", "Verified"]
  end

  # method for retrieving potencial values for resolution field
  def resolution_enum
    ["Fixed", "Invalid", "Wontfix", "Worksforme", "Incomplete"]
  end
end

As table shows resolution needs to be displayed only when status is either "Resolved" or "Verified". In all other cases resolution should be hidden. Thanks to basepack you can easily do this.

app/models/issues.rb

  field :status, :enum do
    html_attributes do
    { 
      data: { 
        "dynamic-fields" => [
          { condition: ["Unconfirmed", "New", "Assigned", "Reopened", "Ready"], field_actions: { resolution: { visible: false }} },
          { condition: ["Resolved", "Verified"], field_actions: { resolution: { visible: true  }} },
        ]   
      }   
    }   
    end 
  end

Let's explain it step by step. You can add html attributes (like class, id, etc.) in configuration of any field through html_attributes. Data hash is used for custom data attributes for many purposes like marking ajax requests or date picker in Rails. Basepack uses it too. For example for dynamicly showed / hidden attributes, dependant select boxes and some field types e.g. wysihtml5.

  html_attributes do
    { 
      class: "my_css_class",
      data: { 
        # this will add data-name attribute to field 
        "name" => "John",

        # this will add data-city attribute to field 
        "city" => "Brno"
      }  
  end

The only missing part is "dynamic-fields" attribute which will be processed by basepack. It contains list (array) of conditions and related actions (hash) when condition is met. Condition is true when given field (in this case status) is equal to any of the values specified in condition array.

  "dynamic-fields" => [
    {
      # when this field has one of these values -> fieds_actions are executed 
      condition: ["Unconfirmed", "New", "Assigned", "Reopened", "Ready"],
      # set visibility of resolution to false (= hide resolution field)
      field_actions: { resolution: { visible: false } } 
    },
    { 
      condition: ["Resolved", "Verified"], 
      field_actions: { resolution: { visible: true  }} 
    }
  ]   

Dependant select boxes

For better demonstration of depandant select boxes let's introduce small change to application and include versions of projects. Project can have many versions and issue will be tied to specific version of project.

So let's generate Version scaffold and add version_id to issues. Then migrate the database.

rails g scaffold Version name project:belongs_to
rails g migration addVersionIdToIssues version:belongs_to
rake db:migrate

Then you need to add associations in appropriate models, do not forget inverse_of option.

class Issue < ActiveRecord::Base
  belongs_to :project, inverse_of: :issues
  belongs_to :version, inverse_of: :issues
  ..
end

class Project < ActiveRecord::Base
  has_many :versions, inverse_of: :project
  has_many :issues, inverse_of: :project
end

class Version < ActiveRecord::Base
  belongs_to :project, inverse_of: :versions
  has_many :issues, inverse_of: :version
end

This leads us to our problem when you are creating or updating issue you now need to enter not just project but specific version of that project which is not working properly. Suppose you have 2 projects A and B. Project A has versions 1.0 and 2.0. Project B has versions 1.0, 1.1 and 1.2, it allows you to enter incorrect data. For example version 1.1 and project A (version 1.1 doesn't belongs to project A). What you want to do is display only versions related to currently selected project.

Actually you might be wondering how is it even possible that options for associations are displayed in selectboxes at all. Where are they from? These values are obtained by making ajax call to options method of particular controller depending on association. For instance values for project association are retrieved by options method call to ProjectsController. Values for version's associations by VersionsController#options call. Basicaly controllers retrieve values of related model no matter whence they are called unless you specify otherwise. That can be done either by changing options_source in model to different url or by defining options method in controller. Here is the example of this method which demonstrates default behaviour (it's going to work the same way if you do not define this method).

app/controllers/versions_controller.rb

  def options
    authorize!(action_name.to_sym, resource_class) # CanCan
    versions = Version.search(params[:f]).result
    options!(collection: versions)
  end

First line needs to be there to ensure authorization. Then it retrieves all Version records that match current search params and forward these records to parent action options!. If you try manually append search params to options url /versions/options for example /versions/options?f[name_cont]=app. which will display versions with name containing substring 'app'.

So you already know that every time you click on select2 field it makes call to options method to display drop-down menu with options and it display all records as options unless you specify search parameters.

Yes, that's cool. But how to set these parameters in new or edit form with current values? The basepack once again make use of html attributes more precisely html data attributes.

app/models/issues.rb

field :version do
      html_attributes do
        { data: { 
            # set project's field as dependent select box
            "dependant-filteringselect" => "field=project_id",

        # post parameters
        "dependant-param" => "f[project_id_eq]" 
      } 
    }
  end
end

To display only versions which belongs to currently selected project it marks projects field as "dependant-filteringselect". Id of currently selected project (if there is any) is send as parameter f[project_id_eq] to VersionsController#options.Without defining dependant-param id of currently selected would be send in params[:undefined] which would be ignored by options method.

You do not have to use search params and you can write your options method the way you want. But most of the time using search params and default options method will be sufficient.

It works quite well except it displays all versions as options at beginning. To deal with this unique case you can set default options query send when form (new or edit) is rendered for the first time. It can be defined in options_source_params block.

You have to remember that you are working with both new and edit form. In new form it would be enough to set default query f[project_id_eq] to id of non-existing project which will not display any versions at the beginning.

app/models/issues.rb

      field :version do
        options_source_params do
          { "f[project_id_eq]" => -1 }
        end
        html_attributes do
          { data: { 
              # set project's field as dependent select box
              "dependant-filteringselect" => "field=project_id",

              # post parameters
              "dependant-param" => "f[project_id_eq]" 
            } 
          }
        end
      end

But if you want to change version in edit form of existing issue that already has project set it would display no versions as well. Unless you select a project which override default id -1. So if the issue (bindings[:object]) has project set use that project_id as a default query otherwise use -1.

app/models/issues.rb

  options_source_params do
    { "f[project_id_eq]" => bindings[:object].try(:project_id) || -1 }
  end