Want to learn?

The welcome word

The most fascinating in being a programmer is that you can create stunning things. And the best about being a Ruby programmer is that you can create them so easy. Just take a look at basecamp.com, twitter, github, hulu or shopify. Those are all great products, that were (well, at least initially) created in Ruby.

The awesome screencast How to build a blog engine in 15 minutes with Ruby on Rails made by DHH (and the power of used TextMate) was probably my initial point of interest. It started the fire, that made us switch from Pylons (Pyramid framework now) and Django (which is also nice, btw) to Ruby on Rails.

Learning curve was absolutely great, simple as that, out of nowhere we started to have some time to work on our own tools (none of which was ever shown to the public, to be fair) and started our interest in the SaaS market. Because how many Social Media apps one can build??

My dream from the point when I started my adventure with Rails, was to create something like Basecamp, but somehow different. I loved the product, but I was missing some features here and there. It just wasn’t working for our scenario, or maybe it’s just me who couldn’t be flexible enough.

Anyway - by creating this “course” we’ve decided, that something like basecamp clone would be a great introduction (and I’m using its name just to give you a general idea of the product we plan to build here). An introduction to all kinds of not-so-experienced Rails programmers who would like to get familiar with the technology itself.

So, what’s exactly all the fuss about SaaS?

Software as a Service (SaaS) is a web-based cloud service that offers remote access to the software (a program or an application). All you need is an electronic device with an Internet connection.

To be more specific it is a box version of ready-to-use product. A package of services that creates a running application. The difference between the traditional approach to the software management and SaaS is that the code, database and servers are maintained by the SaaS vendors.

In other words, SaaS is a box version of the product combined with the back-end requirements.

Disclaimer

We got quite a big feedback from you when we introduced this course. At first it was meant to be a series of rather short blog posts describing how we do SaaS apps. With the feedback we received, we feel that majority of users who signed up to our newsletter are more beginners, than advanced Ruby/Rails coders.

Remembering our own beginnings and knowing current Rails learning curve, we’ve decided, that this course will be dedicated to the first group. Nonetheless, if you’re a more advanced coder, you (hopefully) will still find some useful info here.

Do you think that Ruby on Rails is the only framework of choice for building SaaS applications?

No, of course not. We just know Rails quite well and it just works for us. If you plan to build your own SaaS application and you already know PHP/Python/whatever, do not learn Rails just for it - use your current knowledge.

Is your way the only “correct” way?

No, not at all. This course will be opinionated, it will use some of our proven practices, that turned out to work pretty well during the few last years of gaining experience. We absolutely don’t want to walk in the shoes of the-only-blessed-and-knowing. What we want is just to share our knowledge, show you how we do some things.

And have a little fun, of course.

Table of contents

This course is divided into several parts, each of them will be published every (other) week. Please note, that this is just a plan, some aspects may change, some may be added, some removed. As for now, it will be structured in the following way:

  1. The welcome word and app bootstraping, account model with owner via simple Membership model

    We’re going to introduce course content (as we already did) and start making basic app. We’ll begin with a simple account creation and making subdomains work.

  2. Users, Membership, Invitations

    With the new account, we’re going to create admin profile that lets to invite new users. Newly invited users should be able to register themselves, but to make an account slightly more secure, individual token should be validated. We should be able to see when an invitation was sent and when it was used (or in other words - when an invited user is registered). If the user already has an account, he should be prompted for his credentials and added as a new member to given account.

  3. Starting a real multi-tenant app: projects, permissions, to do lists and items

    Data from one account should not, by any means, be visible to members of another accounts. Fortunately, there’s a gem for that. We will let our users create projects, assign permissions to them and probably create a to-do lists and to-do items.

  4. Messages and discussions

    Users should be able to create new discussions (messages) as well as discuss already created to-do items (comment on them)

  5. Timeline and e-mail notifications

    We’ll create a timeline - a brief summary of what was happening lately. We’ll also allow users to receive one notification every night.

  6. API

    We’ll create an API for above features, to let manage their data ie. using console.

  7. Payments and Tickets

    We'll try to implement payment support as well, probably without subscriptions though.

Prerequisites

