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]
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:
application.js manifest fileapp/assets/stylesheets/basepack_and_overrides.css
app/models/ability.rb
Gemfile
config/basepack-settings.yml
config/locales/basepack.en.yml
app/controllers/resources_controller.rbapplication.html.erb 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: :projectrails_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:
options methodtaggings methodbuild_resource methodIt 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.
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
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
...
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.
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
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 | |
|
No resolution yet. |
| Closed bugs | |
|
|
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 }}
}
]
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