/rails-coffee-shop

Primeiros passos em uma aplicação Rails.

Primary LanguageJavaScript

coffee-shop

O projeto contido neste repositório é para gerenciar o estoque de lojas de café com as seguintes funcionalidades:

  • Listagem do estoque;
  • Adicionar um item no estoque;
  • Atualizar a quantidade em estoque (uma compra foi realizada, salvamos a compra também);
  • (todo) Atualizar os dados de um item no estoque;
  • (todo) Excluir um item do estoque;

Com estas funcionalidades, temos um CRUD (Create, Read, Update, Delete), que em SQL é referente as operações Create, Select, Update e Delete e em REST temos o GET, POST, PUT/PATCH e DELETE.

Banco de Dados

Diagrama do Banco de Dados

Em Rails, as tabelas são criadas no plural enquanto utilizamos as classes desses modelos no singular, e.g. Product é a classe do ORM e products é a tabela a ser criada no banco.

products

  • weight: peso do pacote
  • roast: tipo de moagem (mais fino ou mais grosso)
  • ground: tipo de torra (café mais claro ou mais escuro)
  • price: preço do pacote
  • quantidade: quantidade em estoque

sales

  • product_id: ID do Product que foi vendido
  • quantity: quantidade vendida (será descontado do estoque)
  • total_price: quantidade vendida * preço do pacote

Requisitos

Git

Não é obrigatório, mas Git é um conhecimento necessário para quem deseja trabalhar com desenvolvimento de software. Você precisará ter o Git instalado em seu sistema operacional para versionar o código deste workshop. Se você utiliza Windows e instalou utilizando o Rails Installer, Git já está instalado.

Conhecimento básico em Ruby

Utilize o TryRuby.org para se familiarizar com a sintaxe do Ruby!

Rails

Rails é um framework escrito em Ruby. Para isso, precisamos ter o Ruby instalado (para Linux/Mac, eu utilizo o RVM; para Windows, tem um guia de instalação em INSTALL.md).

Após a instalação do Ruby, podemos instalar o Rails:

$ gem install rails

Dependendo da nossa versão do Ruby, a versão do Rails instalado será 4.x.x ou 5.x.x. Existem diferenças consideráveis entre as duas, mas isso não irá afeter a nossa aplicação.

Com a gem do Rails instalada, podemos utilizar os comandos $ rails <alguma coisa>.

Vamos, então, criar nossa aplicação CoffeeShop:

$ rails new coffee_shop

Isso irá criar uma pasta com todas as dependências do projeto e irá executar, automaticamente, o bundle install. Esse comando instala todas as gems padrões do Rails, que são definidas no arquivo Gemfile.

Como temos uma aplicação criada e as gems instaladas, podemos entrar na pasta do projeto, iniciar o servidor e checar se está tudo ok:

$ cd coffee_shop
$ rails server

Você pode acessar o servidor acessando localhost:3000. Para interromper a execução do servidor, pressione Ctrl + C.

A criação da estrutura inicial do projeto é alteração sufiente para criarmos um commit (não que exista limite min/máx de alterações necessárias para um commit ¯_(ツ)_/¯).

Para podermos criar um commit, criaremos um repositório na pasta do projeto:

$ git init

Você pode adicionar todos os arquivos e criar um commit chamado "Estrutura inicial" com os seguintes comandos:

$ git add .
$ git commit -m "Estrutura inicial"

Voltando a falar do projeto, iremos utilizar as configurações padrões. Logo, utilizaremos SQLite para gerenciamento do banco de dados - com isso, não iremos nos preocupar em configurar conexão nem instalar um PostgreSQL ou MySQL da vida.

Para iniciar o desenvolvimento da nossa aplicação, iremos criar os models que serão abstrações das tabelas no banco de dados.

Assim como visto pela modelagem inicial, teremos as tabelas products e sales.

$ rails generate model product weight:integer roast ground price:float quantity:integer

