Galeria de Fotos com o Shrine
Tags: rails, shrine, ruby
Published: 2020-09-25 14:18:38

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

######*2-Relacionam a suas camisetas*

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

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/1ifw89yhqye3589e06lw.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

---

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

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

```
$ 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 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/ddlwsqyarafn6bc3t3nz.png)

---

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


---

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


* 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
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
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 
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 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](https://www.reserva.ink/owm/gallery)

[Blog do Cavaco](https://www.reserva.ink/blogdocavaco/gallery)

[Inspiring Girls](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!