Rails 4 Application Development HOTSHOT (2014)

Chapter 4. Creating a Restaurant Menu Builder

Tablets and smartphones are becoming cheaper by the day. They have better screens, faster processors, and better graphics. Hence, it makes perfect sense to port the restaurant menus to tablets and smartphones. It will save a lot of issues related to print and design and also save a lot of cost by making the menus easy to update. It will also make the process of ordering much faster and less prone to human errors. In this project, we will build a SaaS-based product to create restaurant menus online.

Mission briefing

In this project, we will create a SaaS-based software to create restaurant menus. Users can sign up and will have their own subdomain and area; they will also have plans to select from. Along with this, they can also create products and menus and assign them to a restaurant.

While building this project, we will take a look at concepts such as concerns, subdomains, creating plans, and managing a SaaS-based product. We will also see various ways to add roles to our application users, multitenancy in applications, and import and export data in various formats. Using these techniques, we can end up refactoring our code.

Our SaaS application's home page looks like the following at the end of our project:

Mission briefing

Why is it awesome?

SaaS-based applications (such as http://basecamp.com/) are one of the most popular business models these days. As a lot of offline businesses are moving towards cloud, we will build a SaaS-based restaurant management application. We will create a multiple plan-based system where users can pay on a monthly basis after a free trial for a limited set of resources. The system will also allow data to be imported and exported in the CSV format.

At the end of this project, we will be able to build a framework for a SaaS-based application.

Your Hotshot objectives

While building this application, we will have to go through the following tasks:

·        Creating organizations with signup

·        Creating restaurants, menus, and items

·        Creating user roles

·        Creating plans

·        Creating subdomains

·        Adding multitenancy and reusable methods

·        Creating a monthly payment model, adding a free trial plan, and monthly billing

·        Exporting data to a CSV format

Mission checklist

We need the following installed on the system before we start with our mission:

·        Ruby 1.9.3 / Ruby 2.0.0

·        Rails 4.0.0

·        MySQL 6

·        Bootstrap 3.0

·        Sass

·        Sublime Text

·        Devise

·        Git

·        A tool for mockups

·        jQuery

·        ImageMagick and RMagick

Creating organizations with sign up

Every user who creates a SignUp for our application will need an organization. This is because a business is not run in isolation, and there will be different stakeholders in the system; the service staff, chefs, and managers will all need access to the system. Now that we have defined the roles of different types of users of the system, we will bring them together on one level of abstraction called organization. In this task, we will build a wizard to set up an organization as a part of the SignUp process. We will use the wickedgem to create a wizard in our application.

The following is our standard protocol of mocking up our page—we will first use our wireframing tool to create a mockup of our home screen. Our mockup for the home screen looks like the following screenshot:

Creating organizations with sign up

Prepare for lift off

Before we begin this task, we need to generate a blank application. Then, we need to create models for organization and user. We can create a users model using the devise gem. In order to create organizations, we first create the model:

gotable$ rails g model name:string description:text

Then, we will create a relationship between users and organizations:

app/models/organization.rb

  has_and_belongs_to_many :users

app/models/user.rb

  has_and_belongs_to_many :organizations

In order to store the association data, we will create a table:

gotable$ rails g migration organizations_users organization_id:integer user_id:integer

Engage thrusters

To create an organization, we will perform the following steps:

1.    We will first add the wicked gem to the Rails Gemfile and run bundle install:

2.  Gemfile

3.        gem 'wicked'

4.  :~/gotable$ bundle install

5.    We will then generate a controller in order to define these steps:

6.  :~/gotable$ rails g controller setup_organization

7.        create  app/controllers/setup_organization_controller.rb

8.        invoke  erb

9.        create    app/views/setup_organization

10.          We will start by defining the steps inside setup_organization_controller.rb and include the wicked wizard module in it to autoload the wicked module as soon as this controller is called:

11.app/controllers/setup_organization_controller.rb

12.class SetupOrganizationController < ApplicationController

13.  include Wicked::Wizard

14.  steps :organization_setup

end

15.          The first step of our wizard is signup. Hence, we will modify the signup method so that it redirects to the step defined in wicked once it is executed. We will edit devise/registrations_controller to suit our needs:

16.app/views/devise/registrations_controller.rb

17.def after_sign_up_path_for(resource)

18.     session[:plan_id] = params[:plan_id]

19.     setup_organization_path(:organization_setup)

  end

20.          The wicked gem uses the show and update actions in order to perform most of the tasks. The show action is used in order to initiate the step and render the page. Then, the update action is used in order to send the variables to the respective model:

21.app/controllers/setup_organization_controller.rb

22. def show

23.    @user = current_user

24.    case step

25.    when :organization_setup

26.      @organization = Organization.new

27.    end

28.    render_wizard

   end

29.          We need a form to submit these values to the database, and the form resource needs to point to the wizard path.

30.          The form for the organization setup submits to the wizard path as a put method instead of a post method:

31.app/views/organizations/_wizard.html.erb

32.<%= form_for(@organization , :url => wizard_pathh, :method => :put) do |f| %>

33.  <% if @organization.errors.any? %>

34.    <div id="error_explanation">

35.      <h2><%= pluralize(@organization.errors.count, "error") %> prohibited this organization from being saved:</h2>

36.      <ul>

37.      <% @organization.errors.full_messages.each do |msg| %>

38.        <li><%= msg %></li>

39.      <% end %>

40.      </ul>

41.    </div>

42.  <% end %>

43.  <div class="form-group">

44.    <%= f.label 'Organization Name' %>

45.    <%= f.text_field :name, :class=>"form-control", :placeholder => "Organization Name"  %>

46.  </div>

47.  <div class="form-group">

48.    <%= f.label :description %>

49.    <%= f.text_area :description, :class=>"form-control", :rows=>"3", :placeholder => "Description" %>

50.  </div>

51.  <%= f.submit 'Create', :class=>"btn btn-default" %>

<% end %>

52.          However, once the form is filled and submitted, we need to send the params to the organization table. We will do so via the update action of wicked in our controller.

53.          In our case, we have a has_and_belong_ to_many (HABTM) relationship between organization and users. Hence, we will create @organization.users as a hash:

54.app/controllers/setup_organizations_controller.rb

55.   def update

56.     @user = current_user

57.     @organization = Organization.new(organization_params)

58.     @organization.users << @user

59.     render_wizard @organization

   end

60.          However, we need to whitelist organization_params in our controller to access them in the method:

61.app/controllers/setup_organizations_controller.rb

62. private

63.      # Never trust parameters from the scary internet, only allow the white list through.

64.    def organization_params

65.      params.require(:organization).permit(:name, :description, :plan_id, {:user_ids => []})

    end

66.          Note that we have added user_ids as an array because it points to the join_table representing has_and_belongs_to_many relationship.

67.          We need to end the wizard once the steps are completed:

68.app/controllers/setup_organizations_controller.rb

69.  private

70.  def redirect_to_finish_wizard

71.    redirect_to dashboard_path, notice: "Thank you for signing up. You can now build beautiful menus"

  end

72.          Lastly, we need a route to tie this all up. The route will build the resource according to the controller name and will be assigned to the wizard_path in our view:

73.config/routes.rb

resources :setup_organization

74.          The following screenshot shows what Step 1 in the form looks like now:

Engage thrusters

Objective complete – mini debriefing

In the previous task, we looked at how to create a wizard in our application. We saw the concept of has_and_belongs_to_many in the context of Rails 4 in this task. HABTM is a special case of relationships in which both models have a many-to-many relationship with each other. In our users and organizations models, we wrote the following:

  has_and_belongs_to_many :organizations

  has_and_belongs_to_many :users

We created a table with organization_id and user_id. This table will store the data for the organizers and associated users. We chose to do it this way because we did not want to do anything further with the association. In case we want to do more with the association of the two models, we can create a third model and define the relationship in the following way:

  has_many :organizations, :through => :members

  has_many :users, :through => :members

We will have to create a separate members model with the following associations:

belongs_to :organization

belongs_to :user

Coming back to our code, in order to assign a user to organizations, we need to pass it as an array:

@organization.users << @user

In our organization controller, we added a user_ids array to our params user so that multiple users can be associated to an organization:

   params.require(:organization).permit(:name, :description, :plan_id, {:user_ids => []})

A wizard significantly improves the engagement and user experience in a website because it decreases the number of fields in a form. However, too many steps can also lead to the same problem. Hence, a balance needs to be attained between the steps and fields that will go as a part of our wizard. The wicked gem is quite a comprehensive solution as far as wizards are concerned, as you can also build a single object across different steps.

In this task, we included the wicked gem and defined the steps for our wizard. The wicked gem completely relies on the ActionController module of Rails. We created a new controller called setup_organizations to define the steps required for the wizard. The wizard identifies the controller using a module inclusion:

  include Wicked::Wizard

When we define steps, they are called one by one inside the show action. This is because wicked binds itself to the object ID in order to create the flow of steps. In order to access the ID, we need to prefix it with the object name; for example, plan_id. The wizard_pathpicks up the path of the current step in the wizard. Once the include method is defined in the controller, the controller and step path are substituted in wizard_path. The same procedure follows for the subsequent steps.

As wicked relies on the object ID, it uses show and update methods to generate the steps. Each step will have a view of the same name. The show action initializes and starts the wizard. The update action updates the params at each step. At the end of the wizard, runs the update query for the record.

In order to end the wizard, we used a custom private method called redirect_to_finish_wizard. This method in wicked allows us to redirect to the page we want to:

def redirect_to_finish_wizardd

end

Finally, we added a route to access all the methods inside the setup_organization controller.

Step 2 in the form in the wizard looks like the following screenshot:

Objective complete – mini debriefing

Creating restaurants, menus, and items

The organization in our case is a company that owns the restaurants. There can be one or more restaurants inside an organization. These restaurants will have different menus; for example, a menu for dessert, a menu for the main course, and a menu for the drinks. These menus are going to have many items. We will add these items by creating nesting between menus and items.

Prepare for lift off

We first have to create models for restaurants, menus, and items with their respective attributes:

rails g scaffold restaurant name:string description:text slug:string

rails g scaffold menu title:string description:text

rails g model item name:string description:text price:float

Engage thrusters

1.    We will add restaurants, menus, and menu items to our application. For the sake of convenience, we will generate a scaffold of the restaurant model. Please be sure to delete the scaffold.css.scss file because it conflicts with the existing CSS file in the system and tends to override it in places. The following code shows what the model for a restaurant looks like:

2.  app/models/restaurant.rb

3.  class Restaurant < ActiveRecord::Base

4.    extend FriendlyId

5.    friendly_id :name, use: :slugged

6.    has_many :menus

7.    belongs_to :organization

8.    validates :name, presence: true

end

9.    We will pass organization_id in the create method in the restaurant controller. The organization ID is our way to lock down all restaurants, menus, and items according to a single organization:

10.controllers/restaurants_controller.rb

11.  def create

12.    @restaurant = Restaurant.new(restaurant_params)

13.    @restaurant.organization_id = current_user.organizations.first.id

14.    respond_to do |format|

15.      if @restaurant.save

16.        format.html { redirect_to @restaurant, notice: 'Restaurant was successfully created.' }

17.        format.json { render action: 'show', status: :created, location: @restaurant }

18.      else

19.        format.html { render action: 'new' }

20.        format.json { render json: @restaurant.errors, status: :unprocessable_entity }

21.      end

22.    end

23.  end

24.def restaurant_params

25.      params.require(:restaurant).permit(:name, :description, :organization_id)

26. end

27. 

28.app/views/_form_errors.html.erb

29.  <% if @restaurant.errors.any? %>

30.    <div id="error_explanation">

31.      <h2><%= pluralize(@restaurant.errors.count, "error") %> prohibited this restaurant from being saved:</h2>

32.      <ul>

33.      <% @restaurant.errors.full_messages.each do |msg| %>

34.        <li><%= msg %></li>

35.      <% end %>

36.      </ul>

37.    </div>

38.  <% end %>

39.views/restaurants/_form.html.erb

40.<%= form_for(@restaurant) do |f| %>

41.   <%= render 'form_errors'%>

42.  <div class="form-group">

43.    <%= f.label :name %><br>

44.    <%= f.text_field :name, :class=>"form-control", :placeholder => "Restaurant Name"  %>

45.  </div>

46.  <div class="form-group">

47.    <%= f.label :description %><br>

48.    <%= f.text_area :description , :class=>"form-control", :placeholder => "Description" %>

49.  </div>

50.  <div class="actions">

51.    <%= f.submit :class=>"btn btn-default" %> <%= link_to 'Cancel', restaurants_path, :class=>"btn btn-default" %>

52.  </div>

<% end %>

At the end of this iteration, our restaurant view looks like the following screenshot:

Engage thrusters

53.          The Menu class is similar to the restaurant class. The main difference here is that we will add support for nesting with items. We will add accepts_nested_attributes_for :items so that we can access parameters of items within the menu model:

54.app/models/menu.rb

55.class Menu < ActiveRecord::Base

56.  belongs_to :restaurant

57.  has_many :items

58.  accepts_nested_attributes_for :items

end

59.          On the restaurants show page, we need to add restaurant_id as a parameter so that it is passed as a parameter to find the menus related to a particular restaurant:

60.controllers/restaurants_controller.rb

61. def show

62.    @menus = Menu.where(:restaurant_id => @restaurant.id)

63.  end

64.views/restaurants/show.html.erb

65.<h3>Menus</h3>

66.<% @menus.each do |m|%>

67. <%=link_to m.title, menu_path(m)%>

68.<%end%>

<%= link_to "Add a Menu", new_menu_pathh(:restaurant_id => @restaurant.id), :class=>"btn btn-default" %>

69.          Items do not have a controller and view separately. They will reside as a part of menu in our application. So, the attributes of items need to be whitelisted inside our menus_controller class:

70.app/controllers/menus_controller.rb

71.# Never trust parameters from the scary internet, only allow the white list through.

72.    def menu_params

73.      params.require(:menu).permit(:title, :description, :restaurant_id,:items_attributes => [:id, :name, :description, :price, _:destroy]

74.                                   )

    end

75.          Associate the item model with the menu:

76.app/model/item.rb

77.class Item < ActiveRecord::Base

78.  belongs_to :menu

end

79.          We now need to build the views. At this point, we need to add the nested_form gem to our application:

80.Gemfile

81.gem "nested_form"

82.          Add jquery_nested_form script to assets/application.js:

83.app/assets/application.js

//= require jquery_nested_form

84.          We will first convert our menu form to a nested form by adding nested_form_for:

85.app/views/menus/_form.html.erb

86.<%= nested_form_for(@menu) do |f| %>

87.  <% if @menu.errors.any? %>

88.    <div id="error_explanation">

89.      <h2><%= pluralize(@menu.errors.count, "error") %> prohibited this menu from being saved:</h2>

90.      <ul>

91.      <% @menu.errors.full_messages.each do |msg| %>

92.        <li><%= msg %></li>

93.      <% end %>

94.      </ul>

95.    </div>

96.  <% end %>

97.  <div class="form-group">

98.    <%= f.label :title %><br>

99.    <%= f.text_field :title, :class=>"form-control", :placeholder => "Title" %>

100.     </div>

101.     <div class="form-group">

102.       <%= f.label :description %><br>

103.       <%= f.text_area :description, :class=>"form-control", :placeholder => "Description" %>

104.     </div>

105.     <%= f.hidden_field :restaurant_id, :value => params[:restaurant_id]%>

106.     <div class="actions">

107.       <%= f.submit 'Create Menu', :class=>"btn btn-default" %>

108.     </div>

<% end %>

109.     In order to add items, we will add fields_for to the nested form:

110.   app/views/menus/_form.html.erb

111.   <%= f.fields_for :items do |i| %>

112.       <div class="form-group">

113.        <%= i.label :name %><br>

114.        <%= i.text_field :name, :class=>"form-control", :placeholder => "Name" %>

115.       </div>

116.       <div class="form-group">

117.        <%= i.label :price %><br>

118.        <%= i.text_field :price, :class=>"form-control", :placeholder => "Price" %>

119.       </div>

120.       <div class="form-group">

121.        <%= i.label :description %><br>

122.        <%= i.text_area :description, :class=>"form-control", :placeholder => "Description" %>

123.       </div>

124.       <%= i.link_to_remove "Remove this item", :class=>"btn btn-default" %>

125.       <% end %>

    <p><%= f.link_to_add "Add an Item", :items, :class=>"btn btn-default" %></p>

Objective complete – mini debriefing

In this task, we first created models for our restaurant, menu, and item.

Using a nested form is a very useful technique and helps reduce a lot of unnecessary code. It is helpful also because it keeps the application structure easy to understand and the flow logical. We used the nested_form gem to create a form for items within our menu form. The gem depends on jQuery and allows the developer to create multiple nested forms inside a model. In order to make it work with strong parameters, we added the :_destroy method to our parameters. This will allow the deletion of the nested model records. In order to make our form recognize methods from the gem, we have to modify form_for to nested_form_for:

<%= nested_form_for(@menu) do |f| %>

The gem add extra form helpers (such as nested_form_for) on top of Rails in order to generate the nested form. Some other form helper methods are link_to_add or link_to_remove that add or remove the tasks. The gem also creates an interface to jQuery in order to add and remove the form using the form helpers that generate add and remove links.

The following screenshot shows what the menu page looks like with a nested form to add items:

Objective complete – mini debriefing

Creating user roles

Our aim here is to create a role-based authentication structure, where we will define various roles for the users. We will use a combination of the rolify gem to define roles and cancan gem, which includes methods to restrict users according to their roles.

Prepare for lift off

As in our previous projects, we have used the devise gem for creating the authentication system. We looked at authentication in Project 1A Social Recipe-sharing Website, and have included it in every project ever since. So now, it is assumed that you will install and configure devise before you begin this step.

Engage thrusters

We will add the basics of the permissions framework in these steps:

1.    Add the cancan gem and rolify gem to our Gemfile in order to use in conjunction with devise:

2.  Gemfile

3.  gem 'cancan'

4.  gem 'rolify', '3.4'

5.    Run the bundler and then generate the ability model to define authorizations:

6.  :~/gotable$ rails g cancan:ability

      create  app/models/ability.rb

7.    This will create a new file inside the models folder called ability. We will now generate the role model and related migrations using the rolify generator:

8.  :~/gotable$ rails generate rolify:role

9.        create  app/models/role.rb

10.      insert  app/models/user.rb

11.      create  config/initializers/rolify.rb

      create  db/migrate/20130929082020_rolify_create_roles.rb

12.          This will also generate an initializer file and insert the rolify method in the user model:

13.app/models/user.rb

14.class User < ActiveRecord::Base

15.  rolify

16.  # Include default devise modules. Others available are:

17.  # :confirmable, :lockable, :timeoutable and :omniauthable

18.  devise :database_authenticatable, :registerable,

19.         :recoverable, :rememberable, :trackable, :validatable

end

20.          Also, the role model has a reference to the join_table between users and roles:

21.app/model/role.rb

22.class Role < ActiveRecord::Base

23.  has_and_belongs_to_many :users, :join_table => :users_roles

24.  belongs_to :resource, :polymorphic => true

25.  scopifyy

end

26.          Once the role methods are generated, we will define the abilities of each user role:

27.app/models/ability.rb

28.class Ability

29.  include CanCan::Ability

30.  def initialize(user)

31.       user ||= User.new # guest user (not logged in)

32.       if user.has_role? :admin

33.         can :manage, :all

34.       else

35.        can :read, Organization

36.       can :manage, Organization if user.has_role?(:owner, Organization)

37.       can :write, Organization, :id => Organization.with_role(:manager, user).map(&:id)

38.       end

  end

39.          Let's try and add a role to our user. We will fire up our Rails console and call our last user in a variable:

40.1.9.3-p327 :001 > user = User.last

41.  User Load (0.7ms)  SELECT `users`.* FROM `users` ORDER BY `users`.`id` DESC LIMIT 1

42. => #<User id: 4, email: "admin@laboncafe.com", encrypted_password: "$2a$10$n7lb8ivkcnAFZZ5rSV0eOuhFWv7sU.HkrU0/OespLOzh...", reset_password_token: nil, reset_password_sent_at: nil, remember_created_at: nil, sign_in_count: 8, current_sign_in_at: "2013-09-29 07:52:10", last_sign_in_at: "2013-09-29 07:51:47", current_sign_in_ip: "127.0.0.1", last_sign_in_ip: "127.0.0.1", created_at: "2013-09-29 04:22:11", updated_at: "2013-09-29 07:52:10", name: "laboncafe">

43.          Once the variable value is set, we will add a role to it. As the user is the owner of the organization, we will add a role called owner:

44.1.9.3-p327 :003 > user.add_role "owner"

45.  Role Load (0.7ms)  SELECT `roles`.* FROM `roles` WHERE `roles`.`name` = 'owner' AND `roles`.`resource_type` IS NULL AND `roles`.`resource_id` IS NULL ORDER BY `roles`.`id` ASC LIMIT 1

46.   (0.6ms)  BEGIN

47.  SQL (0.7ms)  INSERT INTO `roles` (`created_at`, `name`, `updated_at`) VALUES ('2013-09-29 08:38:26', 'owner', '2013-09-29 08:38:26')

48.   (41.1ms)  COMMIT

49.  Role Exists (0.5ms)  SELECT 1 AS one FROM `roles` INNER JOIN `users_roles` ON `roles`.`id` = `users_roles`.`role_id` WHERE `users_roles`.`user_id` = 4 AND `roles`.`id` = 1 LIMIT 1

50.   (0.4ms)  SELECT `roles`.id FROM `roles` INNER JOIN `users_roles` ON `roles`.`id` = `users_roles`.`role_id` WHERE `users_roles`.`user_id` = 4

51.  Role Load (0.4ms)  SELECT `roles`.* FROM `roles` WHERE `roles`.`id` = 1 LIMIT 1

52.  Role Load (0.5ms)  SELECT `roles`.* FROM `roles` INNER JOIN `users_roles` ON `roles`.`id` = `users_roles`.`role_id` WHERE `users_roles`.`user_id` = 4

53.   (0.2ms)  BEGIN

54.   (0.3ms)  INSERT INTO `users_roles` (`user_id`, `role_id`) VALUES (4, 1)

55.   (45.9ms)  COMMIT

56. => #<Role id: 1, name: "owner", resource_id: nil, resource_type: nil, created_at: "2013-09-29 08:38:26", updated_at: "2013-09-29 08:38:26">

57.          To see what the user can or cannot do, we will use the Ability model method:

58.1.9.3-p327 :002 > ability = Ability.new(user)

59.   (0.7ms)  SELECT COUNT(*) FROM `roles` INNER JOIN `users_roles` ON `roles`.`id` = `users_roles`.`role_id` WHERE `users_roles`.`user_id` = 4 AND (((roles.name = 'admin') AND (roles.resource_type IS NULL) AND (roles.resource_id IS NULL)))

60.   (1.2ms)  SELECT COUNT(*) FROM `roles` INNER JOIN `users_roles` ON `roles`.`id` = `users_roles`.`role_id` WHERE `users_roles`.`user_id` = 4 AND ((((roles.name = 'owner') AND (roles.resource_type IS NULL) AND (roles.resource_id IS NULL)) OR ((roles.name = 'owner') AND (roles.resource_type = 'Organization') AND (roles.resource_id IS NULL))))

61.   (0.8ms)  SELECT COUNT(*) FROM `roles` INNER JOIN `users_roles` ON `roles`.`id` = `users_roles`.`role_id` WHERE `users_roles`.`user_id` = 4 AND `roles`.`name` = 'manager'

62.  Organization Load (0.6ms)  SELECT `organizations`.* FROM `organizations` INNER JOIN `roles` ON `roles`.resource_type = 'Organization' AND

63. (`roles`.resource_id IS NULL OR `roles`.resource_id = `organizations`.id) WHERE (`roles`.name IN ('manager') AND `roles`.resource_type = 'Organization') AND (`roles`.id IN (NULL) AND ((resource_id = `organizations`.id) OR (resource_id IS NULL)))

64.1.9.3-p327 :003 > ability.can? :manage, :all

65. => false

66.1.9.3-p327 :004 > ability.can? :manage, Organization

 => true

67.          Load up the roles in the organization model so that these are applied to the organization model once it is initiated:

68.app/models/organization.rb

69.class Organization < ActiveRecord::Base

70.  resourcify

end

Objective complete – mini debriefing

In this task, we used the rolify gem to define different roles in the system. It provides a DSL (domain-specific language) that integrates with cancan and devise with ease. We also used the cancan gem to define the authorization and access for each role.

In order to do this, we first bundled our application with cancan and rolify gem. We then generated a model called ability. In the ability model, the initialize model directly hooks up to the user model:

def initialize(user)

    user ||= User.new # guest user (not logged in)

end

After this, we generated the role model using the rolify generator. The rolify generator depends on two methods: scopify and resourcify. The scopify method is defined in the role model. It loads the scopes (https://github.com/EppO/rolify/blob/master/lib/rolify/adapters/active_record/scopes.rb) and associates them with the role model. The resourcify method, once defined in a model, applies the roles to that model. Another advantage of using rolify is that it integrates well with cancan. In the ability model, we defined an authorization such that if the user has a role owner, he or she can manage an organization. The manage method allows a user to edit and delete a particular resource. Likewise, read, write, and other specific actions can be defined for users. If a user has this role, they will be allowed to perform only that action.

The user.has_role? method is a method from rolify that calls the role inside cancan.

Creating plans

The most important part of SaaS is a multitier plan. A lot of companies now keep one plan for the sake of simplicity. However, plans are created so that our application fits the requirement of companies of different sizes. This section will cover creating a plan and associating it with organizations.

Engage thrusters

We will create plans stepwise and associate it with our application resources:

1.    Generate a plan model by running the Rails generator:

2.  $ rails g model plan name:integer restaurants:integer price:float tables:integer menu_items:integer storage:integer

3.    Now run Rake db:migrate to add a plans table. Load seed data to the plans table:

4.  db/seeds.rb

5.  plans = [

6.    [ "Small", 1, 10, 20, 5, 10 ],

7.    [ "Medium", 5, 50, 50, 10, 30 ],

8.    [ "Large", 10, 100, 50, 50, 50 ]

9.  ]

10.plans.each do |name, restaurants, tables, menu_items, storage, price|

11.  Plan.find_or_create( name: name, restaurants: restaurants, tables:tables, menu_items:menu_items, storage:storage, price:price )

end

12.          Plans will now be added in our database.

13.          We will now display these plans in the home page so that the user can compare them before signing up. For this, we will first create a home controller and home page:

14.Gotable$ rails g controller home index

15.app/controllers/home_controller.rb

16.class HomeController < ApplicationController

17.  def index

18.    @plans = Plan.all.to_a

19.  end

20.end

21.app/views/home/index.html.erb

22.<div class="row">

23.       <% @plans.each do |plan|%>

24.           <div class="col-sm-4">

25.          <div class="list-group">

26.            <a href="#" class="list-group-item active">

27.              <%= plan.name %> - <%= number_to_currency(plan.price)%> / Month

28.            </a>

29.            <a href="#" class="list-group-item"><%= plan.restaurants %> Restaurant</a>

30.            <a href="#" class="list-group-item"><%= plan.tables %> tables / Restaurant</a>

31.            <a href="#" class="list-group-item"><%= plan.menu_items %> Menu Items / Restaurant</a>

32.            <a href="#" class="list-group-item"><%= plan.storage %> GB storage</a>

33.            <%= link_to new_user_registration_path(:plan_id => plan.id),  :class=>"list-group-item" do%><button class="btn btn-success">Sign Up</button><%end%>

34.          </div>

35.         </div><!-- /.col-sm-4 -->

36.        <% end %>

      </div>

37.          Here, we are passing plan_id as a parameter in the link for Sign Up. After adding the Plans section, our home page will look like the following screenshot:

Engage thrusters

38.          We need to display the selected plan on our registration page. In order to do so, we will use pluck. As we need only the plan name, we will just use pluck to call the name of the plan with ID as the parameter. We will add the following in ourregistrations_controller.rb file:

39.app/holders/application_helper.rb

40.module ApplicationHelper

41.  def plan_name(plan_id)

42.  plan_name = Plan.where(:id=> plan_id).pluck(:name).first

43.  end

44.end

45. 

46.app/controllers/devise/registrations_controllers.rb

47.private

48.def update_sanitized_params

49.    devise_parameter_sanitizer.for(:sign_up) {|u| u.permit(:name, :organization_name, :email, :password, :password_confirmation, :plan_id)}

  end

50.          We will now display the plan in the form as follows:

51. app/views/devise/registrations/new.html.erb

<h3>Plan Selected: <%=  plan_name(params[:plan_id])  %></h3>

Engage thrusters

52.          Associate plan and user. In order to associate an organization to a plan, we will associate it via user. This will give a user the freedom to create multiple organizations and get billed for it through their own account. We will define them in our models:

53.models/plan.rb

54.  belongs_to :user

55. 

56.models/user.rb

has_one :plan

57.          We will now save plan IDs along with the user details. This is essential in order to keep a track of the subscription of each plan in accordance with the resources being used by each subscriber. In order to do so, we will first add plan_id to the user:

58.:~/gotable$ rails g migration add_plan_id_to_users plan_id:integer

59.      invoke  active_record

60.      create    db/migrate/20130930030144_add_plan_id_to_users.rb

61.          The following is how the migration looks:

62.db/migrate/20130930030144_add_plan_id_to_users.rb

63.class AddPlanIdToUsers < ActiveRecord::Migration

64.  def change

65.    add_column :users, :plan_id, :integer

66.  end

end

67.          Pass plan_id as a hidden field in the user signup form. We passed plan_id as parameter along with our link to plan in the home page:

68.app/views/devise/registrations/new.html.erb

<%= f.hidden_field :plan_id, :value => params[:plan_id]%>

Objective complete – mini debriefing

In the previous task, we created a structure for plans and defined them in the database. In order to do so, we created a model and table for plans and loaded some seed data into it. Seed data is used to add some default data to the application.

We also saw how to associate our user to a plan. In this way, we will be able to set up a monthly billing account for a particular plan for a specific user. We also saw the use of pluck:

plan_name = Plan.where(:id=> plan_id).pluck(:name).first

Pluck is an ActiveRecord query method that selects only a particular column; in this case it calls the column called name. We added some extra parameters to devise. In order to do so, we added a method called update_sanitized_params in our registrations controller. This method overrides the default params in devise:

def update_sanitized_params

    devise_parameter_sanitizer.for(:sign_up) {|u| u.permit(:name, :organization_name, :email, :password, :password_confirmation, :plan_id)}

  end

We created a helper method to call the plan name in our application helper, which accepts plan_id as an argument. This is to display the plan name in views.

We also displayed the selected plan on the Sign Up page so that the user is clear about what he or she is signing up for.

Creating subdomains

One of the main functions of a SaaS-based application is to provide the users with a separate area completely owned by them. This area is completely abstracted from others and is visible only to the owners and users of the organization. In this task, we will create subdomains and associate them with an organization. We will also explore the definition of a concern in detail and how can it be used to create reusable code components. We will use Tim Pope's solution (http://tbaggery.com/2010/03/04/smack-a-ho-st.html) of extending a domain name called local virtual host (lvh.me) in order to make subdomains work on our localhost.

Engage thrusters

Let us create subdomains for our application users:

1.    We will first save the domain as a part of our SignUp form:

2.  app/views/devise/registrations/new.html.erb

3.  <div class="form-group">

4.      <%= f.label 'domain name' %><br />

5.      <%= f.text_field :domain_name, :autofocus => true, :class=>"form-control", :placeholder => "Domain Name" %>

  </div>

Engage thrusters

6.    Domain names should not have spaces between the words. In order to avoid these, we will add a validation to the user model:

7.    app/models/user.rb

validates :name, presence: true, format: { without: /^((http|https):\/\/)[a-z0-9]*(\.?[a-z0-9]+)\.[a-z]{2,5}(:[0-9]{1,5})?(\/.)?$/ix, multiline: true }

Engage thrusters

8.    In order to create subdomains, we need a class that passes the value of the request and matches the format of a subdomain. As www is not considered a valid subdomain rule, we will check for this and make it nil:

9.  lib/subdomain.rb

10.class Subdomain

11.    def self..matches?(request)

12.      case request.subdomain

13.      when 'www', '', nil

14.        false

15.      else

16.        true

17.      end

18.    end

end

19.          Once this method is set up, we will make a call inside our controller. In order to make only the authenticated user log in, we will create dashboard_controller and add an authenticate_user! filter to it. Dashboard is the page where we can see our activity stream:

20.gotable$ rails g controller dashboard

21.controllers/dashboard_controller.rb

  before_filter :authenticate_user!

22.          We will also check if the user belongs to that subdomain or not:

23.app/controllers/dashboard_controller.rb

24.  before_filter :load_subdomain

25.  def show

26.     @user = User.where(:name => request.subdomain).first || not_found

27.     @user.organizations.each do |o|

28.       @organization_name = o.name

29.     end

30.  end

31.  def not_found

32.    raise ActionController::RoutingError.new('User Not Found')

33.  end

34.  def load_subdomain

35.    @user = User.where(:domain_name => request.subdomain).first

  end

36.          Wire this concern to the route. We will pass our subdomain class as a constraint in order to check the subdomain format as soon as the request is made. Also, we will see if the user has been authenticated or not and based on this, we will redirect him or her to the respective organization's dashboard:

37.config/routes.rb

38.    authenticated do

39.      get '/' => 'dashboard#show', :constraints => Subdomain, :as => 'dashboard'

    end

40.          Create a method in order to first check if there is a value of subdomain supplied or not. If it is present, it will append the subdomain, domain, and port. We also check for the presence of a hash key called subdomain. If the key is present, it will add the value of the host to the value of the with_subdomain method:

41.app/controllers/concern/subdomain.rb

42.module Concerns

43. module Url

44.  extend ActiveSupport::Concern

45.  def with_subdomain(subdomain)

46.    subdomain = (subdomain || "")

47.    subdomain += "." unless subdomain.empty?

48.    [subdomain, request.domain, request.port_string].join

49.  end

50.  def url_for(options = nil)

51.   if options.kind_of?(Hash) && options.has_key?(:subdomain)

52.    options[:host] = with_subdomain(options.delete(:subdomain))

53.   end

54.   super

55.  end

56. end

end

57.          In order to execute this Url manipulation, we will need to include this in the application controller and extend it:

58.controllers/application_controller.rb

59.class ApplicationController < ActionController::Base

60.  include Concerns::Url

end

61.          Now that domains are there, we will have to ensure that the sessions of each subdomain are different from the other. By adding a :domain => :all method, we will have a different session store for each subdomain:

62.config/initializers/session_store.rb

Gotable::Application.config.session_store :cookie_store, key:'_gotable_session', :domain => :all

Objective complete – mini debriefing

At the end of this section, we have successfully created subdomains, abstracted their sessions, and made sure all the redirects are in place. We first validated our domain_name with a regex. In Rails 4, the multiline option is mandatory for the regex to work as it contains anchors such as the dollar sign:

format: { without: /^((http|https):\/\/)[a-z0-9]*(\.?[a-z0-9]+)\.[a-z]{2,5}(:[0-9]{1,5})?(\/.)?$/ix, multiline: true }

The preceding regex matches the format of the domain with numbers, letters, and also the protocol used in the URL. We created a Subdomain class, which we used in our route as a constraint. This constraint will make the www request on a subdomain nil, as www is an invalid request for a subdomain:

class Subdomain

    def self.matches?(request)

      case request.subdomain

      when 'www', '', nil

        false

      else

        true

      end

    end

end

:constraints => Subdomain

We then added a rule for searching the domain once the request is made. This will ensure that the domain is present in the database:

before_filter :load_subdomain

  def load_subdomain

    @user = User.where(:name => request.subdomain).first

  end

In case it was not found, we sent a custom routing error:

def not_found

    raise ActionController::RoutingError.new('User Not Found')

end

We also restricted all our work according to the domain owned by a user. The session is owned by a user and an organization. In order to do so, we used the concerns pattern. In Rails 4, concerns comes as a default folder. It is used to define the code that is reusable in different contexts across the application. Subdomain is defined as a module with a collection of classes and methods. This module can then be included in any controller or model of our choice. We defined a controller concern to request and extract the subdomain from the URL. We then ensured that this concern is loaded across the entire application. It is important to understand that sometimes the same concern can be used in different ways.

If we have categories across multiple models, we can create a concern for it and load it across different models. Lastly, we separated out sessions for each subdomain because each organization has a different one:

Gotable::Application.config.session_store :cookie_store, key:'_gotable_session', :domain => :all

Objective complete – mini debriefing

Adding multitenancy and reusable methods

We have already set up subdomains in order to create separate areas for each organization. However, we have to make sure that the users from one organization do not see data from another organization. A clear separation of this data visibility is called multitenancy. The concept can be compared to renting out apartments to multiple tenants. We will add multitenancy in our application by adding a simple method in our model concern.

Engage thrusters

1.    Next up, we will create the multitenant model of our application. Let's see how. We will first create a separate class to handle the tenants. This class will handle all the code related to tenancy:

  :~/gotable/app/models$ touch tenant.rb

2.    Tenant is a simple ruby class. We will first initialize a user object and extend it in the subsequent steps. We will also pass roles to the user object as we will also check the visibility according to roles:

3.  app/models/tenant.rb

4.  class Tenant

5.    def initialize user

6.      @user = user

7.    end

8.    private

9.      def admin?

10.      @user.has_role? "admin"

11.    end

12.    def owner?

13.      @user.has_role? "owner"

14.    end

end

15.          Now, we will show only the restaurants that are available to a particular organization for a particular role. For this, we will first check for the role associated with the user object. Then, we will create a scope to find the restaurants with a particular organization_idassociated with them:

16.App/models/tenant.rb

17.  def restaurants

18.    admin? ? Restaurant.all.all : Restaurant.where('organization_id = ?', user.organizations.first.id).all

  end

19.          We will then set this as a filter for our entire application. We will pass current_user from devise as the user object:

20.controllers/application_controller.rb

21. before_filter :enable_tenant

22.  def enable_tenant

23.    @current_tenant ||= Tenant.new(current_user.organization)

  end

24.          Finally, make a call in the controller and call the Tenant class before finding the value of restaurants. In this way, if a user is the owner of a restaurant, only the restaurants owned by him or her are visible to them:

25.app/views/controllers/restaurants_controller.rb

26.def index

27.  if params[:id].present?

28.     @restaurants = @current_tenant.restaurants.find(params[:id])

29.  end

end

Objective complete – mini debriefing

Quoting from Wikipedia (https://en.wikipedia.org/wiki/Multitenancy):

Multitenancy refers to a principle in software architecture where a single instance of the software runs on a server, serving multiple client-organizations (tenants).

In order to achieve this, we need to create tenants. Tenants have been extracted into a separate class which checks for the user with the role owner or admin.

We then checked for restaurants associated to a particular tenant. We first queried for the owner role as the organization is associated to a user. If the user is an admin, we return all the restaurants. The colon (:) here represents the if else condition. If the user is not an admin, we chained the query for finding restaurants over the restaurant scope. The following query finds the current user according to the organization ID for all restaurants:

admin? ? Restaurant.all.all : Restaurant.where('organization_id = ?', user.organizations.first.id).all

Rails.scoped was a method used earlier to define a chained query format. However, with Rails 4, it has been deprecated and removed. Instead, Restaurant.all behaves in the same way. In order to achieve what we previously did in Restaurant.all(select * from restaurants), we now need to either do Restaurant.all.to_a or Restaurant.all.all. We then instantiated a new tenant instance as a soon a user logs in to create current_tenant:

@current_tenant = Tenant.new(current_user)

We separated the data of each user depending on their organization_id and their role in that organization. This will help us set up a clear policy framework based on the roles of different users and their rights. This will also help us in tracking things such as billing and checking the limits of plans because all of this is calculated as per the organization.

Creating a monthly payment model, adding a free trial plan, and generate a monthly bill

The most important part of a SaaS application is the ability to bill every month. A lot of applications also give out free trials in order to allow the user to actually try out these applications before they are billed for it. This section will cover how to generate a monthly bill.However, we will not cover an actual payment gateway in this section.

Engage thrusters

1.    We will add the monthly billing code now. Add the credit card details to users table, update the parameters, and validate them:

2.  controllers/devise/registrations_controller.rb

3.  def update_sanitized_params

4.      devise_parameter_sanitizer.for(:sign_up) {|u| u.permit(:name, :organization_name, :email, :password, :password_confirmation, :plan_id,  :active, :first_name, :last_name)}

  end

5.    In order to check whether we have a free trial, we need to get the time difference between the date of joining of the user and the current date. A free trial is valid only if the difference is less than a month. This is because we want the free trial to last for one month:

6.  app/models/user.rb

7.    def date_difference(date1,date2)

8.     month = (date2.year - date1.year) * 12 + date2.month - date1.month - (date2.day >= date1.day ? 0 : 1)

  end

9.    The preceding code will return a value in months and will take two dates as parameters. In order to check whether we have a free trial, we will use the date difference method we just created. We will return a value true or false based on our condition. If it isfalse, then we will generate an error message about the trial being ended:

10.app/models/user.rb

11.  def free_trial

12.    month = date_difference(self.created_at, Date.today)

13.    if month >= 1

14.      return false

15.      errors.add(:trial_end, "Your Free Trial Has Ended, please select from a plan")

16.    elsif month < 1

17.      return true

18.    end

  end

19.          At this point, we will generate a model to record transactions and save them in our database for future references:

20.$ rails g migration transactions user_id:integer status:boolean created_at:datetime updated_at:datetime amount:decimal first_name:string last_name:string

The following code shows what our transactions table looks like:

  create_table "transactions", force: true do |t|

    t.integer  "user_id"

    t.boolean  "status"

    t.datetime "created_at"

    t.datetime "updated_at"

    t.decimal  "amount",      precision: 10, scale: 0

    t.string   "first_name"

    t.string   "last_name"

  end

21.          We will also check if there is an outstanding amount or not. We will check the last transaction date and find the date difference. If it is over a month, we will generate an error message:

22.app/models/user.rb

23.def if_amount_pending

24.    trial = self.free_trial

25.    if (trial == false && self.active?)

26.     last_transaction = Transaction.where(:user_id => self.id).last

27.     if date_difference(last_transaction.created_at,Date.today) >= 1

28.           errors.add(:pending, "You have an outstanding invoice, kindly pay at the earliest.")

29.      true

30.     end

31.    end

  end

32.          Before charging the card, we will double check if it is valid or not. Sometimes it so happens that the card is valid at the signup and expires later. In such cases, we need to check for the card validity every time it is charged:

33.app/models/user.rb

34.  def credit_card_valid

35.   date = Date.today

36.   year = self.year

37.   month = self.month

38.   expiry_month = (date.year - year) * 12 + date.month - month

39.   if expiry_month > 6

40.     true

41.    elsif expiry_month < 6

42.     false

43.      errors.add(:transaction, "Credit Card not valid")

44.   end

  end

45.          In this case, we are checking if the expiry date of a card is less than six months or not.

46.          Finally, we will put all this together to generate a transaction object. The condition needed to change a user will be he or she has a valid credit card, has finished the trial period, has an active account, and has an outstanding amount to be paid:

47.app/models/user.rb

48. def charge_credit_card

49.    trial = self.free_trial

50.    amount_pending = self.amount_pending

51.    credit_card_valid = self.credit_card_valid

52.    if (trial == false && self.active? && amount_pending == true && credit_card_valid == true)

53.      plan_id =  self.plan_id

54.      plan = Plan.where(:id => plan_id).first

55.      transaction = Transaction.new

56.      transaction.attributes(user_id: self.id, first_name: self.first_name, last_name: self.last_name, card_type: self.card_type, card_number: self.card_number, cvv: self.cvv, month: self.month, year: self.year, amount: plan.price)

57.      if transaction.save!

58.        transaction.status = true

59.        transaction.update

60.      else

61.        transaction.status = false

62.        transaction.update

63.        errors.add(:transaction, "Credit Card Could not be Charged")

64.      end

65.    end

  end

Objective complete – mini debreifing

This section is highly subjective and can also be deemed as optional. It applies to a lot of use cases where billing and invoicing is separate from the charges of the credit card. A lot of this code, especially, the way transactions and credit card details are handled, may significantly differ from what we have covered here. A lot of payment gateways store the credit card details so that we don't have to handle it for PCI compliance. There are other gateways as well that tokenize the credit card details.

Here we saw how to create a monthly charge method and check whether there are any free trials in our plans. Although this code is very generic, it can be used readily with custom methods that are specific to payment gateways written right inside these methods. Also, errors, PCI compliance techniques, the structure of the object, and validations are mostly specific to the payment gateway and hence not covered. We also looked at transactions, which are important because we need to create an object for sending to the payment gateway regardless of the provider. So, the transaction model we looked at in the preceding task serves a dual purpose of sending the information to the payment gateway and also recording the transaction in our database.

Exporting data to a CSV format

We often need to transfer data between different systems. Also, sometimes we need to send data to different people in different formats, whom we may or may not want to give our system's access directly. In order to do so, we generally export the data into formats that are commonly readable. One of the most common formats is CSV, or the comma separated values format. It's quick to export because its a text-only format and most files are small in size. Also, it is compatible with most text editors.

Engage thrusters

The final task contains steps to add the Export to CSV functionality. Let's go ahead and add them:

1.    Ruby natively supports the CSV mime type, so it is quite easy and quick to get started with. As it is available as a module of Ruby, we will make a call on it in our application.rb in order to load it for our application:

2.  config/application.rb

3.  require File.expand_path('../boot', __FILE__)

require 'rails/all

4.    CSV is like just another format for rendering and is similar to XML and HTML:

5.  app/controllers/restaurants_controllers.rb

6.  require 'csv'

7.   def export_menus

8.      @menus = Menu.where(:restaurant_id => params[:restaurant_id])

9.      respond_to do |format|

10.      format.html

11.      format.csv { render text: @menus.export_to_csv }

12.    end

  end

13.          However, there is no export_to_CSV method as yet. We will generate an array, loop over all the column names, and convert them to a CSV file:

14.app/models/menu.rb

15.require 'csv'

16.def self.export_to_CSV

17.    CSV.generate do |CSV|

18.      CSV << column_names

19.      all.each do |menu|

20.        CSV << menu.attributes.values_at(*column_names)

21.      end

22.    end

  end

23.          In order to export, we need a link to do so. This link will generate and save a CSV file:

24.config/routes.rb

25. resources :restaurants do

26.    collection do

27.      get 'export_menus'

28.    end

29.  end

30.app/views/restaurants/show.html.erb

 <%= link_to "Export to CSV", export_menus_restaurants_path(format: "csv", :restaurant_id => @restaurant.id), :class=>"btn btn-default" %>

Objective complete – mini debriefing

In this section, we saw how to generate and export a CSV file from records in our database. This helps a lot in several enterprise applications. We may extend this to other data formats as well; for example, Excel. This helps in interoperability of various systems. For example, there is an existing application Xero (xero.com) for accounting, which works in tandem with our application; this could be a very useful feature.

The CSV mime type is a part of the ActionController::Renders module. In order to use this in a controller, we included it at the top of the controller.

We then queried and copied all the values of the menus into a single array. This array of menu objects is then rendered as CSV.

Just like we define the response format in our application as HTML, JSON, or XML, CSV can also be defined as a rendering format.

As soon as we click on Export to CSV, we will get a prompt to save it as shown in the following screenshot. We extended our resource by adding a collection route to it. This collection route calls the export_menus format in order to call the export method for CSV. We can export the menu data as shown in the following screenshot:

Objective complete – mini debriefing

Mission accomplished

In this project, we created a simple SaaS-based application and saw how it is structured. We focused more on concepts of SaaS-based applications such as multitenancy, abstraction, roles and policy framework, and customizing the signup process into a wizard. We solved various issues related to the separation of data and privacy for each user. We also created plans and ways to set up a basic system with plans and pricing. Finally, we learned how to export data from the system for interoperability and work with existing applications.

Hotshot challenges

Of course, we need to enhance what we just learned. The following are some exercises that will help us do so:

·        Use Stripe to add a payment gateway

·        Add a method to cancel the billing of a user and discontinue it

·        Create the export to spreadsheet option along with CSV

·        Add errors to the payment gateway and display them in the frontend

·        Add integration tests for testing subdomains