These past couple weeks have been spent building the most complex project to-date. It is a Private Events application where a user can sign up, create events, and attend other events. Sounds simple on the surface, but at the database level is where it gave me a lot of trouble. Up until now, I’d only dealt with two related tables in my model. A User has_many :posts or a Post has_many :users etc. For this project, a User can create(or host) many events, a User can attend many events, and an Event can be attended by many users. This specific kind of association is called a has_many :through relationship because it requires a third database table to relate the users and events through. Then when I finally got it working in my development environment, it didn’t work on Heroku so I had to almost start all over for the production environment. Needless to say this was a time-consuming and frustrating, but rewarding learning experience. I encountered a problem at every stage of the process from development, to version control, to Heroku deployment, but persisted until the job was complete. Here is my attempted explanation of has_many :through:
First and foremost, I know I need a different type of association for this application because my Events are going to have different types of User (creator and attendee) associations, and my Users are going to have different types of Event (event and attended_event) associations. Therefore, I need to let Rails know which one I am asking for via my models. That's where the through table comes in to play. It acts as a join table and creates new columns to be referenced. In this case, an attendee_id
column for the User class and an attended_event_id
column for the Event class.
/app/models/user.rb
class User < ApplicationRecord
has_many :events, :foreign_key => "creator_id"
has_many :attendances, :foreign_key => :attendee_id
has_many :attended_events, :through => :attendances, :source => :attended_event
/app/models/event.rb
class Event < ApplicationRecord
belongs_to :creator, :class_name => "User"
has_many :attendances, :foreign_key => :attended_event_id
has_many :attendees, :through => :attendances, source: :attendee
/app/models/attendance.rb
class Attendance < ApplicationRecord
belongs_to :attendee, :class_name => "User"
belongs_to :attended_event, :class_name => "Event"
end
Looking at the code above, let's begin with the Attendance model. We associate it with the User model with a belongs_to :attendee
reference. Up in the User model, we accept this relationship with the has_many :attendances, :foreign_key => :attendee_id
reference. The foreign_key is essential because without it Rails would be looking for a column named "attendance_id". The same associations are made for Attendance and Event but with attended_event and attended_event_id. Now that our original tables are connected to the new through table, they need a connection to each other. User gets has_many :attended_events, :through => :attendances, :source => :attended_event
while Event gets has_many :attendees, :through => :attendances, source: :attendee
because remember - attended_event's class_name is Event within the attendance model while attendee's class_name is User. The source option at the end of these commands is just like class_name but for irregularly named associations in the through table.
With these associations properly set up, I was able to use constructs like @event.attendees
in my controllers and views, make model methods using syntax such as self.attendeded_events
and self.attendances.create
, and generate an attendances controller that used this method current_user.attendances.build
.
June 26 2017
- Weekly goals
- Finally finished Private Events app Heroku
- Rails interview questions
June 20 2017
- Figured out the has_many through association for Private Events app
- Leetcode remove element from array problem
def nums(arr, val)
result = arr.each do |x|
if x == val
arr.delete(x)
end
end
result.length
end
# nums([3,2,2,3], 3) => 2
# nums([3,2,2,3,5,3,1,3], 3) => 4
June 14-16, 19 2017
- Began and continued to work on Private Events app
- Worked on personal site and deployed via github-pages
June 13 2017
- Complete Hartl Tutorial User Microposts
- Leetcode common prefix problem
def common_prefix(arr)
return '' if arr.empty?
min, max = arr.minmax
idx = min.size.times{ |i| break i if min[i] != max[i] }
min[0...idx]
end
common_prefix(['predate', 'precedent', 'prefix'])
=> 'pre'
The Odin Project Blog Post 20
Week 23 - Rolling My Own Authentication
After a busy couple of weeks with illness and traveling and Hartl tutorials, I’m back to writing a blog post about rolling my own authentication system with Ruby on Rails. This past week I created a full-fledged app from scratch with the most autonomy that TOP has given it’s users as of yet. It was challenging, but not overly, and segments are still puzzling me, but there is still a ways to go in the Rails path so I expect those gray areas to become clearer with more practice. There is a lot to get into here so let’s not waste time:
After I created my app, the first step was to set up the User model. That means validations. For my Members Only app, I wanted a site that allows users to see and create posts, as well as see who the authors of the posts are, but does not allow non-users to create posts or see the authors of posts. So I used the basic model from a previous exercise and the same validations for name, email, and password_digest.
/app/models/user.rb
class User < ApplicationRecord
before_save { email.downcase! }
validates :name, presence: true
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 25 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false }
has_secure_password
validates :password, presence: true, length: { minimum: 6 }
Next, I set up the sessions controller for signing in users. Things started to get more complex here. I had two methods within the sessions controller - new and create - and my form looked like this:
/app/views/sessions/new.html.erb
Sign In
<%= form_for(:session, url: signin_path) do |f| %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.submit "Sign in", class: "btn btn-primary" %>
<% end %>
First off, sessions#new routes to /signin; that's why form_for needs to be directed there. Now for the first tricky part: setting up the user’s remember_token in ActiveRecord. I needed to add a remember_digest migration, create a remember_token attribute accessor, create a remember method to digest the token, and save it for my user. Luckily, I had already done something almost identical in the Hartl tutorial. This is what it looks like in the end:
/app/models/user.rb
class User < ApplicationRecord
attr_accessor :remember_token
before_save { email.downcase! }
validates :name, presence: true
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 25 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false }
has_secure_password
validates :password, presence: true, length: { minimum: 6 }
def User.digest(string)
Digest::SHA1.hexdigest(string)
end
def User.new_token
SecureRandom.urlsafe_base64
end
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
end
The remember method takes the remember_token attr_accessor and passes it to my User.new_token method where Secure.Random_urlsafe_base64 turns it into a random string. Remember then updates the :remember_digest attributes by passing this random string to the User.digest method and encrypting it with the Digest::SHA1.hexdigest method.
With my users' remember_token safely encrypted in the database, I moved onto the sessions creation function. In order to authenticate the user, the params method checks the email from the form, and compares it to the hashed password that’s been stored in the database for that user. If we have a match, a method within the application_controller signs them in. This sign_in function uses Rails’ session method to place a temporary cookie on the browser containing an encrypted version of their user id, which allows that user to traverse subsequent pages of the site and remain logged in. However, the session expires immediately when the browser is closed, and we want a permanent cookie that lasts until the user signs out. Thus, a second remember function was necessary to store the user id in a cookie. The end result can be seen here:
/app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
# Logs in given user
def sign_in(user)
session[:user_id] = user.id
end
# Remembers given user in persistent session
def remember(user)
user.remember
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
Now for the hardest part for me to wrap my head around: the current_user methods. These are necessary for retrieving your logged in user on subsequent pages and referencing the current user in other methods which we’ll see later. The first method retrieves a @current_user
instance variable from the database by finding the logged in user’s encrypted user id.
# Returns current signed-in user
def current_user
if session[:user_id]
@current_user ||= User.find_by(id: session[:user_id])
elsif cookies.signed[:user_id]
user = User.find_by(id: cookies.signed[:user_id])
if user && user.authenticated?(:remember, cookies[:remember_token])
sign_in user
@current_user = user
end
end
end
And the second method just returns true if the given user is equal to the current user
# Returns true if the current user is the given user
def current_user?(user)
user == current_user
end
With sign_in, remember, and the current_user methods defined, my creation method is now complete. Let's take a look at my session controller:
/app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
@user = User.find_by(email: params[:session][:email])
if @user && @user.authenticate(params[:session][:password])
sign_in @user
remember @user
flash[:success] = "You're signed in!"
redirect_to root_url
else
flash.now[:error] = "Invalid username/password. Try again."
render 'new'
end
end
def delete
sign_out
flash[:success] = "You're signed out!"
redirect_to root_url
end
end
We can see the first step within create is finding the @user by comparing the email params with the hashed password as explained earlier. But what is within the delete method? In order to successfully sign out a user, there are a few things that need to be "undone." Their sessions and cookies I created need to be deleted. So in other words, my site needs to forget who they are, and @current_user needs to be set to nil. I broke the sign_out method into two functions in the application controller:
# End session and forget user
def sign_out
forget(current_user)
session.delete(:user_id)
@current_user = nil
end
# Delete cookies
def forget(user)
cookies.delete(:user_id)
cookies.delete(:remember_token)
end
Finally, to get the delete method to work on the site, I needed to use the code <%= link_to "Sign out", signout_path, method: :delete %>
. The method: :delete
being the unique part here. This is actually JavaScript within the Rails code because web browsers cannot issue DELETE requests.
And with that, my users can sign in, be remembered with a cookie, and sign out. The other half of the app was creating posts, but for length's sake I'll stop here. I now have the ability to "roll my own auth", but honestly I look forward learning to some of the gems that will do that lifting for me.
June 8 2017
- Struggled to but eventually got sample data working on heroku for members-only app Members Only
- Leetcode Integer Palindrome problem
def is_a_palindrome?(x)
x == x.to_s.reverse.to_i
end
is_a_palindrome?(13231) => true
is_a_palindrome?(123) => false
June 7 2017
- Rails Cast Polymorphic Associations
June 6 2017
- Read Rails Guide Active Record Querying
- Read TOP's section on Advanced Active Record querying and polymorphism
- Leetcode Reverse Integer problem
def reverse(x)
arr = x.to_s.split("")
if arr[0] == "-"
arr[1..-1].reverse.join.to_i * -1
else
arr.reverse.join.to_i
end
end
reverse(321) => 321
reverse(-54321) => -12345
June 2, 5 2017
- Began work on members-only app
- Weekly goals
- Leetcode Two Sum problem
def sums(arr, target)
arr.each_with_index do |outer, x|
arr.each_with_index do |inner, y|
if arr[x] + arr[y] == target
return [x, y]
end
end
end
end
sums([2,7,11,18], 9) => [0,1]
sums([2,7,11,18], 18) => [1,2]
June 1 2017
- Completed Hartl tutorial Account Activation
- Completed Hartl tutorial Password reset