There are some prerequisites you (or, to be precise - your system) should meet:

  • you have Ruby installed properly, we’ll be working always on the newest, stable version available, as of date publishing of this post it’s 2.2.2 (2015.05.08)

  • the same applies to Ruby On Rails - you need to have it installed, the newest, stable version (4.2.1 now) (2015.05.08)

  • we’ll be using PostgreSQL (version 9.4) as database engine. In near future we’ll make a use of imagemagick

The idea of this course is to show you how things can be made. And once more - this is opinionated, etc. Remember, we’ll be happy to help you, answer your questions, and so on. However, at the same time, we’d like you to be able to solve your issues with ruby/rails/etc installations or anything similar, by your own.

Last things worth noticing

This is how the commands should look typed in a console. Every command starts with the ‘$’ - console greeting symbol, not a part of our code. We’re going to present you commands with stripped-down, simplified output. Just to show only the most important aspects. Until stated differently all command blocks assume that they run from the app root directory.

$ sample command
output
...
...
some text

Code samples will be displayed in the following way:

echo 'Hello World'
# output: 'Hello World'

Ok, let’s get this baby rolling.

Let’s create a new app. We’re gonna call it getwolfpack (but you can call it whatever you like, it’s really not that important) and use PostgreSQL as a database engine.

Why PostgreSQL?

Because of a few reasons: it’s one of the most powerful db engines out there, it’s fast, it scales well, it will be probably used in production environment (for sure on heroku.com). Plus, we’re going to use some of its more advanced features later on. I also believe that you should be developing in an environment the most similar to the production - this really saves a lot of time and spares troubles that could occur later. Of course - we could use a virtualized environment via vagrant or docker, but this would be overcomplicating things for our needs here.

Let's start coding

Open your terminal and run following command. It will create app for you, copy all required files, run bundle install after completing. Additional parameter -d informs this generator, that it should use PostgreSQL as database. If you want to learn more about this commant, run rails new -h.

$ rails new getwolfpack -d postgresql
      create
      create README.rdoc
      create Rakefile
      create config.ru
      ...

After command completes it's job, cd into directory of just created app. From this moment on we will assume, that all commands are run from within this directory:

$ cd getwolfpack

Let’s start with setting up our database connection. I like to have separate user per each app in development, so my config/database.yml file looks like this:

default: &default
  adapter: postgresql
  encoding: unicode
  username: getwolfpack
  password: getwolfpack
  host: localhost

development:
  <<: *default
  database: getwolfpack_development

test:
  <<: *default
  database: getwolfpack_test

database.yml file defines connections to db - it tells Rails how to connect to database. By default databases are named after environments in which application is run. So if you're running your app in development environment (default when writing the app), Rails will make use of development config. When you'll be writing tests, test environment will be used. You can read more about it here.

Rails can create databases for you, just run this command to create all databases listed in database.yml file:

$ bundle exec rake db:create:all

Normally we use HAML to structure HTML content. Of course, there are other choices, equally good, we just got used to HAML. Why not erb? It simple - HAML enforces cleaner markup via proper indentation. In our opinion, it’s more readable.

Add haml-rails to the Gemfile:

gem 'haml-rails'

and run bundle install

then, via html2haml tool , convert default erb layout to HAML format:

$ cd app/views/layouts/
app/views/layouts/ $ html2haml application.html.erb application.html.haml
app/views/layouts/ $ rm application.html.erb

This could be done with one command: 'rake haml:erb2haml'. It changes all your .erb views into .haml and in some scenarios this may be harmful. I belive, that it's good to know your toolset, but sometimes doing something by hand and explicite is a better way - leaves you more control.

Once we’ve made changes to the markup used by the app, let’s add Bootstrap as well. We use bootstrap because of its well known opinionated structure, that helps making responsive apps really fast. It also provides some elements, that we might be using later. To add Bootstrap libraries to our app we could do one of following - use gem or link to it directly. We will add bootstrap-sass gem to Gemfile. We could add direct links i.e. to bootstrap CDN, but by embedding it via gem we’re sure that all required libraries will be available for offline development. Before making final deploy to production, it may be worth to remove it from gemfile and link from CDN though - in this way the website will load slightly faster.

gem 'bootstrap-sass'

and then run bundle install

