For my first Ruby on Rails project, I chose to build an app I called: Chamption: A Playbook Manager for Football Coaches. Champion allows a football coach to create a collection of playbooks, add plays to them, create opponents and schedule games against their opponents.
The project turned out to be larger than I expected. Initially, I thought I would just do the "playbook" portion, but as I went along and thought things through, I added the "games" piece and then things just kept expanding. In church, we would call this 'mission creep.' So, I had to reign myself in and focus on the core requirements from Flatiron.
Here is a video of the app that introduces the core concepts:
To exemplify some of the code, let's look at how you create a user and also add a profile for that user. The user is a coach, and when they create an account, they can add some additional details that are stored in a separate table. If they choose to log in with Facebook, an empty profile is created for them. First, let's look at how the models are configured:
class User < ApplicationRecord
has_secure_password
has_one :profile, dependent: :destroy
has_many :playbooks
has_many :plays, -> { distinct }, through: :playbooks
has_many :games
has_many :opponents, through: :games
validates :name, presence: true
validates :name, length: { minimum: 2 }
validates :email, presence: true
validates :email, uniqueness: true
validates_format_of :email,:with => /\A[^@\s]+@([^@\s]+\.)+[^@\s]+\z/
accepts_nested_attributes_for :profile
end
You will notice that the user model uses has_secure_password
in order to
use bcrypt
to encrypt the password. If the user comes from Facebook,
a dummy password is supplied just to make sure it passes the validations.
Also, you will notice that a number of other validations are set up to
ensure the user name is of a certain length and the email is valid and
unique. The email address serves as the user's identity. Finally, notice
that a user has one profile and can accept nested attribues for profiles.
Now, let's look at the profile model:
class Profile < ApplicationRecord
belongs_to :user
end
This one is super simple. It simply belongs to a user. This, along with the relationship set up in the user model, sets up the one to one relaionship with a user, so that a user has one and only one profile.
Most of the work of creating a new user is handled in the user controller, so let's look at that:
class UsersController < ApplicationController
before_action :require_login
skip_before_action :require_login, only: [:new, :create]
def new
@user = User.new
@user.profile = Profile.new
end
def create
@user = User.new(user_params(:name, :email, :nickname, :password,
profile_attributes: [:role, :nickname]))
if @user.save
session[:user_id] = @user.id
redirect_to @user
else
render 'new'
end
end
def show
if params[:id].to_i == current_user.id
@user = current_user
else
redirect_to current_user
end
end
def edit
@user = User.find(params[:id])
end
def update
@user = User.find_by(id: params[:id])
@user.update(user_params(:name, :email, :nickname,
profile_attributes: [:role, :nickname]))
redirect_to user_path @user
end
def index
@users = User.all
end
private
def user_params(*args)
params.require(:user).permit(*args)
end
def require_login
return head(:forbidden) unless logged_in?
end
end
In this code, you are required to be logged in for all the routes except for the new and create routes. These are the ones of interest here. You don't need to be logged in for those, becuase you are creating a new account, hence can't be logged in, right? If you look at the create method, if creates a new user based on the input the user provided, and if the user can be saved, it then logs that user in and displays the user's show page, which is their profile page. If the user can't be saved, that means one of the validations didn't pass, so the create form is shown again with errors displayed.
To see how the errors are handled, let's look at the new user form:
<div class='row justify-content-center'>
<div class='col-5'>
<h1 class='display-22'>Sign Up</h1>
<%= render partial: 'shared/error_messages', locals: { item: @user } %>
<%= render partial: 'form', locals: { submit_msg: 'Create Coach' } %>
<p>
Already have an account? <%= link_to "Log in here.", login_path %>
</p>
<p><%= link_to('Or log in with Facebook!', '/auth/facebook') %></p>
</div>
</div>
Here I made use of partials. The first partial is used across all forms to show errors. It is stored in the shared folder and passed the object that has errors as a local variable. This allows the error partial to be generic. Here it is:
<% if item.errors.any? %>
<div id="error_explanation">
<div class="alert alert-danger">
<p>
The form contains <%= pluralize(item.errors.count, "error") %>.
</p>
<ul>
<% item.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
</div>
<% end %>
Finally, let me show you the partial for the new user form itself. It uses a local variable to customize the text on the submit button:
<%= form_for @user do |f| %>
<div class="form-group">
<%= f.label 'Name' %>
<%= f.text_field :name, class: 'form-control' %>
</div>
<div class='form-group'>
<%= f.label 'Email' %>
<%= f.text_field :email, class: 'form-control' %>
</div>
<%= f.fields_for :profile do |ff| %>
<div class='form-group'>
<%= ff.label 'Nickname' %>
<%= ff.text_field :nickname, class: 'form-control' %>
</div>
<div class='form-group'>
<%= ff.label 'Role' %>
<%= ff.text_field :role, class: 'form-control' %>
</div>
<% end %>
<div class="form-group">
<%= f.label 'Password' %>
<%= f.password_field :password, class: 'form-control' %>
</div>
<%= f.submit submit_msg, class: 'btn btn-primary' %>
<% end %>
This project was challenging for me - and I learned a lot. I started off trying to use bootstrap which turned out to be a daunting learning experience in and of itself. And then the general scope of my project expanded, and I had to learn a lot more stuff. I must say I like learning new things...but I also like getting something done. At times, these interests were in conflict. I reminded myself of one of Steve Job's famous qoutes: "Real artists ship." So, now, I'm done!