Fun Drink Recipes App
Tags: beginners, ruby, rails
Published: 2021-09-03 22:48:03

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](https://app.diagrams.net/)):
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2v00iuxbdu0ytrkf95kf.JPG)

**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`.