Now, rename app/assets/stylesheets/applications.css to app/assets/stylesheets/application.css.scss - by doing so you let Rails to know, that it should parse this file using SCSS parser.

$ mv app/assets/stylesheets/application.css app/assets/stylesheets/application.css.scss

Thanks to that we can change applications.css.scss file to load bootstrap assets:

@import "bootstrap-sprockets";
@import "bootstrap";

Also, just as a precaution and in order not to forget that in the future, load bootstrap javascript files in application.js (after loading jquery!)

//= require bootstrap-sprockets

Ok, once we have everything set up, let’s create the first controller and check if everything works as we expect (something loads)

$ bin/rails g controller welcome index

This rails generator will create a controller file, view directory with index.html.haml in it, add it to routes and generate simple test for it. Let’s make it default route.

In config/routes.rb change get 'welcome/index' into root to: 'welcome#index' . At this point we’d like to do one more thing to make our app more readable and structured better - in app/assets/stylesheets and app/assets/javascripts create directory controllers in which we will keep controller-specific asset files (controller specific javascript bindings or stylesheet declarations).

To do so, create both directories and move just generated asset files to them:

$ mkdir app/assets/{javascripts,stylesheets}/controllers
$ mv app/assets/javascripts/welcome.coffee app/assets/javascripts/controllers/
$ mv app/assets/stylesheets/welcome.scss app/assets/stylesheets/controllers/

Now require this directory in both application.js and application.css.scss files in application.js

//= require_tree ./controllers

also from application.js remove last //=require_tree .

in application.css.scss (above *= require_self)

*= require_tree ./controllers

and as with application.js - remove *= require_tree .

As an explanation - we don’t want every script to be loaded automatically - this can lead to serious and hard to debug mess in assets. By moving controller specific files to subdirectories and requiring only this subdirectory we keep some control while still having asset loading automated.

The last thing you might wanna do before checking your results is to add .container class to application.html.haml layout - it’s a top element of bootstrap based layouts - http://getbootstrap.com/css/#overview-container

!!!
%html
  %head
    %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
    %title GetWolfpack
    = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true
    = javascript_include_tag 'application', 'data-turbolinks-track' => true
    = csrf_meta_tags
  %body
    .container
      = yield

Ok, it’s about time to start our app:

$ bin/rails s
=> Booting WEBrick
=> Rails 4.2.1 application starting in development on http://localhost:3000
=> Run `rails server -h` for more startup options
=> Ctrl-C to shutdown server
[2015-04-16 16:54:47] INFO WEBrick 1.3.1
[2015-04-16 16:54:47] INFO ruby 2.2.2 (2015-04-13) [x86_64-darwin14]
[2015-04-16 16:54:47] INFO WEBrick::HTTPServer#start: pid=39737 port=3000

In your browser you should see this: Wolfpack app in browser

And just to check it from tests perspective, run

$ bin/rake test
Run options: --seed 16338
# Running:
.
Finished in 6.489450s, 0.1541 runs/s, 0.1541 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Current WelcomeController is auto-generated and checks only two things: if actions end with success response and if template for this action is generated properly. As we haven’t made any additional changes here, everything should pass as we expected. We’ll leave it as for now.

Ok, it would be good to finally add our code to code repository. We’ll use git.

$ git init
Initialized empty Git repository in /Users/mlitwiniuk/Sites/r4/saas-course/getwolfpack/.git/

Before adding new files, edit .gitignore file and add /config/database.yml to it - we don’t want our database config to be added to the repository. Sometimes, to speed up things a little, we copy config files (database.yml in this example) file to FILE_NAME.sample.

By doing such, a good and working version of the file is stored and maintained for other developers: if necessary, they can relate to it, copy or edit the file to reflect the environment. Doing this is up to you, it’s not obligatory

Let’s add all files to git repository

$ git add .
$ git commit -am “Initial commit with WelcomeController”
[master (root-commit) 4b3a261] Initial commit with WelcomeController
 62 files changed, 1001 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 Gemfile
 create mode 100644 Gemfile.lock
 ...

First model

Ok, it’s time for account model. Account will be an object representing something like organization / company. Identified by it's own subdomain it will group all data belonging to one organization. Ie. by accessing prograils.getwolfpack.com you should be able to access account of organization 'prograils'. As for now it should have only its own name and subdomain.