Serão criadas também as colunas id, created_at e updated_at, que são gerenciadas pelo ActiveRecord e sendo as duas últimas no formato de data.

Temos vários tipos de dados, onde definimos o tipo de cada atributo na criação do modelo - quando não especificamos nenhum, o tipo string é utilizado.

Rails possui muitas configurações que são feitas através de convenções, por isso iremos construir todas as models, controllers etc em inglês. Como o plural em português é bem diferente do jeito que é feito em inglês as coisas ficariam bem confusas se fizéssemos em português (e.g. se tivermos um modelo "papel" teríamos que criar uma tabela chamada "papels").

A saída desse comando vai ser algo parecido com isso:

Running via Spring preloader in process 14385
invoke  active_record
create    db/migrate/20171124001336_create_products.rb
create    app/models/product.rb
invoke    test_unit
create      test/models/product_test.rb
create      test/fixtures/products.yml

Iremos ignorar o que foi criado na pasta test/, já que não iremos falar sobre (mas testes são essenciais em uma aplicação, então leia sobre isso!). O que nos interessa agora é o que está em db/migrate/ e em app/models/.

A migração gerada contem um script do ActiveRecord para criar a tabela products e seus atributos. Esse monte de número no início do nome do arquivo é o timestamp do momento da criação da migração.

O arquivo do model app/models/product.rb só contem a definição da classe. É nesse arquivo que iremos - em alguns instantes - definir os relacionamentos e as validações referentes a entidade product.

Repetiremos os mesmos passos para criar um modelo de sale:

$ rails g model sale product_id:integer quantity:integer total_price:float

Para fazer com que essas migrações geradas sejam executadas no banco de dados (nesse caso, criar as tabelas), utilizaremos uma rake:

$ rake db:migrate

A execução dessa rake altera o nosso arquivo db/schema.rb. Esse arquivo sempre vai conter um esqueleto da situação atual do banco, então todas as migrações que alteram o banco serão refletidas nesse arquivo.

Agora que temos os modelos criados e definidos, é possível utilizar o Rails Console parar adicionar dados ao banco.

$ rails console
irb(main):001:0> Product.create(weight: 250, roast: 'dark', ground: 'medium', price: 22, quantity: 200)
  (0.1ms)  begin transaction
  SQL (0.3ms)  INSERT INTO "products" ("weight", "roast", "ground", "price", "quantity", "created_at",
  "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?)  [["weight", 250], ["roast", "dark"], ["ground", "medium"],
  ["price", 22.0], ["quantity", 200], ["created_at", "2017-11-24 00:21:17.081579"], ["updated_at",
  "2017-11-24 00:21:17.081579"]]
  (47.8ms)  commit transaction
=> #<Product id: 1, weight: 250, roast: "dark", ground: "medium", price: 22.0, quantity: 200, created_at:
"2017-11-24 00:21:17", updated_at: "2017-11-24 00:21:17">

Na execução desse Rails Console criamos um registro de Product passando seus atributos como parâmetros para o método create. A definição dessa classe está em app/models/products.rb e o método create é do ActiveRecord.

E todas essas alterações são conteúdo para mais um commit ~:D é bom sempre checar a situação atual utilizando o comando git status.

$ git status
On branch master
  Untracked files:
    (use "git add <file>..." to include in what will be committed)

    app/models/product.rb
    app/models/sale.rb
    db/migrate/
    db/schema.rb
    test/fixtures/products.yml
    test/fixtures/sales.yml
    test/models/sale_test.rb

nothing added to commit but untracked files present (use "git add" to track)

A saída desse comando nos informa que existem novos arquivos, então iremos prepará-los para commit e salvar nossas alterações.

$ git add .
$ git commit -m "Adicionados modelos de Product e Sale"

Listando do estoque

Para construir (agora de verdade) a aplicação, iremos começar pela definição das rotas. Rotas são as URLs que permitem que a gente acesse diferentes páginas da aplicação.

