DEV Community

José Sobral
José Sobral

Posted on

Galeria de Fotos com o Shrine

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

Alt Text

2-Relacionam a suas camisetas

Alt Text

3-Exibimos uma galeria de fotos e as respectivas imagens associadas a cada foto

Alt Text

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!

  1. Como instalar o shrine do zero

  2. 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"
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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:

  1. Galeria
  2. Fotos
  3. 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
Enter fullscreen mode Exit fullscreen mode
$ rails generate model Picture gallery:references image_data:text
Enter fullscreen mode Exit fullscreen mode
$ rails generate model PictureRelatedProduct picture:references art:references
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 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…

Alt Text


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 é a ID 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 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

Alt Text


  • 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 model GalleryRelatedPicture


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ção

  • operador & = Com ele, a nossa aplicação não irá quebrar caso uma das chamadas seja nil


  • 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ável

  • flash[: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
Enter fullscreen mode Exit fullscreen mode

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 chamado full_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
Enter fullscreen mode Exit fullscreen mode

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


  • 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 
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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. Alguns exemplos:

Owm

Blog do Cavaco

Inspiring Girls


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)