Shrine Photo Uploader – Galeria de Fotos
Tags: rails, shrine, ruby, architecture
Published: 2021-01-18 22:36:31

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](https://dev-to-uploads.s3.amazonaws.com/i/pkff9u5avun3hpvca5vz.png)

*Relacionam à suas camisetas*

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/imyv9kxkmrxjri33u4kg.png)

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/4kjrv96q661r3fimlqor.png)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/h16f075hzaz461obd90o.png)


**Toolkit:** [Shrine](https://github.com/shrinerb/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:

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: <name>_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…

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ogjtvejkavkbypzw1m90.png)

---

**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
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/w0fj23t8lkdchx4kbtr8.png)
---

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

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](https://stackoverflow.com/questions/12719958/rails-where-does-the-infamous-current-user-come-from) 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á 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
```

*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 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
```

* `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!