As rotas são definidas no arquivo config/routes.rb. Como já sabemos que queremos fazer um CRUD para products (que é o café ❤️), utilizaremos um método chamado resources, que nos permite gerar rotas para essas ações (mais sobre rotas no guias do Rails). Ao gerar os resources para product, podemos ver quais são as rotas disponíveis no sistema:

$ rake routes
Prefix        Verb   URI Pattern                  Controller#Action
products      GET    /products(.:format)          products#index
              POST   /products(.:format)          products#create
new_product   GET    /products/new(.:format)      products#new
edit_product  GET    /products/:id/edit(.:format) products#edit
product       GET    /products/:id(.:format)      products#show
              PATCH  /products/:id(.:format)      products#update
              PUT    /products/:id(.:format)      products#update
              DELETE /products/:id(.:format)      products#destroy

Cada linha dessa saída nos indica, respectivamente, qual o método que utilizamos para acessar essa rota (e.g. de acordo com a primeira linha, podemos utilizar o método products_path ou products_url para ter ter acesso a URI da listagem de products), qual o verbo HTTP essa requisição utiliza e qual ação do controller é utilizada para executar essa ação. Nesse caso, teremos um ProductsController com um método index que irá processar essa requisição.

Já que temos uma rota para acessar, podemos testá-la! Ao executar o servidor, tente acessar alguma dessas rotas. Vai dar erro, mas é ok porque ainda não existe um controller para processar isso (é sempre bom ver dar erro ao invés de já ir fazendo o que você julga estar faltando, vai dar uma ideia melhor do que está acontecendo).

Para gerar um controller, iremos utilizar um comando o rails:

$ rails generate controller products
Running via Spring preloader in process 31397
create  app/controllers/products_controller.rb
invoke  erb
create    app/views/products
invoke  test_unit
create    test/controllers/products_controller_test.rb
invoke  helper
create    app/helpers/products_helper.rb
invoke    test_unit
create      test/helpers/products_helper_test.rb
invoke  assets
invoke    coffee
create      app/assets/javascripts/products.js.coffee
invoke    scss
create      app/assets/stylesheets/products.css.scss

Essa é uma boa hora para reiniciar o servidor, já que novos arquivos não são carregados quando o servidor já está rodando. Para isso, pressione Ctrl+C na janela do terminal que o processo está sendo executado e execute $ rails server novamente.

Assim como o gerador de models, foram criados muitos arquivos que não iremos utilizar. Uma opção para evitar isso é criar o controller manualmente (é o que eu - e acho que muita gente - faz no dia-a-dia) ou configurar os geradores para fazer algo mais útil.

Iremos (novamente) ignorar os arquivos gerados na pasta test/. O conteúdo da pasta app/assets/, é referente ao CSS e JS, o padrão do Rails é utilizar CoffeeScript (mas não tenho muita certeza se a comunidade continua utilizando tanto isso) e SASS, por isso vem com essas extensões estranhas, mas também não iremos alterar esses arquivos.

O arquivo gerado em app/helper/ é utilizado para colocarmos lógica da view que não queremos que fique no HTML nem no model (que é onde algumas pessoas acabam colocando). Por hora, iremos mexer no arquivo gerado em app/controllers/ e criaremos arquivos HTML em app/views/products/.

A primeira ação que iremos construir é a de listar. Aproveitaremos os registros criados através do rails console para ter algo pra mostrar (você pode voltar para o rails console e inserir mais coisas no banco, se preferir).

Em app/controllers/products_controller.rb, criaremos um método index (que possui o mesmo nome daquele definido nas rotas - tinhamos products#index) e iremos carregar todos os registros salvos no banco:

def index
  @products = Product.all
end

Utilizar o método Product.all é o equivalente a fazer uma consulta no banco. O retorno é uma lista de objetos do ActiveRecord com registros do tipo Product.

SELECT * FROM products;

Se der treta em algum momento, evite pesquisar os erros diretamente no Stack Overflow, prefira consultar a documentação ou os guias do Rails, para poder ter uma noção melhor do que está acontecendo, isso é bem importante quando se está aprendendo uma tecnologia nova. Por exemplo, ao invés de pesquisar por como retornar os elementos únicos de um array em ruby, vá na documentação da classe Array e veja quais métodos estão disponíveis para a classe Array, você acabará descobrindo quais outras possibilidades temos com arrays em Ruby. :)