$ bin/rails g model Account name:string subdomain:string
      invoke active_record
      create db/migrate/20150417074811_create_accounts.rb
      create app/models/account.rb
      invoke test_unit
      create test/models/account_test.rb
      create test/fixtures/accounts.yml

Before running migration it would be a good idea to modify it a little and add a unique index for the subdomain - as an additional check, to make sure that no one will ever register a second account with the same subdomain as already existing one.

Migrations are instructions for Rails how to perform changes to database. They are always run in alphabetic orders. When generated, migrations are "timestamped" - they always start with current timestamp, so in order to find last created, just open last file in db/migrate/ directory.

Edit create_account migration like so:

t.string :subdomain, index: { unique: true }

and run migration:

$ bin/rake db:migrate
== 20150417074811 CreateAccounts: migrating ===================================
-- create_table(:accounts)
   -> 0.0421s
== 20150417074811 CreateAccounts: migrated (0.0422s) ==========================

Let’s add some validations to our model. Our account should contain both name and a subdomain. Its presence is crucial. What is more, subdomain must be validated - it should be initialized by the letter, then contain an alphanumeric or ‘dash’ symbol, and finally, end with an alphanumeric symbol. So, let’s check if app/models/account.rb meets those requirements:

validates :name, :subdomain, presence: true
validates :subdomain,
          format: { with: /\A[a-z][a-z0-9\-]+[a-z0-9]\Z/ },
          uniqueness: true

\A means beginning of the string (not the line!), \Z indicates the end of the string (once again, not the line!). This is often forgotten and skipping it may lead to security issues in some cases. To get familiar with regular expressions in Ruby, try out Rubylar - it's awesome expression tester.

We’re going to check if we meet above requirements by writing some tests. But before doing so, we’ll add FactoryGirl for generating test data - build-in fixtures are not our favorites. So in Gemfile add factory_girl_rails in development and test group. We’re adding it to development group as well, because we want factories to be generated automatically when we generate models.

group :development, :test do
  ...
  gem 'factory_girl_rails'
  ...
end

Install it and then stop spring - we have to reload it to make sure it’s (spring) aware of the new gem:

$ bin/spring stop
Spring stopped

