The Fun Drink Recipe App is built with Ruby on Rails framework. This web application is for drink lovers to find and create ingredients and recipes from cocktails to milkshakes. Users can also comment and rating on any ingredient. Besides CRUD operations, the app also provides name search and filter functionality.
Models and Associations
There are five models in my project - a User
model, an Ingredient
model, a Drink
model, a Measurement
model and a Review
model. The associations among these models are shown as below (created with Draw.io):
Data Validations
Besides built-in validation methods, I wrote custom validation methods the attributes in my models. For example: the user's input of a drink name will be automatically title cased before the data saved into the database.
# app/models/drink.rb
class Drink < ApplicationRecord
before_validation :make_title_case, :set_defalut_thumb
def make_title_case
self.drink_name = drink_name.titlecase
end
end
Also, if users do not provide an image URL, they would be given a default one by the system.
# app/models/drink.rb
def set_defalut_thumb
if self.drink_thumb == nil || self.drink_thumb == ""
self.drink_thumb = "https://www.thecocktaildb.com/images/media/drink/mrz9091589574515.jpg"
end
end
Scope Methods to Query Data
A scope is an efficient way to filter an application using ActiveRecord queries or SQL. It's also easy to write a WHERE, ORDER, LIMIT, etc. with just one line.
In my project, I defined several scope methods for querying data from the database to implement search and filter functionality. Below is an example of how I implemented name search with a scope method:
# app/models/item.rb
scope :item_search, ->(name) { where("name LIKE ?", "%#{name.titlecase}%") }
On the homepage, the data of latest alcoholic and nonalcoholic drinks are also found with two scope methods:
# app/models/drink.rb
scope :alcohol_drink_filter, -> { Drink.joins(:item). where(item: {alcohol: "yes"}).last(3) }
scope :non_alcohol_drink_filter, -> { Drink.joins(:item). where(item: {alcohol: "no"}).last(3) }
User Authentication
My application provides standard user authentication, including signup, login, logout, and secure passwords. The bcrypt
gem I used helps generate salt and hashed passwords, while the has_secure_password
method that is provided by ActiveRecord saves the passwords in the database and authenticates users.
There are two main methods we get from the #has_many_password method:
(1) The password=
method allows us to create a user with a password attribute rather than a password_digest
attribute. The method uses Bcrypt
gem to hash and salt the password and then save it in the password_digest
column in our table. Note that we don’t have to validate the password manually in our code. #has_secure_password validates the password automatically when being invoked.
(2) The authenticate
method takes in the password a user enters when logging in and uses Bcrypt
to hash and salt the input and compare it to what is saved in the password_digest
column for that user.
Log In with a Third Party
The authentication system of my app also allows users to login from Google. To implement this functionality we need three gems in total -
(1) omniauth
- this gem helps us log in with a third party
(2) dotenv-rails
- thie gem ensures that the environment variables are correctly loaded into the ENV hash in a secure manner
(3) omniauth-google-oauth2
We also need to add two files -
.env
- this file is where you save your ‘GOOGLE_CLIENT_ID’ and ‘GOOGLE_CLIENT_SECRET’ key-value pairs. Don’t forget to save it into the gitignore file!
/config/initializers/omniauth.rb
- this files is to tell Rails to use OmniAuth to Authenticate our user.
# /config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
provider :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET'], :skip_jwt => true
end
Then set up the route -
# config/routes.rb
get '/auth/:provider/callback', to: 'sessions#omniauth'
Note that we don’t have a get ‘/auth/google’ route set up because Omniauth uses Rack Middleware to intercept the request to ‘/auth/google’.
Once the user clicks 'Login with Google’, it momentarily sends the user to mysite.com/auth/google, which quickly redirects to the Google sign-in page.
After the user agrees to let mysite.com access the user’s information, the user is redirected back to
mysite.com/auth/google/callback, and from there, to the page they initially tried to access.
Nested Resources
Making use of a nested resource with the appropriate RESTful URLs has helped me keep my routes neat and tidy. It is better than dynamic route segments for representing parent/child relationships in the system. Note that do not nest more than one level deep.
In my app, I included nested a new measurement route to the drinks resource - new_drink_ingredient_path(@drink)
And nested review
routes to the ingredients
resource -
new: new_ingredient_review_path(@ingredient)
index: ingredient_reviews_path(@ingredient)
show: ingredient_review_path(@ingredient, @review)
Below is how I created the nested index review
route to the ingredients
resource step by step. Note that the URL helper generated for each nested route will accept at least one argument.
Step 1 - set up nested routes in config/routes.rb
:
resources :ingredients, only: [:show] do
resources :reviews
end
Step 2: make changes in the reviews#index
controller action. Add if-else statement to reuse the controller action:
# app/controllers/reviews_controller.rb
def index
if params[:ingredient_id]
@ingredient = Ingredient.find_by(id: params[:ingredient_id])
@reviews = @ingredient.reviews
else
@reviews = Review.all
end
end
If the user comes from nested routes (/ingredients/:id/
) and makes a index
request (/ingredients/:id/reviews
), the ingredient_id
key-value pair would be automatically saved in the params
hash. Else the user is determined coming from the review index page (/reviews
). That’s why we check the user’s http request by params[:ingredient_id]
.
Step 3 - make changes to links in views:
# app/views/ingredients/show.html.erb
<%= link_to "Add a Review", ingredient_reviews_path(@ingredient) %>
Step 4 - make changes in the index
form:
# app/views/reviews/_form.html.erb
<div class="container">
<% if @ingredient %>
<h3>All Reviews on <%= @ingredient.name %></h3>
<%= render partial: "reviews/reviews", locals: {reviews: @ingredient.reviews } %>
<%= link_to "Return #{@ingredient.name}", ingredient_path(@ingredient), class: "btn btn-info btn-sm" %>
<%else%>
<h3>All Reviews</h3>
<%= render partial: "reviews", locals: {reviews: @reviews} %>
<%end%>
</div>
DRY Code
Writing appropriate partials
and helpers
are crucial to DRY up the code and follow the single responsibility principle. Practicing more is extremely important to acquire these techniques.
Further Thoughts
During the project, one of my biggest challenge is how to build up the most effective and workable model relationships. I would like to do more researches on the model associations such as has_one :through
and has_and_belongs_to_many
later on. Furthermore, I want to practice set up the forms with
the #form_with method to substitute form_for
and form_tag
.
Top comments (0)