Adicionamos esses registros à uma variável @products, que possui esse @ por ser uma variável de instância (mais sobre variáveis de classe e instância aqui) e conseguimos acessá-la na view.

Por padrão, depois de executar @products = Product.all, será renderizado o arquivo em app/views/<nome do controller>/<nome da ação>.html.erb, que no nosso caso é app/views/products/index.html.erb. Os arquivos HTML com extensão .erb (Embedded RuBy) nos permitem utilizar código Ruby dentro do HTML com a ajuda de <%= %> (mais sobre ActionView templates aqui).

Todas alterações que fizemos na listagem também são conteúdo para mais um commit! Podemos adicionar todos os arquivos e fazer um novo commit com os seguintes comandos:

$ git add .
$ git commit -m "Adicionados recursos para listagem dos registros em Coffee"

Lembre-se de visualizar as suas alterações no browser, acesse localhost:3000/products pra ver essa listagem.

Adicionando um produto no estoque

Agora que temos a listagem, próximo passo é criar uma maneirao do usuário adicionar novos registros através de um formulário. Como as rotas já estão prontas, iremos direto para o controller.

A ação de criação envolve duas etapas:

  • abrir uma página com o formulário vazio;
  • receber esses dados, persistir no banco e retornar mensagem de sucesso (ou erro) pro usuário.

Para a primeira etapa, criaremos um método new no controller e iremos iniciar um objeto Product.

def new
  @product = Product.new
end

Repare que agora temos uma variável @product, que por armazenar apenas um objeto de Product - ao invés de @products que armazenava um array -, temos uma variável no singular. Isso irá ajudar na leitura do código, onde uma variável no singular guarda somente um elemento e uma variável no plural guarda um array (apenas convenção, isso não muda nada pro interpretador).

Para criar o formulário, iremos editar o arquivo app/views/products/new.html.erb e utilizaremos a gem SimpleForm para poder criar um formulário.

Antes de editar o arquivo, teremos que instalar a gem. Para isso, é só seguir os passos de instalação que estão no repositório do SimpleForm - e isso já é conteúdo para outro commit.

Voltando ao arquivo, criamos um formulário seguindo os passos iniciais indicados no repositório. Fica como dever de casa customizar mais esse formulário! Uma coisa legal seria fazer os campos de ground e roast serem um select, já que são especificações pré-definidas (e.g. a torra pode ser clara, média ou escura).

<%= simple_form_for @product do |f| %>
  <%= f.input :weight %>
  <%= f.input :roast %>
  <%= f.input :ground %>
  <%= f.input :price %>
  <%= f.input :quantity %>
  <%= f.button :submit %>
<% end %>

Ao clicar no botão de enviar, irá retornar um erro pois o form faz uma requisição para uma rota do tipo POST do formulário. Isso acontece pois o objeto em @product ainda não foi persistido - ele não possui um id -, caso contrário, a requisição iria para a rota em PUT/PATCH.

Antes de criar um registro no banco com os dados do formulário, por questões de segurança, iremos utilizar o strong params e adicionar um método private em app/controllers/products_controller.rb para validar as keys dos parâmetros, isso impede que atributos mais sensiveis do modelo sejam atualizados.

def product_params
  params.require(:product).permit(:weight, :roast, :ground, :price, :quantity)
end

O retorno desse método é um Hash, que conseguimos passar como parâmetros para o Product.new ou Product.create.

def create
  @product = Product.new(product_params)

  # o método `save` retorna true/false referente ao objeto ter sido persistido ou não.
  if @product.save
    # se der tudo ok, a gente redireciona pra listagem. ~lição de casa: adicionar flash messages
    redirect_to products_path
  else
    # irá renderizar o conteúdo de 'new', mas como o objeto `@product` possui coisas em
    # `@product.errors`, algumas mensagens de erro serão renderizadas (mas por enquanto não vai
    # cair aqui porque o model não possui nenhuma validação).
    render :new
  end
end

Iremos adicionar um link para, a partir da tela inicial, permitir o usuário ir para o formulário de criação. Para isso, Rails nos fornece o link_to. Em app/views/products/index.html.erb:

<%= link_to "Novo produto", new_product_path %>

Com isso, temos outro commit ~:D mais uma vez:

$ git add .
$ git commit -m "Adicionado formulario e acao de persistencia de Product"

Atualizando o estoque

A nossa ação de atualizar o estoque irá acontecer ao realizar uma venda ou pela chegada de mercadoria nova. No primeiro caso, iremos registrar as adições como uma venda. Para isso, já temos o model da venda criado, que é o Sale.

A venda de um produto possui duas etapas:

  • abrir página para realizar uma venda;
  • atualizar um objeto de Product e criar um novo registro em sales;

Para a primeira etapa, iremos criar uma rota para acessarmos products/<id>/sale. Voltando ao arquivo config/routes.rb, iremos adicionar uma requisição GET a essa URL:

resources :products do
  get 'sales/new', to: 'sales#new'
end

Se você executar $ rake routes novamente, verá as configurações dessa nova rota!

Como eu defini que a rota vai pra sales#new, teremos um novo controller, chamado SalesController que irá receber essas requisições no método new. Sabendo que a compra pertence a um produto, teremos sempre que salvar o :product_id para persistir junto ao Sale.

Antes de tratar essa requisição, temos que definir o relacionamento entre Sale e Product.

Sabemos que um Product possui vários Sale e um Sale pertence a um Product. O ActiveRecord possui métodos pra gente traduzir isso e adicionar aos models. Isso nos dará alguns métodos novos, como Product#sales e Sale#product.

Em app/models/product.rb:

class Product < ApplicationRecord
  has_many :sales
end

E em app/models/sale.rb:

class Sale < ApplicationRecord
  belongs_to :product
end

Agora podemos voltar ao controller! Criaremos o app/controllers/sales_controller.rb (você pode utilizar o gerador ou criar um arquivo novo, ur call).

Uma diferença em relação ao ProductsController é que sempre iremos realizar ações que são de um Product, ou seja, sempre precisaremos de um product_id em Sale. Analisando as definições de rotas, vemos que a rota que criamos possui a url products/:product_id/sales/new. Utilizaremos o :product_id que vem dos parâmetros e criaremos uma variável de instância no controller.

def set_product
  @product = Product.find(params[:product_id])
end

O método Product.find faz a seguinte query:

SELECT * FROM products WHERE id=[:product_id];

Para executar o método set_product antes da execução de todos os métodos e garantir que sempre teremos um objeto @product, adicionamos no começo do arquivo:

before_action :set_product

Assim, o método new ficará do mesmo jeito que fizemos em ProductsController.

def new
  @sale = Sale.new
end

A tela dessa ação será um formulário com apenas um campo, quantidade - que não pode ser negativa, afinal, a gente não vende -5 sacos de café.

Em config/routes.rb (e dentro de resources :products) adicionamos uma rota para receber a requisição POST com os dados da criação de uma nova venda.:

post 'sales', to: 'sales#create'

Em app/views/sales/new.html.erb:

<%= simple_form_for @sale, url: products_create_sale_path(@product) do |f| %>
  <%= f.input :quantity %>
  <%= f.button :submit %>
<% end %>

Pra acompanhar os erros que deveriam acontecer, crie a rota apenas após tentar criar o formulário sem definir a rota, para ver a mensagem que o Rails retorna.

No controller, criaremos um método de create assim como em ProductsController, mas dessa vez, queremos criar um Sale com um Product associado. Já que definimos o @product no before_action, teremos sempre acesso a esse objeto.