Now, run model generator once again, this time adding the option --skip - this won`t change anything, it will generate factory for us though:

$ bin/rails g model Account name:string subdomain:string --skip
   ...
   identical app/models/account.rb
      invoke test_unit
   identical test/models/account_test.rb
      invoke factory_girl
      create test/factories/accounts.rb

Please, notice the last line of output.

Remove old fixture test/fixtures/accounts.rb and fix factory to support sequences:

FactoryGirl.define do
  factory :account do
    sequence(:name) { |n| "Account #{n}" }
    sequence(:subdomain) { |n| "subdomain-#{n}" }
  end
end

Before moving forward, let’s add minitest-rails and minitest-spec-rails gems to Gemfile to have a nice minitest support for Rails and use spec-like format.

group :test do
  gem 'minitest-rails'
  gem 'minitest-spec-rails'
end

We should require minitest/rails in our test_helper.rb - put it just below the last require:

require 'minitest/rails'

The next step is to test if our factory is valid - fortunately FactoryGirl can do this for us. Create file test/models/factory_girl_test.rb and let’s check if Factories are OK:

require 'test_helper'

class FactoryGirlTest < ActiveSupport::TestCase
  it 'lint all factories' do
    FactoryGirl.lint
  end
end

Go ahead and run it:

$ ruby -Itest test/models/factory_girl_test.rb
Run options: --seed 18063
# Running:
.
Finished in 0.090845s, 11.0077 runs/s, 0.0000 assertions/s.
1 runs, 0 assertions, 0 failures, 0 errors, 0 skips

Now it’s time to test the Account model. Let’s do this in test/models/account_test.rb

require 'test_helper'

class AccountTest < ActiveSupport::TestCase
  let(:account) { FactoryGirl.build(:account) }

  it 'validates subdomain format' do
    account.subdomain = ''
    account.valid?.must_equal false

    account.subdomain = 'ab'
    account.valid?.must_equal false

    account.subdomain = '---'
    account.valid?.must_equal false

    account.subdomain = 'ab-'
    account.valid?.must_equal false

    account.subdomain = '-ab'
    account.valid?.must_equal false

    account.subdomain = '1abc'
    account.valid?.must_equal false

    account.subdomain = 'abc1'
    account.valid?.must_equal true

    account.subdomain = 'abc'
    account.valid?.must_equal true

    account.subdomain = 'a-b1c'
    account.valid?.must_equal true
  end
end

We don’t have to check if the name is present - this is done by FactoryGirl’s lint method. What we could do though is to write additional test to ensure, that the subdomain is unique. Lets do this:

  it 'checks for subdomain uniqueness' do
    account.save!
    a = FactoryGirl.build(:account)
    a.valid?.must_equal true

    a.subdomain = account.subdomain
    a.valid?.must_equal false
  end

Run test to check if everything is ok:

$ bin/rake test
Run options: --seed 23309
# Running:
....
Finished in 0.531356s, 7.5279 runs/s, 22.5837 assertions/s.
4 runs, 12 assertions, 0 failures, 0 errors, 0 skips

It seems everything is all right. Now it may be a good time to commit last changes:

$ git add .
$ git commit -am "added account model"
[master 1f69197] added account model
 8 files changed, 117 insertions(+)
 create mode 100644 app/models/account.rb
 create mode 100644 db/migrate/20150417074811_create_accounts.rb
 create mode 100644 db/schema.rb
 create mode 100644 test/factories/accounts.rb
 create mode 100644 test/models/account_test.rb
 create mode 100644 test/models/factory_girl_test.rb

Account owner - first user of our app and basic memberships

Time to add the owner to the account. We will use devise to authorize user and its generator to generate a model and migration. Add devise to gemfile:

gem 'devise'

and run bundle install

Now run devise installer:

$ bin/rails g devise:install
      create config/initializers/devise.rb
      create config/locales/devise.en.yml
      ...

Generate user model via devise:

$ bin/rails g devise user
      invoke active_record
      ...
      route devise_for :users

Now modify user migration to add two additional fields: first_name and last_name. We tend to add such modification on top of devise migration - thanks to it, it’s more readable, plus when you operate on raw sql data those custom columns are displayed as first.

class DeviseCreateUsers < ActiveRecord::Migration
  def change
    create_table(:users) do |t|
      t.string :first_name
      t.string :last_name

Before migrating let’s add additional model - Membership. This model will be used to tell us if the user has access to the account.

$ bin/rails g model Membership account:references user:references role:string
      ...

Now modify a membership migration and force role to be always set by adding null: false to role column declaration:

t.string :role, null: false

After that, run migrations - apply changes to the database:

$ bundle exec rake db:migrate

As role should be one of predefined values, we should store them somewhere. We tend to put most of system-wide used constants in custom initializer - config/initializers/constants.rb. Create and edit this file, add constant with possible roles to it:

SYSTEM_ROLES = %w(owner admin user)

Now we can validate that in Membership model. Membership model must belong to both account and user, they should be able to validate as well:

validates :account, :user, presence: true
validates :role, inclusion: { in: SYSTEM_ROLES }

Also - let’s add relations to both Account

has_many :memberships, dependent: :destroy
has_many :users, through: :memberships

and User models:

has_many :memberships, dependent: :destroy
has_many :accounts, through: :memberships

Right now tests won’t pass, as there are some issues with factories. Let’s fix them.

In factories/users.rb we should define at least e-mail and password:

  factory :user do
    sequence(:email) { |n| "email_#{n}@getwolfpack.dev" }
    password '12345678abc'
  end

while in factories/memberships.rb we should fix associations and add role:

  factory :membership do
    account
    user
    role SYSTEM_ROLES.first
  end

Tests should pass now.

Ok, time to create account controller and add the possibility to create a new account. Let’s generate controller (without actions, we’ll add them by hand):

$ bin/rails g controller accounts
      create app/controllers/accounts_controller.rb
      invoke haml
      create app/views/accounts
      invoke test_unit
      create test/controllers/accounts_controller_test.rb
      invoke helper
      create app/helpers/accounts_helper.rb
      invoke test_unit
      invoke assets
      invoke coffee
      create app/assets/javascripts/accounts.coffee
      invoke scss
      create app/assets/stylesheets/accounts.scss

Remember about moving controller-specific assets to controller directory. I will also remove accounts_helper.rb as it won’t be needed at this point.

What happens now is that we’d like you to create an account with you as an owner assigned to it. We assume, that when the new account is created, the owner is automatically asked to provide his credentials so he could log in and manage this account at once.

We want to create account together with its owner - that is - we assume for now, that when user (site visitor) is creating new account, he should provide his credentials as well. To make is possible, we will accept nested attributes for owner - specific relation, that we have to add (Owner is user, who belongs to account with membership role == ‘owner’).

Unfortunately, we can’t define has_one :owner relation directly - we have to make it in a tricky way:

  has_one :owner_membership, -> { where(role: 'owner') }, class_name: 'Membership'
  has_one :owner, through: :owner_membership, source: :user, class_name: 'User'

First, we create relation to one owner membership that account can have. Then we create relation to User model via owner_membership. On the other hand user will have many owner_accounts records - it’s the account that has only one owner.

Before we start creating views, let’s just add simple_form gem to Gemfile to make form creation easier:

gem 'simple_form'

After installing it, run its generator to create config files and add support for bootstrap structure:

$ bin/rails g simple_form:install --bootstrap

Before anything we should add account to config/routes.rb.

resource :account, only: [:new, :create]

We specify singular resource because we won’t be listing account. We also limit actions only to new and create - we don’t need more actions right now. It's better to practice limiting something explicitly earlier, that to leave it for undefined future and, as a result, forget about it.

In the controller we should add a new action and build both account and its owner:

  def new
    @account = Account.new
    @account.owner = User.new #!
  end

Normally, with direct has_one relation you should be able to do something like @account.build_owner instead of above construction, but as this relation is “tricky”, we have to use a workaround.

Let’s add nested attributes support for owner in Account - in account model add:

accepts_nested_attributes_for :owner

Now, add account form view (in app/views/accounts/new.html.haml)

= simple_form_for @account, url: account_path do |f|
  = f.input :name
  = f.input :subdomain
  = f.fields_for :owner do |fo|
    = fo.input :email
    = fo.input :first_name
    = fo.input :last_name
    = fo.input :password
    = fo.input :password_confirmation

  = f.submit

Also , take a look at welcome/index.html.haml - let’s finally change a default text and add link to the new account form. Classes btn btn-success are provided by bootstrap and will make our link look slightly nicer.

= link_to 'Create new account', new_account_path, class: 'btn btn-success'

Make sure, that our form displays correctly - let’s write a test for it. In test/controllers/accounts_controller_test.rb add test for new action:

 it 'shows new account form' do
    get :new

    must_respond_with :success # 1
    must_render_template :new

    assigns(:account).must_be_instance_of Account
    assigns(:account).owner.must_be_instance_of User
  end

must_respond_with is a custom alias to assertion method assert_response - in our opinion must_xxx methods are more readable. We’ve added those aliases in test/test_helper.rb

module MiniTest::Expectations
  infect_an_assertion :assert_redirected_to, :must_redirect_to
  infect_an_assertion :assert_template, :must_render_template
  infect_an_assertion :assert_response, :must_respond_with
end

Now you should be able to see the form, but you can’t submit it. It’s because create action is not added yet. Add it to AccountsController:

   def create
    @account = Account.new(account_params) # 1.
    @account.owner = User.new(user_params) # 2.

    if @account.save # 3.
      redirect_to root_url
    else
      render :new
    end
  end

  private

  def account_params
    params.require(:account).permit(:name, :subdomain)
  end

  def user_params # 4.
    params.require(:account).require(:owner_attributes).permit(:email, :first_name, :last_name, :password, :password_confirmation)
  end

Normally we should create only account and accept resources for its owner (if the owner belongs directly to the account) in account_params. In our situation (with has_one :owner relation via :owner_membership) we have to deal with that differently. So, first (1.) we're initializing account from params, then (2.) we're initializing it's owner. Only single save(3.) call is required to perform all validations - Rails are clever enough to check owner (User) model as well. Also (4.) - in user_params method we require params to have the owner - so it’s not possible to have an account created without the owner. We have defined accepts_nested_attributes_for :owner method in model - thanks to that Rails will perform validations on owner as well.

Let’s test it (accounts_controller_test.rb). Before making new account let’s build hash of attributes, that will be sent in form.

  let(:account_attributes) do
    FactoryGirl.attributes_for(:account).
      merge(owner_attributes: FactoryGirl.attributes_for(:user))
  end

Method let with its block is basically equivalent of following method declaration (this is some simplification, but we can live with it):

  def account_attributes
    FactoryGirl.attributes_for(:account).
      merge(owner_attributes: FactoryGirl.attributes_for(:user))
  end

let is just more readable in tests in our opinion.

So let's test create action of AccountsController - add this new test to accounts_controller_test.rb:

  it 'creates new account' do
    lambda {
      lambda {
        post :create, account: account_attributes
      }.must_change 'User.count', 1
    }.must_change 'Account.count', 1

    must_respond_with :redirect
    must_redirect_to root_url
  end

Ok, this proves, that Account and User are in fact created, after that user is redirected to root_url. Let’s check if system reacts properly, or if there's something wrong with the form itself:

  it 'renders form for invalid params' do
    post :create, account: { owner_attributes: { first_name: '' } }

    must_render_template :new
  end

Dashboard for authenticated users

Now we can create new accounts. It would be great if their owner could also sign in to new account. All important actions are going to happen in accounts dashboard - there in the future we will be displaying timeline with recent activity. Let’s create a controller for it. Accessing it will require user to be authenticated.

$ rails g controller dashboards
 ...

As previously, remember about moving controller-specific assets.

$ mv app/assets/javascripts/dashboards.coffee app/assets/javascripts/controllers/
$ mv app/assets/stylesheets/dashboards.scss app/assets/stylesheets/controllers/

You may also remove generated helper file (rm app/helpers/dashboards_helper.rb).

Add routes for it, for now we will only have show action

resource :dashboard, only: [:show]

And define show action in controller:

def show
end

and create view file (app/views/dashboard/show.html.haml). Add whatever you want to it.

If you go to localhost:3000/dashboard it should work. Ok, let’s show this view only to authorized users. On top of dashboards_controller.rb add before_action

before_action :authenticate_user!

before_action method is fired on every request before getting into action methods. If method called will return any type of response methods (ie. redirect_to), the whole chain will be stoped and response will be sent to user. :authenticate_user! is method provided by devise which checks if user is signed in and if not - it redirects user to sign in form.

After applying above change, when you will try to show this view again, you will be redirected to sign in form.

When you take a closer look at sign in form, you’ll notice, that a user is able to sign up as well. This is not expected behavior right now (as user would sign up to the system, but wouldn’t have an account assigned to it). Let’s block it for now. To do so in User model remove :registerable from devise method call. Voila, magic happens.

It still does not make much sense to access dashboard via /dashboard link. It should be available when you visit root url of your account (ie. prograils.getwolfpack.com/ ). Let’s start with requiring to have subdomain present to access dashboard. Remove resource :dashboard line from routes.rb and add following block:

  constraints SubdomainRequired do
    root to: 'dashboards#show', as: 'subdomain_root'
  end

Constraint make sure, that given routing block is available only, when certain criteria are met. Here - SubdomainRequired must check if subdomain is present. On top of routes.rb define this constraint:

class SubdomainRequired
  def self.matches?(request)
    request.subdomain.present? && request.subdomain != "www"
  end
end

Now accessing localhost:3000/dashboard shouldn’t work anymore. Small hint here. As you probably see, anything.localhost:3000 won’t work. You can use one of domains, that is resolving always to localhost, like lvh.me, or wait for it... prograils.io ;) So visiting prograils.io:3000 should call your welcome controller, while anything.prograils.io:3000 should give you login form. Try that.

Time to force our users to visit only their accounts. For now, unless we fully implement memberships and invitation, we will redirect user to their first account (as they can’t create a second one anyway). Such global checks should be done in ApplicationController - edit it and add the following code block:

 before_action :redirect_to_subdomain # 1.

  private

  def redirect_to_subdomain
    return unless user_signed_in? # 2.

    if request.subdomain.blank? # 3.
      if current_user.accounts.any?
        redirect_to root_url(subdomain: current_user.accounts.first.subdomain)
      end
    else # 4.
      unless current_user.accounts.pluck(:subdomain).include?(request.subdomain)
        redirect_to root_url(subdomain: current_user.accounts.first.subdomain)
      end
    end
  end

What it does:

  1. It runs redirect_to_subdomain method on every request.

  2. It skips further checking if user is not signed in.

  3. Otherwise it checks, if subdomain exists. If not, user is redirected to root_url of his first account.

  4. If subdomain exists, system checks if it belongs to the account of which user is a member. If not, he’s redirected to his account.

It’s about time to check if everything we’ve done so far is ok. Before moving forward with tests, let’s just apply additional feature, that will help us with testing a code, that requires users to be signed in. Let’s monkey-patch ActionController::TestCase in test_helper.rb:

class ActionController::TestCase
  include Devise::TestHelpers
end

So… in dashboards_controller_test.rb let’s check if user is redirected to sign_in form if there is subdomain present

  it 'redirects to sign in user is not signed in' do
    request.host = 'subdomain.prograils.io'

    get :show

    must_respond_with :redirect
    must_redirect_to new_user_session_url(subdomain: 'subdomain')
  end

That should be it. Now we should check if the system behaves properly if a user is signed in and trying to access his account. He should see show action of DashboardController.

Start with adding lazy factories (via let) to test:

describe DashboardsController do
  let(:user) { FactoryGirl.create(:user) }
  let(:account) { FactoryGirl.create(:account, owner: user) }
  # ...

then let's add basic test

  describe 'for signed in user' do
    before do
      account # preload
      sign_in user
    end

    it 'renders show template if user is signed in' do
      request.host = "#{account.subdomain}.prograils.io"
      get :show
      must_respond_with :success
      must_render_template :show
    end
  end

Now let’s check if a user is redirected to his domain, if he’s signed in, but when subdomain is missing. Add following block in describe 'for signed in user' block defined above:

   it 'redirects to subdomain if subdomain is absent for signed in user' do
     request.host = 'prograils.io'
     get :show
     must_redirect_to "http://#{account.subdomain}.prograils.io/"
    end

And finally, check if signed in user is being redirected to his account, when he’s trying to access different account (also put it in same describe block):

    it 'redirect to proper subdomain' do
      request.host = "wrong.prograils.io"
      get :show
      must_redirect_to "http://#{account.subdomain}.prograils.io/"
    end

Let’s run tests one last time (in this part)

$ bin/rake test
Run options: --seed 58678
# Running:
...........
Finished in 4.036164s, 2.7254 runs/s, 6.6895 assertions/s.
11 runs, 27 assertions, 0 failures, 0 errors, 0 skips

Regarding tests and order of writing them - we know, that this is not fully TDD. But full TDD is hard to achieve, sometimes is counter-productive and hard to make in this course. Our main idea is not to teach you how to do tests, you can find plenty of them in the Internet. We belive that this simplification is ok enough to be continued in the next part, while it still puts some impact on testing and shows where they are important. Generally, you should write tests before writing the code, that proves they are ok. Generally.

See you next time.

P.S. Do not forget to add all files to Git and then commit them.

If you have problems, we have published app in the same state on github - this links to tagged commit and should contain more or less same code, as presented in this chapter.

Check our latest product - it's based on our experience of managing over 50-people strong company. The tool we're missing as a small company and not an enterprise.

humadroid.io is an employee and performance management software. It's an unique tool allowing everyone to be in the loop - by having up to date info about co-workers, time-off, benefits, assets, helping with one-on-ones, being a go-to place for company-wide announcements.

Check out humadroid.io
Top

Contact us

* Required fields

The controller of your personal data provided via this contact form is Prograils sp. z o.o., with a registered seat at Sczanieckiej 9A/10, 60-215 Poznań. Your personal data will be processed in order to respond to your inquiries and for our marketing purposes (e.g. when you ask us for our post-development, maintenance or ad hoc engagements for your app). You have the rights to: access your personal data, rectify or erase your personal data, restrict the processing of your personal data, data portability and to object to the processing of your personal data. Learn more.

Notice

We do not track you online. We use only session cookies and anonymous identifiers for the purposes specified in the cookie policy. No third-party trackers.

I understand
Elo Mordo!Elo Mordo!