DEV Community

José Sobral for Reserva INK

Posted on

Shrine Photo Uploader – Galeria de Fotos

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

Alt Text

Relacionam à suas camisetas

Alt Text

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

Alt Text

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

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

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

  1. Galeria
  2. Fotos
  3. 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
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 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 %>
Enter fullscreen mode Exit fullscreen mode

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

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%>
    <button> Deletar Foto da Galeria </button>
<% end %>
Enter fullscreen mode Exit fullscreen mode

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

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

Alt Text

II-Relacionamento de fotos e artes

Adicionar relação

<%= link_to gallery_insert_related_path(art.id, @image.id) %>
    <button> Adicionar </button>
<% end %>
Enter fullscreen mode Exit fullscreen mode

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

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

Remover relação

<%= link_to gallery_remove_related_path(art.id, @image.id) %>
    <button> Remove </button>
<% end %>
Enter fullscreen mode Exit fullscreen mode

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

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

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| %>
Enter fullscreen mode Exit fullscreen mode
  • gallery_upload_path
post '/user/dashboard/gallery/', to: 'user/dashboard/galleries#upload', as: :gallery_upload
Enter fullscreen mode Exit fullscreen mode

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

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

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| %>
Enter fullscreen mode Exit fullscreen mode
  • gallery_update_picture_path
post '/user/dashboard/gallery/update/:pic_id', to: 'user/dashboard/galleries#update', as: :gallery_update_picture
Enter fullscreen mode Exit fullscreen mode

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

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
get '/user/dashboard/gallery/delete/:id', to: 'user/dashboard/galleries#delete', as: :gallery_delete_picture
Enter fullscreen mode Exit fullscreen mode
  • gallery_delete_picture
get '/user/dashboard/gallery/delete/:id', to: 'user/dashboard/galleries#delete', as: :gallery_delete_picture
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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!

Oldest comments (0)