def create
  @sale = Sale.new(sale_params)
  @sale.product = @product

  # if save [...]
  # em caso de sucesso, redirecionamos para products_path porque *ainda* não temos outra tela.
end

private

def sale_params
  params.require(:sale).permit(:quantity)
end

Agora conseguimos criar vendas para cada produto. Rode o servidor (se já não tiver o feito - deveria, na verdade) e teste valores aleatórios e veja se está tudo sendo persistido fazendo consultas pelo rails console.

Só que a ação de venda não ocorre como esperávamos, por dois motivos: conseguimos vender produtos com quantidades negativas e o nosso estoque não está sendo atualizado.

Adicionamos a validação no model de Sales (app/models/sale.rb). Mais sobre validações no guia do Rails.

validates :quantity, numericality: { only_integer: true, greater_than: 0 }

Para persistirmos de maneira correta o Sale, iremos alterar o controller para fazer todas as ações (o certo seria fazer em um service ou afins).

def create
  @sale = Sale.new(sale_params)
  @sale.product = @product

  # calculamos o preço total multiplicando o preço de cada produto pela quantidade vendida
  @sale.total_price = (@product.price * @sale.quantity)

  # e atualizamos a quantidade de products subtraindo a quantidade vendida
  @product.quantity = (@product.quantity - @sale.quantity)
  @product.save

  if @sale.save
    redirect_to products_path
  else
    render :new
  end
end

Todas essas alterações são mais um commit! Crie um, se ainda não o fez.

Lição de casa: conseguimos salvar o Product com quantidades negativas, é uma boa adicionar essa validação também! Você fazer um before_save no model Sale pra validar se a quantidade em Product é maior ou igual a quantidade que você deseja salvar, se não, adiciona o erro em Sale#quantity avisando que essa quantidade é inválida (mais detalhes em ActiveModel::Errors#add).

Além de realizarmos a ação, temos que criar um link para podermos realizar uma venda para aquele produto a partir da página inicial! Iremos alterar a tela de products#index e para cada registro de @products, iremos adicionar esse link.

Em app/views/products/index.html.erb:

<td><%= link_to "Realizar venda", product_purchases_new_path(product) %></td>

Falando em rotas e links, a atual página inicial da aplicação é uma página padrão do Rails, dizendo "Yay! You’re on Rails!". Podemos alterar isso e definir que a página inicial é a listagem de Products. Para isso, em config/routes.rb:

root 'products#index'

E isso é mais outro commit ~:)

Excluindo um produto

Para excluir um Product, precisaremos de duas coisas:

  • um botão para excluir, a partir da listagem;
  • a ação de excluir, que fica no controller.

Já temos a rota pronta, que de acordo com $ rake routes é /products/:id. Iremos criar um método destroy em app/controllers/product.rb e ele irá excluir não só o Product, mas também todos os Sale associados a ele.

def destroy
  @product = Product.find(params[:id])

  # excluir todos os 'sales' associados
  @product.sales.delete_all

  # excluir o 'product'
  @product.delete

  # redirecionamos para o index, que é a página que estavamos inicialmente
  redirect_to products_path
end

Para o usuário realizar essa ação, adicionaremos um link na página inicial, só que dessa vez, é um link que faz uma requisição para o método HTTP DELETE. Em app/views/products/index.html.erb:

<td><%= link_to "Excluir produto", product_path(product), method: :delete %></td>

TODO:

  • ProductsController#show (com listagem de Product#sales);
  • Utilizar enum pra ground e roast - plus: change_column.
  • Adicionar auth com Devise

Algumas ideias de melhoria

(aka mais lições de casa)

  • Upload de imagem (para salvar uma foto da embalagem) utilizando CarrierWave;
  • Busca/filtro de produtos pela moagem, tipo de grão etc utilizando Ransack;

Muito do que fizemos aqui pode ser resumido com um $ rails generate scaffold, mas é muita mágica e eu não utilizar isso te ajuda entender melhor como as coisas funcionam :)

Outros recursos