José Sobral
Posted on September 25, 2020
Você já tentou criar uma aplicação web que permitisse upload de fotos? Nós, da Reserva INK já e gostaríamos de partilhar sobre a experiência de como criar uma feature iradíssima em Rails =)
Feature: Galeria de fotos
1-Usuários fazem upload de suas fotos
2-Relacionam a suas camisetas
3-Exibimos uma galeria de fotos e as respectivas imagens associadas a cada foto
Toolkit: Shrine
Neste artigo, contaremos um pouco mais sobre como instalar o Shrine e como utilizamos ele para criar a Galeria de Fotos da INK. Toda a aplicação desta feature foi escrita no paradigma MVC (Model View Controller), em Rails e tentaremos passar em detalhes a construção de cada parte do código.
Lembrando que a nossa intenção é muito mais compartilhar o conhecimento do que se mostrar referência na linguagem. Dito isso, se você tiver quaisquer sugestões de como poderíamos fazer melhor cada um dos passos a seguir ou apenas quiser conversar sobre a feature, estamos de braços abertos para receber feedbacks através do jose.sobral@reserva.ink
Mãos ao código!
Como instalar o shrine do zero
Como configuramos em nossa aplicação
a) Model
- Modelagem de dados
- Criando os models no rails
- Utilizando o shrine para guardar as imagens
b) View
- Como criar o layout do form que guardará suas imagens
- Relacionar fotos e camisetas
c) Controller
- Criando a galeria de cada loja
- CRUD de fotos
- Relacionamento de fotos-camisetas
1-Como instalar o shrine do zero
A instalação do Shrine é bem tranquila e segue basicamente dois passos:
a) Gemfile
gem "shrine", "~> 3.0"
b) config > initializers > shrine.rb*
require "shrine"
require "shrine/storage/file_system"
if Rails.env.development?
Shrine.storages = {
cache: Shrine::Storage::FileSystem.new(“public”, prefix: “uploads/cache”)
store: Shrine::Storage::FileSystem.new(“public”, prefix: “uploads”),
elsif Rails.env.Production?
Require ‘shrine/storage/s3’
s3_options = {
***Suas credenciais do S3***
}
Shrine.storages = {
cache: Shrine::Storage::S3.new(prefix: ‘cache’, **s3_options), # temporary
store: Shrine::Storage::S3.new(prefix: ‘store, **s3options), # permanent
}
end
Shrine.plugin :activerecord # loads Active Record integration
Shrine.plugin :cached_attachment_data # enables retaining cached file across form redisplays
Shrine.plugin :restore_cached_data # extracts metadata for assigned cached files
Diferenciamos o Storage para o ambiente de Desenvolvimento e para o ambiente de Produção.
- Quando a feature está no ar, o armazenamento de dados ocorre através do S3.
- Em desenvolvimento, o upload é realizado para um banco local.
2-Como configuramos em nossa aplicação
a) MODEL
- Modelagem de dados
Para início de conversa, falemos sobre como a modelagem de dados foi feita. No ínicio, sabíamos que teriam três fluxos de dados através das entidades:
- Galeria
- Fotos
- Relacionamento fotos-camiseta
Por isso, quando começamos o projeto pensamos em criar três Models:
Gallery
Pictures
GalleryRelatedProducts
Gallery
Estruturando o problema:
- Cada seller teria sua própria galeria
- Por enquanto, na nossa arquitetura atual, cada loja teria apenas uma galeria
Decisão
ID | Store_id
Com esta modelagem, cada loja teria sua própria galeria (Store_id
) e, no futuro, se quisermos que cada loja tenha mais de uma galeria, não teríamos que remodelar todo nosso Model pois poderíamos ter ID’s diferentes para o mesmo Store_id
.
Picture
Estruturando o problema:
- Cada loja precisava possuir suas próprias fotos
- A mesma loja pode ter N fotos
Decisão:
ID | Gallery_id | Image_data
Com essa modelagem, cada foto teria seu próprio ID
e saberíamos qual o owner da foto através do gallery_id
GalleryRelatedProducts
Estruturando o problema:
- Cada loja poderia relacionar N fotos a mesma camiseta
- Cada foto poderia ser associada a N camisetas
Decisão
ID | Picture_id | Art_id
Com isso, conseguimos criar um model N-N, que permite que a mesma foto (picture_id
único) se relacione com tantas camisetas quanto necessário (Art_id
) e vice e versa.
- Criando os models no rails
Para criar os models executamos através do cmd,
$ rails generate model Gallery store:references
$ rails generate model Picture gallery:references image_data:text
$ rails generate model PictureRelatedProduct picture:references art:references
PS1: O comando references
cria automaticamente uma foreign_key com o model que veio chamado antes dos “:”
PS2: O campo image_data
não foi escolhido por acaso, a documentação do shrine pede que utilizemos o nome do tipo de arquivo seguido de “data” para este atributo.
Genericamente: <name>_data
- Utilizando o shrine para guardar as imagens
Neste passo você começará a sentir a potência do Shrine.
Descreveremos em 3 passos como fazer esta configuração
Primeiro passo: Criamos uma classe uploader, herdando métodos do Shrine
app > uploaders > store_picture_uploader.rb
Class StorePictureUploader < Shrine
end
Segundo passo: Na model que receberemos os dados do Upload (picture, no nosso caso), faremos uma chamada desta classe que criamos em (i)
app > models > picture.rb
class Picture < ApplicationRecord
include StorePictureUploader::Attachment(:image)
end
Com isso, garantimos que os dados que chegarem neste atributo (image_data
) venham através do Shrine.
*PS: Usamos :image
pois foi o nome do campo que criamos no model Picture (`image_data ), tirando a palavra “data”. *
Terceiro passo: Não tem passo 3, é só isso mesmo :)
b) VIEW
Arquivos utilizados:
app > views > user > dashboard > galleries > index.html.erb
app > views > user > dashboard > galleries > edit_image.html.erb
O usuário terá acesso a três interfaces:
- Upload de fotos
- Alterar e Deletar fotos / associar foto a um produto
- Galeria que exibirá suas fotos aos customers da loja
PS: A interface de exibição das fotos não entrará neste artigo pois utilizamos uma arquitetura DDD (Domain Driven Design). Posteriormente, em outro artigo falaremos sobre como utilizamos esta arquitetura.
Vamos explorar as duas primeiras interfaces
- Como criar o layout do form que guardará suas imagens
Upload de fotos
app > views > user > dashboard > galleries > index.html.erb
<%= form_for :picture, url: gallery_upload_path do |f| %>
<%= f.hidden_field :image %>
<%= f.file_field :image %>
<%= f.submit “Enviar foto” %>
<% end %>
Debugando:
:picture
→ Model que estaremos enviando as informações do form:image
→ Atributo que estaremos enviando a imagem. Lembrando que, mesmo o nome do campo sendo image_data, deixamos apenas image segunda a documentação do Shrinegallery_upload_path
→ Rota de post para onde enviaremos as informações de upload
post '/user/dashboard/gallery/', to: 'user/dashboard/galleries#upload', as: :gallery_upload
Adicionando um CSS e helpers de bootstrap…
Deletar fotos
app > views > user > dashboard > galleries > edit_image.html.erb
<%= link_to gallery_delete_picture_path(@image.id) do%>
Deletar Foto da Galeria
<% end %>
Debugando:
@image = Picture.find(params[:pic_id])
-
allery_delete_picture_path
→ Rota para deletar uma imagem no banco. Ela pede um parâmetro que é aID
da foto
get '/user/dashboard/gallery/delete/:id', to: 'user/dashboard/galleries#delete', as: :gallery_delete_picture
Alterar fotos
app > views > user > dashboard > galleries > edit_image.html.erb
<%= form_for :picture, url: gallery_update_picture_path(@image.id) do |f| %>
<%= f.hidden_field :image %>
<%= f.file_field :image, onchange: ‘this.form.submit();’ %>
<% end %>
Debugando:
:picture
→ model para onde enviaremos o formonchange: this.form.submit()
; → com isso, fazemos que o próprio botão de escolha da foto seja o mesmo de submit.gallery_update_picture_path
→ Rota para atualizarmos uma imagem no banco
post '/user/dashboard/gallery/update/:pic_id', to: 'user/dashboard/galleries#update', as: :gallery_update_picture
- Relacionamento de fotos-camisetas
Adicionar relação
<%= link_to gallery_insert_related_path(art.id, @image.id) %>
Adicionar
<% end %>
Debugando:
-
art
- Para exibimos todas as artes do usuário no carrosel, fizemos um
<% arts.each do |art| %>
<%= link_to gallery_insert_related_path(param1, param2) %>
Adicionar
<% end %>
<% end %>
*PS: param1 = art.id
, param2 = @image.id
*
*PS2: Por isso que acessamos a variável art
. Além disso, @arts = @store.arts
, sendo @store
a variável que instanciamos através do before_action
*
Debugando
-
gallery_insert_related_path
→ Rota para chamarmos a action de criação do relacionamento
get '/user/dashboard/gallery/insert_related/:art_id/:pic_id', to: 'user/dashboard/galleries#insert_related', as: :gallery_insert_related
Remover relação
<%= link_to gallery_remove_related_path(art.id, @image.id) %> Remove
<% end %>
Debugando:
-
gallery_remove_related_path
→ Rota deletarmos uma associação do modelGalleryRelatedPicture
get '/user/dashboard/remove_related/:art_id/:pic_id', to: 'user/dashboard/galleries#remove_related', as: :gallery_remove_related
C) CONTROLLER
app > controllers > user > dashboard > galleries_controller.rb
- Criando a galeria de cada loja
Para criarmos uma galeria para o usuário, utilizamos o método before_action
do rails. Com ele, assim que o usuário acessa a página do seu respectivo controller, antes de tudo, é executada uma ação.
Nossa lógica então foi fazer o seguinte: Assim que o usuário acessar a galeria através de seu dashboard, faremos:
galleries_controller.rb
class User::Dashboard::GalleriesController > ApplicationController
before_action :load_store_and_galley
private
def load_store_and_gallery
load_gallery
load_store
end
def load_gallery
@gallery = current_user&.store&.gallery || Gallery.new(store: current_user&.store)
end
def load_store
@store = current_user&.store
end
Debugando:
current_user
= método nativo do rails para sabermos qual o usuário atual que está na nossa aplicaçãooperador
&
= Com ele, a nossa aplicação não irá quebrar caso uma das chamadas sejanil
- CRUD de fotos
Insert
Como mostramos na view, estamos usando um form_for
, passando a rota gallery_upload_path
como argumento
- form_for
<%= form_for :picture, url: gallery_upload_path do |f| %>
- gallery_upload_path
post '/user/dashboard/gallery/', to: 'user/dashboard/galleries#upload', as: :gallery_upload
A action upload
, mencionada na rota acima ficou assim:
galleries_controller.rb
def upload
@picture = Picture.new(post_params)
@picture.gallery = @gallery
if !@picture.save
flash[:error] = @picture.errors.messages[:image]
end
end
private
def post_params
params.require(:picture).permit(:image)
end
Debugando:
@gallery
→ conseguimos acessar a variável de instância @gallery mesmo sem tê-la chamado neste action pois criamos ele usando o before_action. Logo, todas as actions do controller gallery terão acesso a esta variávelflash[:error]
= @picture.errors.message[:image] → em @picture, estamos instanciando um novo objeto do model Picture, logo ele retornará um ActiveRecord. Com isso, conseguimos acessar alguns métodos de objeto ActiveRecord, entre eles o objeto errors.message. Mas antes de falar sobre este método, mostraremos uma das validações que fizemos neste model.
app > models > picture.rb
validate :validate_image
def validate_image
if image.blank?
Errors.add(:image, ‘Por favor, adicione uma imagem’)
return
end
end
Com isso, fazemos com que, caso a imagem venha vazia (submit sem upload), adicionemos um “error” ao objeto Picture
que foi instanciado (@picture = Picture.new
no nossso caso) e não salvemos a imagem vazia no banco.
Por isso podemos combinar if !@picture.save
→ caso a imagem não seja salva no banco, @picture.errors.message[:image]
→ chamemos o log de erro que causou o não-salvamento do atributo :image
.
Update
Também mostrado na view, usamos um form_for passando a rota gallery_update_picture_path
.
- form_for
<%= form_for :picture, url: gallery_update_picture_path(@image.id) do |f| %>
- gallery_update_picture_path
post '/user/dashboard/gallery/update/:pic_id', to: 'user/dashboard/galleries#update', as: :gallery_update_picture
galleries_controller.rb
def update
@picture = Picture.find_by(id: params[:pic_id])
if @picture.update(post_params)
flash[:error] = @picture.errors.full_messages.first
end
end
Debugando:
@picture.update
→ Como@picture
é um objeto ActiveRecord, conseguimos utiizar o método udpate@picture.errors.full_messages.first
→ Como não estamos instanciando um objeto novo no model Picture, a validaçãovalidate_image
mostrada é executada de forma diferente. A saída que achamos para conseguimos carregar o log dos erros gerados foi usar este método chamadofull_messages.first
.
Delete
Esse foi o mais simples. Tivemos apenas que criar um link_to
para a rota de delete – gallery_delete_picture_path
e executar os devidos comandos no controller
- link_to
<%= link_to gallery_delete_picture_path(@image.id) do%>
- gallery_delete_picture
get '/user/dashboard/gallery/delete/:id', to: 'user/dashboard/galleries#delete', as: :gallery_delete_picture
galleries_controller.rb
def delete
@gallery_picture = Picture.find_by(id: params[:id])
@gallery_picture . destroy
redirect_to user_dashboard_gallery_path
end
Debugando:
params[:id]
→ ID da imagem que desejamos excluir.destroy
→ método para deletar um objeto ActiveRecordredirect_to
→ método para redirecionamos o usuário para uma rotauser_dashboard_gallery_path
→
get '/user/dashboard/gallery(/:page)', to: 'user/dashboard/galleries#index', as: :user_dashboard_gallery
- Relacionamento de fotos-camisetas
# Adicionar relação foto – arte
Foi bem simples fazer. Colocamos aquele botão “adicionar” que mostramos na view e, por trás do panos do controller fizemos
galleries_controller.rb
def insert_related
@picture = Picture.find_by(id: params[:id])
@related = GalleryRelatedPicture.new(art_id: params[:art_id], picture: @picture, gallery: @gallery)
@related.save
flash[:error] = @related.errors.full_messages.first if @related.errors
redirect_to request.referrer
end
Debugando:
-
request.referrer
→ Com este método conseguimos chamar a rota que o usuário estava antes da sua atual. Em outras palavras, é como dar um “click to go back” na seta para esquerda do seu navegador
Removendo relação foto-camiseta
A lógica foi bem parecida com adição: colocamos um link_to
para a rota de remover relação (gallery_remove_related_path
) passando os atributos art.id
e @image.id
nesta rota.
gallery_remove_related_path
get '/user/dashboard/remove_related/:art_id/:pic_id', to: 'user/dashboard/galleries#remove_related', as: :gallery_remove_related
galleries_controller.rb
def remove_related
@related = GalleryRelatedPicture.find_by(art_id: params[:art_id], picture_id: params[:pic_id], gallery_id: @gallery.id)
@related.destroy
redirect_back(fallback_location: root_path)
end
Debugando:
-
redirect_back(fallback_location: root_path)
→ método semalhante aorequest.referrer
. Caso tenha alguma diferença gritante, gostaríamos de ouvi-la leitor =)
Depois poucas semanas desta feature no ar já temos mais de 1000 fotos no model Picture
. A percepção dos usuário foi excelente e temos muito orgulho do impacto dela na loja de cada usuário. Alguns exemplos:
Ficou alguma dúvida, crítica ou sugestão? Fala com a gente, estamos em busca de criar uma rede de Devs que programem em Rails para compartilhamos cada vez mais nossos aprendizados =)
Abração!
Posted on September 25, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.