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:
Usuários fazem upload de suas fotos
Relacionam à suas camisetas
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
1-Como instalar o shrine do zero
2-Como configuramos em nossa aplicação
a)Model
I-Modelagem de dados
II-Criando os models no rails
III-Utilizando o shrine para guardar as imagens
b)View
I-Como criar o layout do form que guardará suas imagens II-Relacionar fotos e camisetas
c)Controller
I-Criando a galeria de cada loja
II-CRUD de fotos
III-Relacionamento de fotos e camisetas
1-Como instalar
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 configuraramos em nossa aplicação
a)Model
i-Modelagem de dados
Para início de conversa, falemos sobre como a modelagem de dados foi feita. No ínicio, sabíamos que teriam dois fluxos de dados através das entidades:
- Galeria
- Fotos
- Relacionamento fotos-produto
Por isso, quando começamos o projeto pensamos em criar três Models: Picture e GalleryRelatedProducts e Gallery, da seguinte forma:
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 se relacionar N fotos ao mesmo produto
- Cada Foto, poderia ser associada a N fotos
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 artes quanto necessário (Art_id) e vice e versa.
ii-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: _data
iii-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 chegarão neste atributo (image_data) chegarão 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 os clientes de uma 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.
Sendo assim, vamos explorar as duas primeiras interfaces
I-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 Shrine -
gallery_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%>
<button> Deletar Foto da Galeria </button>
<% end %>
Debugando:
@image = Picture.find(params[:pic_id])
-
gallery_delete_picture_path
-> Rota para deletar uma imagem no banco. Ela pede um parâmetro que é a ID da foto
get '/user/dashboard/gallery/delete/:id', to: 'user/dashboard/galleries#delete', as: :gallery_delete_picture
Debugando:
-
:picture
-> model para onde enviaremos o form -
onchange: ‘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
II-Relacionamento de fotos e artes
Adicionar relação
<%= link_to gallery_insert_related_path(art.id, @image.id) %>
<button> Adicionar </button>
<% 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(art.id, @image.id) %>
<button> Adicionar </button>
<% end %>
<% end %>
PS: 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
-
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) %>
<button> Remove </button>
<% 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
I-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:
app/controllers/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ção -
&
-> Com ele, a nossa aplicação não irá quebrar caso uma das chamadas seja “nil”
II-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, ficou assim:
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ável -
flash[:error] = @picture.errors.message[:image]
-> em@picture
, estamos instanciando um novo objeto do model Picture, logo ele retornará umActiveRecord
. 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
app > controllers > 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ção validate_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
get '/user/dashboard/gallery/delete/:id', to: 'user/dashboard/galleries#delete', as: :gallery_delete_picture
-
gallery_delete_picture
get '/user/dashboard/gallery/delete/:id', to: 'user/dashboard/galleries#delete', as: :gallery_delete_picture
app > controllers > 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 ActiveRecord -
redirect_to
-> método para redirecionamos o usuário para uma rota -
user_dashboard_gallery_path
->
get '/user/dashboard/gallery(/:page)', to: 'user/dashboard/galleries#index', as: :user_dashboard_gallery
III-Relacionamento de fotos e artes
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
app > controllers > 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 – arte
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
app > controllers > 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_locatin: root_path)
end
Debugando:
-
redirect_back(fallback_location: root_path)
-> método semalhante ao request.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.
https://www.reserva.ink/owm/gallery
https://www.reserva.ink/blogdocavaco/gallery
https://www.reserva.ink/inspiringgirls/gallery
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!
Top comments (0)