Skip to content

Instantly share code, notes, and snippets.

@luizfonseca
Last active July 17, 2017 22:35
Show Gist options
  • Save luizfonseca/1876b56f02fa9a048d8566b9e68d8eb0 to your computer and use it in GitHub Desktop.
Save luizfonseca/1876b56f02fa9a048d8566b9e68d8eb0 to your computer and use it in GitHub Desktop.
Cookbook Day II
# Requiring it because Rake tests controllers
# individually
require_relative 'view'
require_relative 'recipe'
require_relative 'parser'
class Controller
def initialize(cookbook)
@cookbook = cookbook
@view = View.new
# We are using a Parser class now, to allow
# users to import/search recipes based on
# ingredients
@parser = Parser.new
end
# CRUD: Read
# List recipes
def list
show_recipes
end
# CRUD: Create
def create
name = @view.ask_for_name
description = @view.ask_for_description
# Instantiating a new Recipe instance, so
# we can talk to it (.name, .description and so on)
recipe = Recipe.new(name, description)
# Adding it/Synchronizing it with the repo
@cookbook.add_recipe(recipe)
end
# This method is being called by the Router file
# see router.rb
def import
# Get user input
ingredient = @view.ask_user_about_ingredient
# Fetch data from a specific website
import_ingredient_recipes(ingredient)
end
# CRUD: Delete
def destroy
show_recipes # => calling the private method
recipe_index = @view.delete_recipe
@cookbook.remove_recipe(recipe_index)
end
private
# defining a method that has a common behavior
# so we can reuse it inside other methods
def show_recipes
recipes = @cookbook.all
@view.list_recipes(recipes)
end
# NOTE: We are making it private because only the controller can call it!
def import_ingredient_recipes(ingredient)
# Call the Parser method to fetch new recipes based on the given
# ingredient. Note that this method returns Recipe instances, so its
# easier for us to talk to them (.name, .description, .cooking_time)
results = @parser.search_recipes(ingredient)
# Display the results we just go from the Parser class
# see parser.rb
@view.display_import_results(results)
# Getting the user input as a Fixnum/Integer
# So we can match with the index in the results
recipe_index = @view.ask_for_which_recipe_to_import
# Call for save the imported recipe
@cookbook.add_recipe(results[recipe_index])
end
end
# This class is the repository,
# where we store things and define methods
# related to storing/retrieving
require 'csv'
require_relative 'recipe'
class Cookbook
# This method initializes with a path
# to a csv file (see router.rb)
def initialize(filepath)
@recipes = []
@filepath = filepath
# At the start, we get all recipes from CSV
# and transform them to Recipe objects
# So listing them works just fine
CSV.foreach(@filepath) do |row|
# Make the CSV synchronize with the @recipes array
# So we can type "1" and list all the recipes from the
# CSV file
@recipes << Recipe.new(row[0], row[1], row[2], row[3])
end
end
# Just return all recipes (instance method)
def all
@recipes
end
# Add a recipe to the Recipes Array
def add_recipe(recipe)
@recipes << recipe
# This method saves the recipe to the CSV
# after we add it to the array
write_recipes # reusing a common instruction
end
# Remove a recipe from the Recipes Array
# and update the CSV afterwards
def remove_recipe(recipe_index)
@recipes.delete_at(recipe_index)
write_recipes # reusing a common instruction
end
private
# Common instruction: it updates the CSV
#
def write_recipes
CSV.open(@filepath, 'wb') do |csv|
@recipes.each do |recipe|
# UPDATE: we add new fields to the initialize
# now that we have more of them
# See line 19, we update it as well
csv << [recipe.name, recipe.description, recipe.cooking_time, recipe.difficulty]
end
end
end
end
# We need to require Recipe because we are using
# it on this file
require_relative "recipe"
# Using nokogiri and openning urls require these
# 2 libraries
require "nokogiri"
require "open-uri"
# This class deals with getting recipes from
# The website Letscookfrench. We are using Nokogiri, which
# allows us to walk over the HTML document (and its elements)
class Parser
# In order to use this method, we can do
# parser = Parser.new -> parser.search_recipes('strawberry')
def search_recipes(ingredient)
# Initialing an empty array before the loop, so we
# can store the results there
results = []
# I am storing this outside because we can than change this for another search
# afterwards
url = "http://www.letscookfrench.com/recipes/find-recipe.aspx?s=#{ingredient}&type=all"
webpage = Nokogiri::HTML(open(url), nil, 'utf-8')
# We got the entire document on the variable called WEBPAGE
# We can then use .search on it, asking for specific elements
# See nokogiri documentation, it's kinda tricky, but you will get
# the hang of it
webpage.search(".m_contenu_resultat").each do |element|
# We ask for each element based on INSPECT on the browser
# .search is chainable, so you can .search().search() until
# you find something you need
title = element.search('.m_titre_resultat a').text
description = element.search('.m_texte_resultat').text
cooking_time = element.search('.m_detail_time div ~ div').text
# This input comes as Vegan - Very Easy - Moderate
# So we use .split('-') so we can get then separated, and then
# we get the 3rd item from it (comes after Very Easy in the example above)
difficulty = element.search('.m_detail_recette').text.split("-")[2].strip
# Add a new instance of Recipe to the results array
# You see, we dont add strings or numbers etc
# We add Recipe instances so we can deal with them just as
# simple. We can then call all the Recipe methods on other places
results << Recipe.new(title, description, cooking_time, difficulty)
end
# Just returning the results (multiple Recipe)
return results
end
end
class Recipe
# define that :name and :description, cooking_time, difficulty
# can be read, but no written
attr_reader :name, :description, :cooking_time, :difficulty
def initialize(name, description, cooking_time, difficulty)
@name = name
@description = description
@cooking_time = cooking_time
@difficulty = difficulty
end
end
class Router
def initialize(controller)
@controller = controller
@running = true
end
def run
puts "Welcome to the Cookbook!"
puts " -- "
while @running
display_tasks
action = gets.chomp.to_i
print `clear`
route_action(action)
end
end
private
def route_action(action)
case action
when 1 then @controller.list
when 2 then @controller.create
when 3 then @controller.destroy
when 4 then @controller.import
when 5 then stop
else
puts "Please press 1, 2, 3 or 4"
end
end
def stop
@running = false
end
def display_tasks
puts ""
puts "What do you want to do next?"
puts "1 - List all recipes"
puts "2 - Create a new recipe"
puts "3 - Destroy a recipe"
puts "4 - Import recipes from [Marmiton|LetsCookFrench]"
puts "5 - Stop and exit the program"
end
end
# This class is responsible for only:
# Displaying (puts) and
# Receiving (gets) user inputs/outputs
# ITS only related to recipes controller
class View
def list_recipes(recipes)
if recipes.empty?
puts "You have no recipes!!"
else
recipes.each_with_index do |recipe, index|
puts "#{index + 1} - #{recipe.name} (#{recipe.description})"
end
end
end
def ask_for_name
puts "What's the name of your recipe?"
print "> "
gets.chomp
end
def ask_for_description
puts "What's the description of your recipe?"
print "> "
gets.chomp
end
def delete_recipe
puts "What recipe do you want to delete?"
print "> "
gets.chomp.to_i - 1
end
def ask_user_about_ingredient
puts "What ingredient would you like a recipe for?"
print "> "
return gets.chomp
end
def display_import_results(recipes)
puts "#{recipes.size} results found!"
recipes.each_with_index do |recipe, index|
puts "#{index + 1}. #{recipe.name} | #{recipe.cooking_time}"
end
end
def ask_for_which_recipe_to_import
puts "Please type a number to choose which recipe to import"
print "> "
return gets.chomp.to_i - 1
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment