# README

## Platanus - La Guía

Esta es La Guía de Platanus. Acá encontrarás una suerte de mandamientos, lineamientos, acuerdos y todo lo necesario para que podamos comunicarnos de la mejor forma posible. Es importante que todos compartamos una misma cultura.

## Colaborar

La guía se genera automáticamente a partir de documentos internos que mantenemos en Platanus. Por esto, no estamos esperando demasiada colaboración externa. De todos modos, estamos abiertos a recibir propuestas y sugerencias que puedes hacernos llegar a través de issues o pull requests que, por lo que expliqué anteriormente, no serán mezclados pero sí tomados en consideración por miembros del equipo.

## Secciones

### Acuerdos

* [Guía de Estilo](/acuerdos/guia_de_estilo)
  * [Ejemplo: Módulo para variables de entorno](/acuerdos/guia_de_estilo/ejemplo_modulo_para_variables_de_entorno)

### Stack

* [Getting Started](/stack/getting_started)
* [Nuestro MVC extendido](/stack/nuestro_mvc_extendido)
* [Ruby/Rails](/stack/ruby_rails)
  * [Power Types](/stack/ruby_rails/power_types)
    * [General](/stack/ruby_rails/power_types/general)
    * [Patrones](/stack/ruby_rails/power_types/patrones)
      * [Commands](/stack/ruby_rails/power_types/patrones/commands)
      * [Utils](/stack/ruby_rails/power_types/patrones/utils)
      * [Services](/stack/ruby_rails/power_types/patrones/services)
      * [Values](/stack/ruby_rails/power_types/patrones/values)
      * [Observers](/stack/ruby_rails/power_types/patrones/observers)
  * [Potassium](/stack/ruby_rails/potassium)
  * [Power API](/stack/ruby_rails/power_api)
  * [Active Admin](/stack/ruby_rails/active_admin)
    * [General](/stack/ruby_rails/active_admin/general)
    * [Active Admin Addons](/stack/ruby_rails/active_admin/active_admin_addons)
  * [Pundit](/stack/ruby_rails/pundit)
  * [Shrine](/stack/ruby_rails/shrine)
    * [General](/stack/ruby_rails/shrine/general)
    * [Manejo y procesamiento de imágenes](/stack/ruby_rails/shrine/manejo_y_procesamiento_de_imagenes)
  * [Pry](/stack/ruby_rails/pry)
  * [Strong Migrations](/stack/ruby_rails/strong_migrations)
  * [Data Migrate](/stack/ruby_rails/data_migrate)
  * [Active Job](/stack/ruby_rails/active_job)
  * [Gems](/stack/ruby_rails/gems)
  * [Engines - Modularización en Rails](/stack/ruby_rails/engines_modularizacion_en_rails)
* [JavaScript](/stack/javascript)
  * [Vue](/stack/javascript/vue)
    * [General](/stack/javascript/vue/general)
    * [Testing](/stack/javascript/vue/testing)
  * [AlpineJS](/stack/javascript/alpinejs)
* [CSS](/stack/css)
* [Mobile](/stack/mobile)
  * [Expo](/stack/mobile/expo)
  * [React Navigation](/stack/mobile/react_navigation)
  * [Redux](/stack/mobile/redux)
    * [Crear y conectar una slice en Redux](/stack/mobile/redux/crear_y_conectar_una_slice_en_redux)
  * [Styling](/stack/mobile/styling)
    * [Usando Tailwind en React Native](/stack/mobile/styling/usando_tailwind_en_react_native)
  * [Recursos](/stack/mobile/recursos)
* [Resolviendo problemas (debugging)](/stack/resolviendo_problemas_debugging)
* [Machine Learning](/stack/machine_learning)

### Setup

* [Configuración de tu entorno local](/setup/configuracion_de_tu_entorno_local)
  * [Instalación Base](/setup/configuracion_de_tu_entorno_local/instalacion_base)
    * [OSX](/setup/configuracion_de_tu_entorno_local/instalacion_base/osx)
    * [Windows](/setup/configuracion_de_tu_entorno_local/instalacion_base/windows)
    * [Linux](/setup/configuracion_de_tu_entorno_local/instalacion_base/linux)
  * [Tecnologías](/setup/configuracion_de_tu_entorno_local/tecnologias)
    * [Ruby](/setup/configuracion_de_tu_entorno_local/tecnologias/ruby)
    * [Docker](/setup/configuracion_de_tu_entorno_local/tecnologias/docker)
    * [Node](/setup/configuracion_de_tu_entorno_local/tecnologias/node)
  * [Herramientas](/setup/configuracion_de_tu_entorno_local/herramientas)
    * [Linters](/setup/configuracion_de_tu_entorno_local/herramientas/linters)
    * [Editores](/setup/configuracion_de_tu_entorno_local/herramientas/editores)
      * [IDE/Editores de Código](/setup/configuracion_de_tu_entorno_local/herramientas/editores/ide_editores_de_codigo)
        * [Visual Studio Code](/setup/configuracion_de_tu_entorno_local/herramientas/editores/ide_editores_de_codigo/visual_studio_code)
        * [Sublime Text](/setup/configuracion_de_tu_entorno_local/herramientas/editores/ide_editores_de_codigo/sublime_text)
    * [Git](/setup/configuracion_de_tu_entorno_local/herramientas/git)
* [Configuración de proyectos](/setup/configuracion_de_proyectos)
  * [Getting Started](/setup/configuracion_de_proyectos/getting_started)
  * [Heroku](/setup/configuracion_de_proyectos/heroku)
  * [Rails](/setup/configuracion_de_proyectos/rails)
  * [Circle CI](/setup/configuracion_de_proyectos/circle_ci)
  * [Vue](/setup/configuracion_de_proyectos/vue)
  * [Apple App Store](/setup/configuracion_de_proyectos/apple_app_store)
  * [Google Play](/setup/configuracion_de_proyectos/google_play)
  * [Expo](/setup/configuracion_de_proyectos/expo)
  * [S3](/setup/configuracion_de_proyectos/s3)
  * [Git](/setup/configuracion_de_proyectos/git)
  * [Cloudflare](/setup/configuracion_de_proyectos/cloudflare)
  * [Sendgrid](/setup/configuracion_de_proyectos/sendgrid)
  * [Dominio + Mailing](/setup/configuracion_de_proyectos/dominio_mailing)
  * [Google Tag Manager, Analytics, Search Console, etc.](/setup/configuracion_de_proyectos/google_tag_manager_analytics_search_console_etc)
    * [Google Tag Manager](/setup/configuracion_de_proyectos/google_tag_manager_analytics_search_console_etc/google_tag_manager)
      * [Configurar Google Tag Manager](/setup/configuracion_de_proyectos/google_tag_manager_analytics_search_console_etc/google_tag_manager/configurar_google_tag_manager)
    * [Google Analytics](/setup/configuracion_de_proyectos/google_tag_manager_analytics_search_console_etc/google_analytics)
    * [Indexación en Google](/setup/configuracion_de_proyectos/google_tag_manager_analytics_search_console_etc/indexacion_en_google)
    * [Google Ads](/setup/configuracion_de_proyectos/google_tag_manager_analytics_search_console_etc/google_ads)
  * [Crear un bucket de S3](/setup/configuracion_de_proyectos/crear_un_bucket_de_s3)
  * [SlackBot](/setup/configuracion_de_proyectos/slackbot)
  * [Google BigQuery](/setup/configuracion_de_proyectos/google_bigquery)

### Deployment

* [Rails](/deployment/rails)
* [Ruby Gems](/deployment/ruby_gems)
* [Browser and Node (Open Source)](/deployment/browser_and_node_open_source)
* [Mobile](/deployment/mobile)
  * [Mobile Resources](/deployment/mobile/mobile_resources)
  * [Apple App Storage](/deployment/mobile/apple_app_storage)
  * [Google Play](/deployment/mobile/google_play)

### Upgrades

* [Upgrade de Vue 2 a Vue 3](/upgrades/upgrade_de_vue_2_a_vue_3)
* [Migración Hound → reviewdog](/upgrades/migracion_hound_reviewdog)
* [Upgrade de Postgresql](/upgrades/upgrade_de_postgresql)

## License

La Guia is © 2024 platanus, spa. It is free software and may be redistributed under the terms specified in the LICENSE file.

![Platanus](http://platan.us/gravatar_with_text.png)

La Guia is maintained by [platanus](http://platan.us).


# Guía de Estilo

## General

* No abreviar nombres de variables, clases, métodos, etc. Priorizar que algo sea fácil de leer y que se pueda entender rápidamente lo que hace o qué representa, sin importar si queda un nombre más largo de lo que a un@ le gustaría.

## Rails

* Las variables de ambiente deberían ser definidas como métodos en un módulo para agruparlas y para que sea más fácil hacer mock de ellas. ([Ejemplo: Módulo para variables de entorno](/acuerdos/guia_de_estilo/ejemplo_modulo_para_variables_de_entorno))

  ```ruby
  # ❌
  DEFAULT_TIME = ENV.fetch("DEFAULT_TIME", 60 * 24 * 3).to_i
  DEFAULT_TIME * 4

  # ✅
  Item.default_time * 4
  ```
* Encapsular lógica de negocios en Jobs, sin importar si no es reutilizable, para limitar el tamaño de los controllers.
* En API usar siempre memoization. En app usar `set_model` explícito en el método de la acción. Nunca usar `before_action`.

  ```ruby
  # ❌
  before_action :set_variables

  def set_variables
    @variables = Variable.all
  end

  # ✅
  def variables
    @variables ||= Variable.all
  end
  ```
* Preferir Shrine por sobre ActiveStorage para manejo de imágenes.
* Serializer: Solo métodos para estructurar data que se ve a necesitar en una API
* Decorador: Cosas que tienen html o especifico para consumo de vista por parte de Rails
* Presenter: Lógica/condicionales para usar en vistas. En general se debería preferir Vue por sobre presenters.

## API

* Usamos PowerAPI para crear APIs
* Cada controller debería manejar un solo recurso.

  ```ruby
  # ❌

  class BookController
    def index
      respond_with Book.all
    end

    def chapters
      respond_with Chapter.all
    end
  end

  # ✅
  class BookController
    def index
  	  respond_with Book.all
    end
  end

  class ChapterController
    def index
      respond_with Chapter.all
    end
  end
  ```
* Todo lo que es API para uso interno (formularios, scroll infinito, etc) se usa PowerAPI en modo internal
* Solo cuando la aplicación va a ser consumida de manera independiente a la web (app mobile) se usa las API exposed, teniendo tanto API interna como exposed si es necesario.
* Se usa `serialize_resource` para pasar variables de Rails a Vue `:prop="<%= serialize_resource(@resource, @options) %>"`
* Para generar endpoints nuevos preferir usar el generador de PowerAPI (`bin/rails generate power_api:controller my_resource`
* Preferir usar BaseController a la hora de agregar cosas como Pundit en vez de editar cada controller.
* Serializers - No sacar el root. El formato de respuesta debería ser consistente en todos los serializers
* Usar objetos decorados en serializer
* Preferir `respond_with` a `render json:`
* Evitar agregar valores traducidos o formateados en la respuesta de una API. Idealmente los valores siempre se mandan sin formatear (fechas, números, moneda).
* Valores para vistas deberían ir en serializer y/o decorador, métodos de datos deberían ir modelo.
* Por definir: estructura REST de rutas - uso de verbos en url (<https://platanus.slack.com/archives/C021F62E15G/p1651518650410749>)

## Modelos

* Para manejar callbacks después de un evento en un modelo usamos dos maneras:
  * Callbacks en el modelo cuando se editan atributos de la misma tabla
  * PowerTypes - Observers para ejecutar side-effects (mandar correos, crear otro recurso en otra tabla, etc)
* Evitar side-effects en AASM (mandar emails, modificar otros modelos), usar Observers para eso.
* Si se están usando estados de AASM, siempre usar sus eventos para cambiar de un estado a otro. No cambiar de estado con `Resource.update`
* usar keys de locale en errores activemodel
* En observers todo lo que depende de un recurso externo se ejecuta en perform\_later

## ActiveAdmin

* No tener lógica directo en el DSL de active admin. Usar jobs o servicios.
* Si se usan componentes de Vue, se pueden llamar a los endpoint de AA directamente con `.json` usando query params de Ransack si es necesario
* A veces se necesita hacer una vista custom para ActiveAdmin, ya sea por un `member_action` o algo como un form custom. Si la vista requiere mucho html, se puede usar un `.erb`, pero si está más cargado al ruby, preferir usar un `.arb` , qué nos permite usar la misma sintáxis que se usan en ActiveAdmin. Considerar también que es posible utilizar componentes Vue en estos templates de ActiveAdmin.

## Modelos: ActiveRecord

* Preferir dos queries simples versus una query muy complicada.
* pluck en vez de map para obtener atributos de un modelo
* Preferir siempre métodos de ActiveRecord para las relaciones por sobre consultas con where o raw SQL. En otras palabras, no usar SQL “a mano”
* En colecciones de ActiveRecord, preferir métodos de ActiveRecord sobre métodos de Ruby/Enumerable, para delegar el cálculo o búsqueda a la base de datos y no traer toda la colección a memoria innecesariamente. Una excepción puede ser cuando los elementos ya se cargaron previamente en memoria.

  ```ruby
  # ❌
  collection.find { |record| record.some_column == 'something' }
  collection.select { |record| record.some_column == 'something' }
  collection.select { |record| record.association.some_column == 'something' }
  collection.pluck(:some_column).sum

  # ✅
  collection.find_by(some_column: 'something')
  collection.where(some_column: 'something')
  collection.joins(:association).where(
    associations: { some_column: 'something' }
  )
  collection.sum(:some_column)
  ```
* Usar transactions para grupos de acciones.
* Los scopes siempre deberían ser chainables.
* Preferir definir scopes en vez de definir queries en controllers.

## Views

* Mientras menos nesteado el html, mejor. No agregar divs wrappers para agregar una sola clase a menos que sea 100% necesario.

  ```javascript
  <!-- ❌ -->
  <div class="mt-2">
    <div class="bg-white">Hola</div>
  <div>

  <!-- ✅ -->
  <div class="mt-2 bg-white">Hola</div>
  ```

## Vue

* Seguir guía de estilos de Vue a la hora de ponerle nombre a los componentes.
  * Tener todos los componentes en la misma carpeta. Fuente ([detailed explanation en guía de estilo](https://vuejs.org/style-guide/rules-strongly-recommended.html#tightly-coupled-component-names))
    * Si es que se hace inmanejable la cantidad de componentes, lo más probable es que la aplicación en si sea lo suficientemente grande como para dividir en engines, en cuyo caso los componentes deberían ir en sus carpetas de engine respectivas.
  * Si un componente es global y básico (inputs, botones) usar prefijo `base`. Ejemplos: `base-input`, `base-modal`. [Fuente](https://vuejs.org/style-guide/rules-strongly-recommended.html#tightly-coupled-component-names)
  * Si es un componente que se usa una sola vez (headers, footers, etc) usar prefijo `The`. Ejemplos: `the-header`, `the-contact-form` [Fuente](https://vuejs.org/style-guide/rules-strongly-recommended.html#tightly-coupled-component-names)
  * Componentes que solo se van a usar en otro componente, tienen de prefijo el nombre de ese componente. Ejemplo: `the-header-nav-bar`. [Fuente](https://vuejs.org/style-guide/rules-strongly-recommended.html#tightly-coupled-component-names)
  * Usar `kebab-case` en vez de `PascalCase` ya que usamos el DOM template en las vistas de Rails. [Fuente](https://vuejs.org/style-guide/rules-strongly-recommended.html#component-name-casing-in-templates)
* Usar `href` en vez de `@click` cuando la única acción que se quiere realizar es cambiar de página.
* Usar variables multilinea en vez desactivar regla de eslint
* Preferir librerías que no muten sus valores, por ejemplo date-fns en vez de moment.
* Preferir sintaxis que ayude al tree-shaking, para bajar el tamaño del bundle.

  ```javascript
  // ❌
  import * as dateFns from 'date-fns'
  // o, en el caso especifico de lodash
  import { get } from 'lodash'

  // ✅
  import get from 'lodash/get'
  // o 
  import { get }  from 'lodash-es'

  import { format, parse } from 'date-fns'
  ```
* Las promesas deberían estar con catch y mostrar feedback al usuario.

  ```javascript
  // ❌

  const items = await itemsApi.getAll();
  doSomething(items)

  // ✅
  try {
  	const items = await itemsApi.getAll();
    doSomething(items);
  } catch (e) {
    showTryAgainMessage.value = true;
  }
  ```
* Estructuras repetidas deberían ser extraidas a componentes o ser usadas con v-for (sobre todo si se están usando métodos en vez de valores computed)
* Preferir computed por sobre métodos (cuando son valores para el template)
* Si hay CSS custom en `<style>` usar `scoped` para limitar su efecto al resto de la aplicación.
* Usar [form generators](https://vee-validate.logaretm.com/v4/tutorials/dynamic-form-generator/) puede ahorrar mucho tiempo si el proyecto tiene varios formularios con el mismo estilo.
* Inline SVGs:
  * Todos los iconos *o s*i se necesita cambiar el color de un SVG que no es un icono, usar <https://www.npmjs.com/package/vue-inline-svg>. La librería es necesaria para que los SVGs queden insertados directamente en el template en vez de quedar como imágenes enlazadas, y así puedan ser modificados con css.
  * Íconos SVG no deberían tener atributo `fill` o deberían tener `fill="currentColor"` para que tomen el color del texto.
  * <https://github.com/jamesmartin/inline\\_svg> (con `inline_svg_tag`) cuando el svg se vaya a usar en un template de rails directamente.
  * En otro casos usar img con el SVG directamente en el src.
* Evitar anidar elementos interactivos (`<button>` dentro de )

  Normalmente esto pasa cuando se quiere que todo un elemento sea clickable pero que además tenga acciones extras. La solución es separar las acciones secundarias de la acción principal:![](/files/GLv9hNmDwtlnzosY382I)
* `<button>` siempre debería tener un `type`\<!-- ❌ -->\<form>  \<button @click="cancel">Cancelar\</button>  \<button @click="submit">Enviar\</button>\</form>\<!-- ✅ -->\<form @submit="submit">  \<button @click="cancel" type="button">Cancelar\</button>  \<button type="submit">Enviar\</button>\</form>Base mínima a la que queremos llegar de accesibilidadPor lo mínimo los formularios deberían ser navegables con el teclado (inputs y botones en vez de divs, todo dentro de un form, evento submit debería ser manejado para que funcione el enter)Cada input debería tener un label, ya sea dentro del tag o con un id/for. Si por diseño no se puede ver un label, usar la clase `sr-only` para esconderlo.Evitar `@click` en cosas que no sean links o botones. No usar divs.No usar outline-none en los elementos a menos que el focus se marque de otra manera (`focus-visible`)Todos los formularios deben ser implementados en Vue con submit mediante API.Preferir Vue por sobre Rails (y presenters) cuando hayan condicionales o cosas dinámicas. Considerar casos de uso futuros a la hora de decidir.En lo posible la primera carga de una página nunca debería requerir esperar más requests para mostrar contenido. Usar el mismo serializer para el prop y el endpoint de la API (`serialize_resource`)No usar tags self-closing en `.erb`. Se usan en Vue por ser más simples y rápidas de usar, pero en HTML normal no son válidas y tienden a romper el template de manera misteriosa.\<!-- ❌ -->\<super-component />\<!-- ✅ -->\<super-component>\</super-component>Si en un test se necesita seleccionar un elemento, no agregar una clase, ref o cualquier otro atributo que ya tenga otro significado. Para esto se le puede agregar al elemento un `data-testid="something"` si es un elemento único, o `data-test` si no lo es\*\*Por definir: \*\*Qué hacer con variables globales que vienen desde Rails (ej, current user) y se necesitan en VueEn Vue 3, poner primero el `<script>`, luego el `<template>` y al final `<style>` si es que hay

State Management (Pinia, Vuex)

* **Usamos** [**Pinia**](https://pinia.vuejs.org/) **en vez de Vuex.**
* El store debería estar normalizado (array de ids + objeto con ids identificando cada item)

  ```javascript
  <!-- ❌ -->
  const state = {
    items: [
      {id: 1, name: 'Name 1'},
      {id: 2, name: 'Name 2'}
    ]
  }

  const itemWithId = state.items.find(item => item.id === ITEM_ID);

  <!-- ✅ -->
  const state = {
    items: {
      1: {id: 1, name: 'Name 1'},
      2: {id: 2, name: 'Name 2'}
    }
  }

  const itemWithId = state.items[ITEM_ID];
  ```
* Evitar usar getters que acepten parámetros. Si es necesario que un getter sea dinámico, los parámetros deberían ser atributos del mismo store.
* Solo en Vuex: Mutaciones deberían ir en su propio archivo como constantes.

## Vue - Librerías

* [Pinia ](https://pinia.vuejs.org/)para state management
* [Axios ](https://github.com/axios/axios#installing)para hacer requests a APIs + [vue-query](https://vue-query.vercel.app/#/getting-started/installation) para manejar los estados de loading/success/error
* [date-fns](https://date-fns.org/docs/Getting-Started#installation) para manejar fechas
* [VueUse](https://vueuse.org/) para utilidades varias usando la Composition API.

## Typescript

Las interfaces que definen cosas que vienen del backend deberían ir en la carpeta `api/`, aun si definen cosas que no tienen un endpoint especifico o no tienen endpoint todavía. Por ejemplo si están haciendo un `Form` pero este `Form` tiene `FormCategory` y `FormField`, las tres irían en un mismo archivo `api/form.ts` y se exportarían hacia los componentes desde ahí.

```typescript
// api/form.ts
export interface FormCategory {
  id: number;
  name: string;
}

export interface FormField {
  id: number;
  label: string;
  inputType: string;
}

export interface Form {
  id: number;
  title: string;
  formCategory: FormCategory;
  formFields: FormField[];
}

export default {
  createForm(...
}
```

Las interfaces que definen cosas internas que solo se usan en en un mismo componente deberían ir dentro de `<script setup>`. Si es algo que usan varios componentes, definir en la carpeta `/types`, de preferencia como exports. En *algunos* casos puede ser preferible usar interfaces globales pero en general evitar para no causar confusiones del tipo “de donde salió este type”.

## Tailwind

* Extraer clases repetidas a componentes o iterar templates (con `.each` o `v-for`)
* No cambiar tamaño base de fuente en body
* Considerar estados (active, focus, hover) a la hora de agregar estilo a elementos interactivos
* Aparte de agregar los colores y fuente de la marca, tratar de no modificar ni extender mucho el `theme` de tailwind. Ver si se puede obtener un resultado suficientemente parecido usando las clases ya existentes. Si se necesita un valor arbitrario que no está en estas clases, y este valor se usa en solo una parte, preferir agregarlo [como valor arbitrario directo en el html](https://tailwindcss.com/docs/adding-custom-styles#using-arbitrary-values)

  ```html
  <!-- ❌ -->
  <div id="hero-only-used-once" class="h-hero w-hero">

  <!-- ✅ -->
  <div id="hero-only-used-once" class="h-[400px] w-[100%]">
  ```

## Quiero implementar trackeo de cambios en valores de modelo

Usar paper\_trail

## Quiero implementar 2FA

Usar devise-two-factor

PR de ejemplo: <https://github.com/platanus/ventures-nest/pull/322/files>

## Quiero implementar Tags

Usar act-as-taggable-on

[Ejemplo: Módulo para variables de entorno](/acuerdos/guia_de_estilo/ejemplo_modulo_para_variables_de_entorno)


# Ejemplo: Módulo para variables de entorno

```ruby
# lib/my_module.rb
module MyModule
  MY_NUMBER = ENV.fetch("MY_NUMBER", 10)
  MY_BOOLEAN = ENV["MY_BOOLEAN"]

  def self.my_number
    MY_NUMBER.to_i
  end

  def self.my_boolean?
    MY_BOOLEAN == "true"
  end
end

# se acceden así: 
MyModule.my_number
	MyModule.my_boolean?
```

Definimos un módulo en donde los fetch de las variables de entorno están fuera de los métodos (de esta manera si una variable de entorno no está definida fallará el build). Dentro de los métodos sólo se encuentra el formateo de las variables

Para que el módulo esté disponible en la aplicación, se debe agregar el require en el archivo `application.rb`

```ruby
# application.rb
config.before_configuration do
   require Rails.root.join("lib/my_module.rb")
end
```


# Getting Started

El 90 % de los proyectos Platanus, son aplicaciones web alojadas en Github que comienzan con un proyecto Ruby on Rails. Este muy probablemente no será una SPA y usará Vue.js + Tailwind CSS para enriquecer la experiencia de usuario. En algún momento del desarrollo conectaremos algunas partes del frontend con el back utilizando una API REST y probaremos nuestro código con RSpec. Además, romperemos un poco el MVC que propone Rails con patrones como observers, comandos y servicios para estructurar y manejar mejor el código. Cuando los procesos se vuelvan pesados usaremos jobs para ejecutar en background y, a la hora del deploy, nos serviremos de [Circle CI](https://circleci.com/) para ayudarnos en el proceso de integración continua. Al final, nuestra aplicación se servirá en internet con la ayuda de [heroku](https://www.heroku.com/).

Si estás pensando en postular para trabajar en Platanus o simplemente sientes curiosidad de nuestro stack, te recomendamos mirar los recursos listados debajo. El resto de la guía contiene información específica de cómo hacemos las cosas aquí. Por esto, si no estás familiarizado con nuestras herramientas, este es un buen lugar para partir:

## Recursos

### Github

* [Git and GitHub for Beginners - Crash Course](https://www.youtube.com/watch?v=RGOj5yH7evk)
* [Git Tutorial for Beginners: Command-Line Fundamentals](https://www.youtube.com/watch?v=HVsySz-h9r4)

### Ruby/Rails

* [Ruby Programming Language](https://www.youtube.com/watch?v=t_ispmWmdjY): introducción básica a Ruby.
* [Rails 5: The Tour](https://youtu.be/OaDhY_y8WTo): introducción básica a Rails. Está hecho a partir de Rails 5 (aunque ya vamos por la versión 6) pero en esencia es lo mismo y es material oficial.
* [Ruby on Rails Guides](https://guides.rubyonrails.org/active_job_basics.html): guía oficial de Ruby on Rails. Toda la guía es excelente pero quizás es un poco grande. De aquí al menos revisaría: [Getting Started with Rails](https://guides.rubyonrails.org/getting_started.html), [Active Record Basics](https://guides.rubyonrails.org/active_record_basics.html), [Active Record Migrations](https://guides.rubyonrails.org/active_record_migrations.html), [Action Controller Overview](https://guides.rubyonrails.org/action_controller_overview.html), [Layouts and Rendering in Rails](https://guides.rubyonrails.org/layouts_and_rendering.html) y [Action Mailer Basics](https://guides.rubyonrails.org/action_mailer_basics.html).

### Vue

* [Official Documentation](https://vuejs.org/guide/): documentación oficial de Vue.
* [Learn Vue.js - Full Course for Beginners](https://www.youtube.com/watch?v=4deVCNJq3qc)

### API Rest

* [What is REST API?](https://www.youtube.com/watch?v=rtWH70_MMHM)
* [REST API concepts and examples](https://www.youtube.com/watch?v=7YcW25PHnAA)

### RSpec

* [Testing with RSpec](https://www.youtube.com/watch?v=71eKcNxwxVY)
* [RSpec Documentation](https://relishapp.com/rspec/rspec-core/docs)
* [Better Specs](http://www.betterspecs.org/): buenas y malas prácticas en RSpec.
* [Mocking con RSpec](https://blog.platan.us/mocking-con-rspec-4c2b2689cf93): quizás esto es un poco avanzado pero es bueno tener conocimiento sobre mocks, stubs, etc. ya que, probablemente, este tema es el más difícil de incorporar sobre testing.

### Tailwind

* [Getting started](https://tailwindcss.com/docs/installation/): documentación oficial.
* [Tailwind CSS Crash Course](https://www.youtube.com/watch?v=UBOj6rqRUME)

### Patrones

* [Services, Commands y otros poderosos patrones en Rails](https://blog.platan.us/services-commands-y-otros-poderosos-patrones-en-rails-27c2d3aa7c2e): blog post de Platanus donde explicamos para qué se usan los comandos, servicios y alguna cosa más.

### Jobs

* [Active Job Basics](https://guides.rubyonrails.org/active_job_basics.html): guía oficial de Rails sobre ActiveJob.
* [Drifting Ruby - Background Jobs with Sidekiq](https://www.youtube.com/watch?v=CStZg8ql9Vs): video que explica de manera muy concisa el funcionamiendo de sidekiq con Rails. Algunas cosas de configuración han cambiado pero igual es un buen video para entender como trabaja.


# Nuestro MVC extendido

Rails es la base de nuestro stack, y como quizás ya sabes tiene un modelo MVC:

* `Models` que definen la estructura de la data y se comunican con la base de datos
* `Views` que definen la UI - lo que ve el usuario
* `Controllers` que sacan data de los modelos y ponen a disposición de la vista correspondiente la información que necesiten

En Platanus hemos adaptado un poco este modelo MVC, agregando distintas capas y herramientas que nos ayudan a tener responsabilidades más separadas, cosa de mantener nuestros modelos y controllers lo más \*skinny \*posible.

Puedes encontrar explicaciones más detalladas de estas tecnologías en Stack , la idea de esta sección es explicar un poco como interactúan estas distintas partes:

* Power API: herramienta para generar **controladores de API**. Estos, a diferencia de los controladores de app normales, no tienen una vista `.html.erb` asociada, si no que retornan un json. El front (o una app mobile por ejemplo) puede hacer request a estos endpoints de API y obtener este json sin necesidad de recargar la página.
* Active Admin: herramienta que nos da una manera fácil y rápida de definir interfaces de administración. Básicamente con una sintáxis simple crea controllers y vistas que le permiten a un admin hacer el CRUD tradicional sobre un modelo en particular.
* Pundit: introduce el concepto de \*\*Policy, \*\*clases que funcionan como una capa de \*\*autorización \*\*en los controllers y en Active Admin. Chequean si el usuario actual tiene permitido acceder a una acción en particular.
* <https://github.com/heartcombo/devise>: funciona como una capa de **autenticación**, osea permite iniciar sesión y requerir que esta esté activa en ciertas acciones. Se usa en controllers y Active Admin.
* <https://github.com/drapergem/draper>: introduce el concepto de **Decorators**, clases donde va la lógica de presentación asociada a un modelo, que puede ser usada en varias vistas. En otras palabras, métodos que podrían ser de instancia de un modelo en particular, pero que serían únicamente para ser usados en vistas, entonces lo separamos de la lógica propiamente tal en el modelo.
* Active Job y Services: los jobs son el principal lugar donde definimos la **lógica de negocios** (además de tareas que se deban encolar o requieran recurrencia), dado que el MVC tradicional no tiene un lugar obvio para ésta. Es común verlos en controllers con un `perform_now` encapsulando lógica que va más allá del CRUD simple y el manejo de la response/request que hace el controller. También es común verlo en Observers con un `perform_later`, gatillando algún side-effect debido al update o create de un record.

  Los Services, aunque menos comunes, también se usan para lógica de negocio. La diferencia es que exponen varios métodos, y la idea es que estos estén fuertemente relacionados y hagan uso de los mismos parámetros.
* Observers: clases asociadas a un modelo que gatilla “algo” frente a cambios en un record. Tiene los mismos [callbacks que un modelo](https://guides.rubyonrails.org/active_record_callbacks.html), pero la idea es que en el observer vaya todo cambio que **sea un side-effect externo a la instancia.** En otras palabras, todo cambio que deba ir en un callback que toque al objeto mismo debe ir en el modelo, y todo efecto externo (mandar una notificación a slack por ejemplo), en el observer.
* \[DRAFT] Clients: clases dónde se definen las interacciones con alguna **página web externa**, ya sea llamadas a una **API o scrapping**. Es común verlo en Job que haga algo con la respuesta, o directo en un controller u observer.
* Data Migrate: gema que separa las migraciones de datos de las de schema. Rails tiene el concepto de migraciones, pero estas definen la manera de cambiar la estructura de la base de datos. Hay veces que no solo se quiere cambiar la estructura (columnas y tablas), sino que también la data misma (las filas). Para eso, usamos migraciones de data por separado.
* Vue: framework de JS que usamos para armar la UI. Usamos `.html.erb` todavía para algunas cosas, pero la mayoría de nuestro front lo estamos construyendo en Vue. La interacción más común que tenemos es que **Rails maneja el ruteo**, pero las vistas se construyen con Vue. Esto quiere decir que tenemos controladores no-API con sus acciones, y esas acciones tienen un `.html.erb` asociado, y dentro de esa vista es común que solo haya un componente Vue que recibe props desde Rails.
* [Tailwind](https://tailwindcss.com/): framework CSS que usamos para estilizar nuestras vistas. Prácticamente no usamos CSS puro o SCSS, en casi todos los casos usamos Tailwind. Se usa en vistas `.html.erb` o en componentes Vue

En este diagrama se muestran más o menos las interacciones descritas:

![](/files/YCCx43fYEH6k0EH6iEkG)

<https://app.diagrams.net/#G16C9xcj5y6dPcNtThtR819z8DFbTTasOe>


# Ruby/Rails

[Rails](https://github.com/platanus/la-guia/blob/master/stack/setup/configuracion_de_proyectos/rails.md)

[Power Types](/stack/ruby_rails/power_types)

[Potassium](/stack/ruby_rails/potassium)

[Power API](/stack/ruby_rails/power_api)

[Active Admin](/stack/ruby_rails/active_admin)

[Pundit](/stack/ruby_rails/pundit)

[Shrine](/stack/ruby_rails/shrine)

[Pry](/stack/ruby_rails/pry)

[Strong Migrations](/stack/ruby_rails/strong_migrations)

[Data Migrate](/stack/ruby_rails/data_migrate)

[Active Job](/stack/ruby_rails/active_job)

[Gems](/stack/ruby_rails/gems)

[Engines - Modularización en Rails](/stack/ruby_rails/engines_modularizacion_en_rails)

[Sentry](https://github.com/platanus/la-guia/blob/master/stack/ruby_rails/sentry.md)

[State Machine AASM](https://github.com/platanus/la-guia/blob/master/stack/ruby_rails/state_machine_aasm.md)


# Power Types

[General](/stack/ruby_rails/power_types/general)

[Patrones](/stack/ruby_rails/power_types/patrones)


# General

Power Types es una [gema](https://github.com/platanus/power-types) desarrollada por **Platanus** que promueve el uso de estos poderosos patrones: **Services**, **Commands**, **Utils** y **Values**.

Estos se basan en el [SRP (Single Responsability Principe)](https://blog.platan.us/solid-single-responsability), que nos dice que cada clase debe tener **1** sola función. Por ejemplo, si tenemos un modelo con operaciones complejas como este:

```ruby
class User
  def upgrade_membership
    # ...
  end

  def notify_external_system
    # ...
  end

  def register_payment_card
    # ...
  end
end
```

Deberíamos llevar cada una de sus funciones a Commands o Services independientes:

```ruby
class UpgradeMembership < Command
  # ...
end

class ExternalNotifierService < Service
  # ...
end

class RegisterPaymentCard < Command
  # ...
end
```

Estructurando nuestro código de forma modular y desacoplada tenemos las siguientes ventajas:

* Menos riesgo: Aislar errores, no pisar variables
* Más claridad, que hace cada clase
* DRYness
* Unit Testing de cada funcionalidad

## Referencias

Para mayor información sobre esta gema, visita los siguientes vínculos:

* [Services, Commands y otros poderosos patrones en Rails](https://blog.platan.us/services-commands-y-otros-poderosos-patrones-en-rails-27c2d3aa7c2e)
* [Anatomy of a Rails Service Object](http://multithreaded.stitchfix.com/blog/2015/06/02/anatomy-of-service-objects-in-rails/)


# Patrones

[Commands](/stack/ruby_rails/power_types/patrones/commands)

[Utils](/stack/ruby_rails/power_types/patrones/utils)

[Services](/stack/ruby_rails/power_types/patrones/services)

[Values](/stack/ruby_rails/power_types/patrones/values)

[Observers](/stack/ruby_rails/power_types/patrones/observers)

[Clients](https://github.com/platanus/la-guia/blob/master/stack/ruby_rails/power_types/patrones/clients.md)

[Presenters](https://github.com/platanus/la-guia/blob/master/stack/ruby_rails/power_types/patrones/presenters.md)


# Commands

> 🚨 En Platanus dejamos de usar comandos. Ahora estamos usando Active Job como lugar para la lógica de negocio

Los *Comandos* son clases destinadas a realizar operaciones acotadas e independientes. Se implementan a través de un método `perform` que recibe argumentos y realiza operaciones con ellos entregando un resultado. También poseen un generador para construir su estructura,

```bash
$ rails generate command DoSomething foo
```

Esto generará una clase que implementa el método `perform`

```ruby
class DoSomething < PowerTypes::Command.new(:foo, :bar)
  def perform(args)
  end
end
```

Luego pueden ser llamados y ejecutados de la siguiente forma,

```ruby
result = DoSomething.for(foo: waffle, bar: pancake)
```

Donde `:foo, :bar` son los argumentos. Están disponibles en el comando como variables de instancia `@foo, @bar`


# Utils

Las utils son helpers que nos permiten agrupar métodos relacionados. Tienen las siguientes particularidades:

* No tienen lógica del dominio de la aplicación donde se usan.
* Suelen ser clases estáticas.
* Su uso normalmente se repite por toda la aplicación.

## ¿Por qué la usamos?

Usamos utils porque Rails no define un lugar específico para este tipo de helpers.

En Rails todo lo que no es del dominio de una app se coloca dentro de `/lib`. En platanus reservamos este directorio para toda aquella lógica que sea candidata a convertirse en una gema y no para simples helpers.

## ¿Cómo la usamos?

### Instalación

Vienen con la gema [power-types](https://github.com/platanus/power-types#installation) que normalmente viene instalada en nuestro proyectos generados con [potassium](https://github.com/platanus/potassium).

### Uso básico

Luego de instalada la gema, se corre el generador:

`bin/rails generate util util_name method_1 method_2 method_n`

Por ejemplo si queremos crear una clase util para simplificar el uso de enums podemos hacer:

`bin/rails generate util Enums translate_enum_attr translate_aasm_attr enum_options_for_select`

Esto generará el helper en: `app/utils/enums_utils.rb`

```ruby
class EnumsUtils < PowerTypes::BaseUtil
  def self.translate_enum_attr(class_name, enum, key)
    I18n.t(
      "activerecord.attributes.#{class_name.model_name.i18n_key}.#{enum.to_s.pluralize}.#{key}"
    )
  end

  def self.enum_options_for_select(class_name, enum)
    class_name.send(enum.to_s.pluralize).map do |key, value|
      [translate_enum_attr(class_name, enum, key), value]
    end
  end
end
```

Luego se usaría así:

```ruby
supply_purchase.supply_type = :fertilizer
EnumUtils.translate_enum_attr(Supply, :supply_type, supply_purchase.supply_type) #=> "Fertilizante"

EnumUtils.enum_options_for_select(Supply, :supply_type) #=> [["Fertilizante", :fertilizer], ["Agroquímicos", :agrochemical]]
```

Repasemos las particularidades de las utils contra el ejemplo:

* “No tienen lógica del dominio de la aplicación donde se usan.” → En este caso se cumple ya que `EnumsUtils` podría moverse a otra aplicación y seguiría funcionando. No hay lógica específica del proyecto.
* “Suelen ser clases estáticas.” → Como se puede ver `translate_enum_attr` y `enum_options_for_select` son métodos de clase. No crea una instancia de `EnumUtils` en ningún momento.
* “Su uso normalmente se repite por toda la aplicación” → Sirve para el modelo `Supply` que usamos en el ejemplo pero también podría servir para cualquier otro modelo de la aplicación.

### Recursos útiles

* [Presentación platanus](https://www.youtube.com/watch?v=vAVq-WxIodI\&list=PL4jJY1sbBn7C9aNISuaOwRnmhYKjJYdfA\&index=2\&t=1944s)
* [Documentación de power types](https://github.com/platanus/power-types#utils)


# Services

Los servicios son objetos de Ruby destinados a separar la lógica de negocios del resto de la aplicación. Éste permite agrupar muchos métodos que pertenezcan a la misma lógica de negocios.

A menudo pueden ser confundidos con `Clients`, ya que su estructura es bastante similar: una clase que agrupa métodos de una misma lógica. La diferencia entre estos radica en su función más general, como explicamos antes los servicios agrupan lógica de negocios (de la aplicación), mientras que los clientes son hechos para comunicarnos con con servicios externos, como por ejemplo consumir una API. Este podría ser removido de la aplicación actual, ser utilizado en otra y seguir funcionando. Un servicio puede consumir un cliente.

Acá puedes ver un servicio de `Bsale` (sistema de ventas) que agrupa 2 métodos que toman una orden (modelo de la aplicación), crean un factura o una nota en `Bsale` (a través de un `Client`) y guardan el documento generado en la base de datos del proyecto.

```bash
$ rails generate service BsaleService order
```

Esto generará una clase cuyo nombre termina, por convención en `..Service`

```ruby
class BsaleService < PowerTypes::Service.new(:order)
  def create_commercial_invoice
    return unless @order.invoiceable?

    document = client.post_commercial_invoice(@order)
    return unless document.success?

    save_document(document, 'commercial_invoice')
    @order.generate_invoice!
  end

  def create_credit_note
    return if invoice.nil?

    invoice_details = client.get_commercial_invoice_details(invoice)
    return unless invoice_details.success?

    credit_note = client.post_credit_note(@order, invoice, invoice_details)
    return unless credit_note.success?

    document = client.get_document(credit_note)
    return unless document.success?

    save_document(document, 'credit_note')
    @order.cancel_invoice!
  end

  private

  def client
    @client ||= BsaleClient.new
  end

  def invoice
    @invoice ||= @order.last_invoice
  end

  def save_document(document, document_type)
    @order.last_payment.documents.create!(
      document_type: document_type,
      document_url: document.url,
      document_identifier: document.id
    )
  end
end
```

Luego pueden ser utilizados fácilmente instanciando la clase y llamando a sus métodos.

```ruby
service = BsaleService.new(order: order)
result = service.create_credit_note
```

### **Servicios vs Jobs**

Los jobs tienen una función similar a la de los servicios, separar la lógica de negocios del resto de la aplicación, sin embargo, estos separan una función en específico, mientras que el servicio puede separar un grupo de funciones que apuntan a la misma lógica.

Aún así, podemos replicar el funcionamiento de un servicio con varios jobs bajo un mismo namespace, donde cada uno tiene una función específica correspondiente a uno de los métodos del service.

```
- Jobs
  - Bsale
    - base_job.rb
    - create_commercial_invoice_job.rb
    - create_credit_note_job.rb
```

```ruby
class Bsale::CreateCreditNoteJob < ApplicationJob
  def perform(resource)
    # code
  end
end
```

Entonces, ¿Cuándo debemos usar cada una de estas opciones? La verdad es que no hay ninguna regla que diga cuándo usar alguno de estos, por lo que queda a gusto del consumidor, pero podemos nombrarte los pro y contras de cada opción.

Al usar servicios es fácil compartir lógica en todos sus métodos mediante algún método privado, es más cómodo encontrar los métodos que encapsulan toda esa lógica de negocios en un solo archivo, sin embargo, es fácil que este empiece a crecer con lógica que realmente no pertenece a ese espacio, quizá lógica necesaria para llevarlo a cabo, pero que no debería estar ahí.

Al usar jobs el compartir lógica entre todos se debe realizar mediante un `base_job` y todos los demás jobs deben heredar de este, los archivos están separados por cada función, lo cual lo hace un poco más verboso, sin embargo, al tener en cada job una función específica, no se presenta el problema de hacer crecer estos archivos con lógica que no pertenece a ellos, o es más fácil de detectar.


# Values

Los values corresponden a clases Ruby que pueden ser utilizadas para contener información que no persiste en la base de datos, y por lo tanto solo existe en memoria. Entonces si por ejemplo, generamos dinámicamente un reporte, en vez de retornarlo como `Hash`:

```ruby
class BuildCleaningReport < PowerTypes::Command.new(:data)
  def perform
    # execute report logic, and finally return:
    {
      date: @date,
      area: cleaned_area,
      duration: cleaning.time,
      effiency: cleaned_area / cleaning.time
    }
  end
end
```

Mejor encapsular el resultado en una clase `Report`:

```ruby
# app/values/report.rb
class Report
  attr_accesor :date, :area, :duration

  def eficciency
    area / duration
  end
end
```

Estos objetos pueden ser utilizados para mover la información de forma estructurada dentro de las distintas capas de la aplicación.


# Observers

Un observer es una clase que, como dice su nombre se encarga de observar al modelo que lleva su nombre y tienen una labor muy parecida o igual a los [callbacks](https://guides.rubyonrails.org/active_record_callbacks.html) de modelos.

Por ejemplo si queremos llamar una función cada vez que se crea una instancia de un modelo lo podemos hacer de la siguiente manera:

```bash
$ rails generate observer MyModel
```

Esto generará un observer de `MyModel`

```ruby
class MyModelObserver < PowerTypes::Observer
  after_create: :puts_hello

  def puts_hello
    puts 'hello'
  end
end
```

Luego en `MyModel` debemos añadir:

```ruby
class MyModel < ActiveRecord::Base
  include PowerTypes::Observable
end
```

Ahora cada vez que se ejecute

```ruby
MyModel.create
```

se ejecutará `puts_hello`.

Esto también se puede hacer con callbacks de la siguiente manera:

```ruby
class MyModel < ActiveRecord::Base
  after_create: :puts_hello

  def puts_hello
    puts 'hello'
  end
end
```

Como pueden ver las dos formas son equivalentes. Entonces ¿Por qué usar observers?

### **¿Por qué ocupar observers?**

Porque nos permite desacoplar lógicas de los modelos que no están directamente relacionadas con ellos.

### **¿Cuál es el criterio para poner algo en un callback o en el observer?**

Por un lado en los callbacks va todo lo necesario para mantener la integridad del objeto, por ejemplo, el formateo de un rut. Si guardamos un objeto sin formato de rut y todos los demás están formateados, entonces ese modelo en sí estará "corrupto".

Por otro lado, en observers debería ir toda la lógica que está relacionada con el modelo pero que no es necesaria para mantener la integridad de este, por ejemplo, el envío de un mail.

Entonces la regla general sería algo como: ¿Mi objeto puede vivir sin esto?, si la respuesta es sí, entonces va en un observer, si la respuesta es no, va en un callback.

Por último la lógica no debe estar literalmente dentro del observer, lo mejor es que en el observer se llame algún job, comando, value, etc y que estos manejen la lógica. Por ejemplo:

```ruby
class SalesObserver
  after_update :add_to_sales_report

  def generate_report
    AddToSalesReportJob.perform_later(object) # En este job va toda la lógica
  end
end
```

### **¿Qué son y cuándo usar los callbacks after commit?**

Los modelos de ActiveRecord tienen [callbacks de transacción](https://guides.rubyonrails.org/active_record_callbacks.html#transaction-callbacks) dentro de los que está el callback `after_commit`. Este puede ser ejecutado después del `create`, `update` o `save`, asegurando que su ejecución sea después de que los cambios hayan sido guardados efectivamente en la base de datos, es decir, después del commit.

PowerTypes permite utilizar estos callbacks dentro de los observers. Se llaman `after_create_commit`, `after_update_commit` y `after_save_commit`.

La recomendación es usar estos callbacks cuando se ejecute algo que deberá volver a buscar el objeto a la base de datos. Por ejemplo, si en un callback `after_save` se encola un job que recibe el objeto como parámetro, y este job es ejecutado instantáneamente por Sidekiq, ocurrirá que irá a buscar la instancia a la base de datos justo antes de que se haga el commit de los cambios, generando una ejecución inconsistente del job ya que se utilizó la versión no actualizada del objeto. En la siguiente imagen se muestra un ejemplo:

![](/files/qxWjepp3gii5t6O3LJgz)

En el ejemplo anterior, el callback `after_save` es ejecutado antes de que el objeto sea guardado en la base de datos, mientras que el `after_save_commit` es ejecutado después del commit, asegurando que los cambios ya están guardados en la base de datos.


# Potassium

Para crear nuestras aplicaciones Rails con toda la configuración y herramientas que comúnmente utilizamos en Platanus.

<https://github.com/platanus/potassium>


# Power API

Es un [engine de Rails](https://guides.rubyonrails.org/engines.html#what-are-engines-questionmark) desarrollado por **Platanus** que reúne un conjunto de gemas y configuraciones pensadas para construir APIs REST de calidad.

### ¿Por qué la usamos?

Por dos motivos:

1. Porque en esta gema hemos ido recopilando todas aquellas herramientas que consideramos nos han sido útiles a lo largo de nuestros desarrollos en Platanus.
2. Porque todas estas configuraciones varían muy poco (o nada) de proyecto en proyecto. Por esto, nos pareció buena idea realizar estas mejoras en un único lugar para luego ocupar en todas nuestras aplicaciones.

### ¿Cómo la usamos?

### Instalación

La gema viene instalada si el proyecto se generó usando [Potassium](https://github.com/platanus/potassium) con la opción `api` activada. Si el proyecto no fue creado con Potassium, se puede instalar siguiendo las instrucciones de [README](https://github.com/platanus/power_api#installation).

### Usamos el generador de la gema para agregar un recurso a la API

Supongamos que tenemos el modelo:

```ruby
class Blog < ApplicationRecord
end
```

y queremos tener los típicos endpoints REST:

```
GET     /api/internal/blogs
POST    /api/internal/blogs
GET     /api/internal/blogs/:id
PUT     /api/internal/blogs/:id
DELETE  /api/internal/blogs/:id
```

Para esto, deberíamos ejecutar el siguiente generador:

```bash
rails g power_api:controller blog
```

Esto creará el controlador (dentro de `/app/controllers/api/internal/blogs_controller.rb`) y todo lo necesario para que tengas tus endpoints funcionando. Para más información u opciones que permite el comando, revisa la [documentación de la gema](https://github.com/platanus/power_api#controller-generation-exposed-and-internal-modes)

```ruby
class Api::Internal::BlogsController < Api::Internal::BaseController
  def index
    respond_with Blog.all
  end

  def show
    respond_with blog
  end

  def create
    respond_with Blog.create!(blog_params)
  end

  def update
    blog.update!(blog_params)
    respond_with blog
  end

  def destroy
    respond_with blog.destroy!
  end

  private

  def blog
    @blog ||= Blog.find_by!(id: params[:id])
  end

  def blog_params
    params.require(:blog).permit(
      :title,
      :body
    )
  end
end
```

**Tener en cuenta:**

El generador sirve para crear los endpoints REST típicos y [anidados](https://github.com/platanus/power_api#--parent-resource). Si necesitas algo custom, deberás hacerlo a mano. Pero siempre ten en cuenta que si piensas en recursos REST, probablemente encontrarás una forma de modelar que calce con lo que ofrece el generador.

### Usamos [AMS](https://github.com/rails-api/active_model_serializers) para estructurar el formato de respuesta de nuestra API

Siguiendo el ejemplo anterior, si corremos el generador, se agregará el siguiente serializer en `/app/serializers/api/internal/blog_serializer.rb`

```ruby
class Api::Internal::BlogSerializer < ActiveModel::Serializer
  type :blog

  attributes(
    :id,
    :title,
    :body,
    :created_at,
    :updated_at
  )
end
```

Al ejecutar por ejemplo la request `GET /api/internal/blogs` obtendremos algo así:

![](/files/OpWaPXXRii1FVFxZ3kZo)

**Tener en cuenta:**

* Siempre utiliza serializers para responder con el API. Es importante que todos los endpoints devuelvan la información con el mismo formato.

### Usamos un [concern de Rails](https://api.rubyonrails.org/classes/ActiveSupport/Concern.html) para manejar los errores que genera la API

Supongamos que tenemos el siguiente controller con el `create` endpoint y supongamos también que el atributo `title` es requerido:

```ruby
class Api::Internal::BlogsController < Api::Internal::BaseController
  def create
    respond_with Blog.create!(blog_params)
  end

  def blog_params
    params.require(:blog).permit(
      :title,
      :body
    )
  end
end
```

Al ejecutar la request `POST /api/internal/blogs` sin enviar el atributo `title`, se lanzará una exception que será manejada por el [concern](https://github.com/platanus/power_api#the-apierror-concern) devolviendo una respuesta con formato estándar:

![](/files/9kDDZ5hMPzISOQtRQRvK)

**Tener en cuenta:**

* Siempre lanza exceptions y deja que el concern los maneje. No hagas condicionales ni devuelvas errores custom. Los errores siempre deberían tener el mismo formato y ser manejados en un único lugar (el concern).
* Si necesitas manejar algún tipo de error específico, siempre podrás agregarlo así:

  ```ruby
  class Api::BaseController < PowerApi::BaseController
    rescue_from "MyCustomErrorClass" do |exception|
      respond_api_error(:bad_request, message: "some error message", detail: exception.message)
    end
  end
  ```

### Usamos un [custom responder](https://github.com/platanus/power_api#the-apiresponder) para manejar las respuestas de nuestra API

Siguiendo el ejemplo anterior:

```ruby
class Api::Internal::BlogsController < Api::Internal::BaseController
  def create
    respond_with Blog.create!(blog_params)
  end

  def blog_params
    params.require(:blog).permit(
      :title,
      :body
    )
  end
end
```

Al ejecutar `POST /api/internal/blogs`, el método `respond_with` invocará al responder y entregará el objeto `Blog` que se acaba de crear usando el serializer y, por tratarse de un `POST`, entenderá que debe devolver el HTTP status code 201 (created).

El responder además devolverá:

* Un HTTP status code 200 OK si se trata de un `GET` o un `PUT` además del/los recurso/s serializado/s con AMS.
* Un HTTP status code 204 No Content si se trata de un `DELETE`.

**Tener en cuenta:**

* Evitar usar `render`. El uso de `render` se salta el responder y nos obliga a especificar el código y recurso a devolver en el mismo controller. Esto es una mala práctica porque es algo que debería definirse en un único lugar y ser consistente a través de todos los controladores.

### Usamos RSpec para testear nuestra API

Si usamos el generador `rails g power_api:controller blog`, se crearán dentro del directorio: `spec/requests/api/internal/blogs_spec.rb` los tests para nuestro controlador.

```ruby
require 'rails_helper'

RSpec.describe 'Api::Internal::BlogsControllers', type: :request do
  describe 'GET /index' do
    let!(:blogs) { create_list(:blog, 5) }
    let(:collection) { JSON.parse(response.body)['blogs'] }
    let(:params) { {} }

    def perform
      get '/api/internal/blogs', params: params
    end

    before do
      perform
    end

    it { expect(collection.count).to eq(5) }
    it { expect(response.status).to eq(200) }
  end

  describe 'POST /create' do
    let(:params) do
      {
        blog: {
          title: 'Some title',
          body: 'Some body'
        }
      }
    end

    let(:attributes) do
      JSON.parse(response.body)['blog'].symbolize_keys
    end

    def perform
      post '/api/internal/blogs', params: params
    end

    before do
      perform
    end

    it { expect(attributes).to include(params[:blog]) }
    it { expect(response.status).to eq(201) }
  end

  describe 'GET /show' do
    let(:blog) { create(:blog) }
    let(:blog_id) { blog.id.to_s }

    let(:attributes) do
      JSON.parse(response.body)['blog'].symbolize_keys
    end

    def perform
      get '/api/internal/blogs/' + blog_id
    end

    before do
      perform
    end

    it { expect(response.status).to eq(200) }

    context 'with resource not found' do
      let(:blog_id) { '666' }

      it { expect(response.status).to eq(404) }
    end
  end

  describe 'PUT /update' do
    let(:blog) { create(:blog) }
    let(:blog_id) { blog.id.to_s }

    let(:params) do
      {
        blog: {
          title: 'Some title',
          body: 'Some body'
        }
      }
    end

    let(:attributes) do
      JSON.parse(response.body)['blog'].symbolize_keys
    end

    def perform
      put '/api/internal/blogs/' + blog_id, params: params
    end

    before do
      perform
    end

    it { expect(attributes).to include(params[:blog]) }
    it { expect(response.status).to eq(200) }

    context 'with resource not found' do
      let(:blog_id) { '666' }

      it { expect(response.status).to eq(404) }
    end
  end

  describe 'DELETE /destroy' do
    let(:blog) { create(:blog) }
    let(:blog_id) { blog.id.to_s }

    def perform
      delete '/api/internal/blogs/' + blog_id
    end

    before do
      perform
    end

    it { expect(response.status).to eq(204) }

    context 'with resource not found' do
      let(:blog_id) { '666' }

      it { expect(response.status).to eq(404) }
    end
  end
end
```

### Usamos [API Pagination](https://github.com/davidcelis/api-pagination) para la paginación

Para activarla simplemente debemos ejecutar el método `paginate` así:

```ruby
class Api::Internal::BlogsController < Api::Internal::BaseController
  def index
    respond_with paginate(Blog.all)
  end
end
```

Para ver el recurso paginado se deberá ejecutar la request así:

```
http://localhost:3000/api/internal/blogs?page[number]=2&page[size]=5
```

Esto agregará:

* Headers relacionados con la paginación.
* Links a `self`, `first`, `prev`, `next` y `last` en el serializer.

### Usamos [Ransack](https://github.com/activerecord-hackery/ransack) para el filtrado de recursos

Para activarlo simplemente debemos ejecutar el método `filtered_collection` así:

```ruby
class Api::Internal::BlogsController < Api::Internal::BaseController
  def index
    respond_with filtered_collection(Blog.all)
  end
end
```

Para filtrar la información se deberá ejecutar la request así:

```
<http://localhost:3000/api/internal/blogs?q[title_eq]=Vile%20Bodies>
```

Para más opciones de filtrado revisa [la documentación](https://github.com/activerecord-hackery/ransack#search-matchers)

### Modo internal vs. Modo exposed

El modo internal se utiliza cuando la API va a ser consumida por un cliente front que comparte sesión con la API. En cambio, la API modo exposed está pensada para ser usada por clientes que están servidos en otro lado. Las principales diferencias son que:

* En modo internal se usa [devise](https://github.com/heartcombo/devise) para autenticar los recursos. En cambio en el modo exposed se usa [Simple Token Authentication](https://github.com/gonzalo-bulnes/simple_token_authentication) (que se instala sobre devise)
* En modo internal no hay versiones. Se entiende que la API tendrá un único cliente entonces no tiene sentido versionar. Por esto en modo internal verás controllers como: `Api::Internal::BlogsController` y en exposed: `Api::V1::BlogsController`, `Api::V2::BlogsController`, etc.

> ℹ️ Es importante saber que se puede tener en un mismo proyecto una api internal y exposed.

### Usamos [Simple Token Authentication](https://github.com/gonzalo-bulnes/simple_token_authentication) para autorizar el acceso a nuestra API (solo en modo exposed)

Para ver el funcionamiento, puedes visitar el README de la gema pero en corto lo que hacemos es lo siguiente:

1. Corriendo el generador de la gema, crearemos una migración que agregará un `authentication_token` al modelo que queremos autorizar.
2. Ejecutaremos en el modelo a autorizar el método `acts_as_token_authenticatable` así:

   ```ruby
   class User < ApplicationRecord
     acts_as_token_authenticatable

     # more code...
   end
   ```
3. Luego en el controller que queremos que sea autorizado ejecutaremos `acts_as_token_authentication_handler_for` así:

   ```ruby
   class Api::V1::BlogsController < Api::V1::BaseController
     acts_as_token_authentication_handler_for User, fallback: :exception

     # endpoints...
   end
   ```

   Esto exigirá que las requests sean firmadas con email y token
4. Ejecutaremos la request así: `GET <http://localhost:3000/api/v1/blogs/1?user_email=developer@platan.us?user_token=xxx`>

**Tener en cuenta:**

* Podemos correr el instalador de Power API así:

  ```bash
  rails g power_api:exposed_api_config --authenticated-resources=user
  ```

  para que deje lista la configuración de Simple Token Authentication
* Por defecto el token se manda por query string, quizás sería bueno cambiar la configuración de Simple Token Authentication para que se mande por header.
* La gema no viene con un endpoint que permita conseguir el token del usuario. Por esto, es buena idea crear a mano un controller `login_attempts_controller.rb` con un action `create` que devuelva el token para un `email` y `password`.

### Recursos útiles

* [Power API](https://github.com/platanus/power_api)
* [Blog Post: Cómo crear una API REST en Rails testeada, documentada y con buenas prácticas en 1 minuto y 54 segundos.](https://blog.platan.us/c%C3%B3mo-crear-una-api-rest-en-rails-testeada-documentada-y-con-buenas-pr%C3%A1cticas-en-1-minuto-y-54-4839009318a0) (Está un poco desactualizado el ejemplo pero el espíritu es el mismo)
* [API Pagination](https://github.com/davidcelis/api-pagination)
* [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers)
* [Ransack](https://github.com/activerecord-hackery/ransack)
* [Responders](https://github.com/heartcombo/responders)
* [Simple Token Authentication](https://github.com/gonzalo-bulnes/simple_token_authentication)


# Active Admin

[General](/stack/ruby_rails/active_admin/general)

[2FA - Active Admin](https://github.com/platanus/la-guia/blob/master/stack/ruby_rails/active_admin/2fa_active_admin.md)

[Active Admin Addons](https://github.com/platanus/la-guia/blob/master/stack/ruby_rails/active_admin/active_admin_addons.md)


# General

Es una gema que utilizamos como back office.

## ¿Por qué la usamos?

La utilizamos porque resuelve rápidamente los típicos CRUD de admin. Con ActiveAdmin en unos minutos puedes tener para un recurso (modelo ActiveRecord): el menu para acceder, las vistas para crear/editar, el listado paginado con filtros y varias cosas más.

## ¿Cómo la usamos?

### Instalación

La gema viene instalada si el proyecto se generó usando [Potassium](https://github.com/platanus/potassium). Si no es así, igual puedes agregarlo luego ejecutando `potassium install admin`.

### Uso básico

Supongamos que tenemos el modelo `Blog` con los atributos `title`, `body` y `user` (owner del Blog). Si agregamos bajo `/app/admin/blogs.rb` el siguiente código:

```ruby
ActiveAdmin.register Blog do
end
```

Al acceder a `http://localhost:3000/admin/blogs` veremos el listado de blogs:

![](/files/vYhumOl12TOs3SlkM6eF)

y si hacemos clic en el link "Editar" de alguno de los blogs veremos el formulario:

![](/files/mpN7yT2ptlPSITLJstnP)

Listo! Eso es todo lo que necesitas hacer para tener algo funcionando. De todos modos, en un proyecto Platanus comúnmente verás algo como esto:

```ruby
ActiveAdmin.register Blog do
  permit_params :title, :body, :user_id

  filter :title

  index do
    selectable_column
    id_column
    column :title
    column :user
    actions
  end

  show do
    attributes_table do
      row :title
      row :body
      row :user
    end
  end

  form do |f|
    f.semantic_errors

    f.inputs do
      f.input :title
      f.input :body
      f.input :user
    end

    f.actions
  end
end
```

para tener más control de lo que se quiere mostrar. Por ejemplo, así se ve el index con la configuración custom:

![](/files/e2yWWD20eHfMn13XUI6t)

### I18n

Active Admin toma la configuración de locales de Rails para traducir los nombres de las columnas. Por ejemplo, para traducir los atributos de `Blog` deberías tener la siguiente configuración:

`/config/locales/es-CL.yml`

```yaml
es-CL:
  activerecord:
    attributes:
      blog:
        title: Título
        body: Texto
        user: Dueño
```

De esta manera al entrar por ejemplo la vista de un `Blog` verás los atributos traducidos:

![](/files/HpPxgzyHCtf1Heny33l8)

Ten en cuenta que también funciona con métodos (getters) custom. Por ejemplo, podrías tener:

`/app/models/blog.rb`

```ruby
class Blog < ApplicationRecord
  def my_method
    "Hola!"
  end
end
```

`/config/locales/es-CL.yml`

```yaml
es-CL:
  activerecord:
    attributes:
      blog:
        my_method: Mi método
```

`/app/admin/blogs.rb`

```ruby
ActiveAdmin.register Blog do
  index do
    selectable_column
    id_column
    column :my_method
    actions
  end

  show do
    attributes_table do
      row :my_method
    end
  end
end
```

y funcionará.

### Menú

**Agrupar recursos**

Para agrupar varios items dentro de un mismo menú, debes:

1. Definir el menú en el initializer de active admin:

   `/config/initializers/active_admin.rb`

   ```ruby
   ActiveAdmin.setup do |config|
     config.namespace :admin do |admin|
       admin.build_menu do |menu|
         menu.add id: :some_menu_id, label: "Grupo"
       end
     end
   end
   ```
2. Definir en el recurso su menú padre:

   `/app/admin/blogs.rb`

   ```
   ActiveAdmin.register Blog do
     menu parent: :some_menu_id
   end
   ```

   ![](/files/Qwrd65g0vUwKAwuxaHsf)

**Ocultar menú**

Se hace de la siguiente manera:

`/app/admin/blogs.rb`

```ruby
ActiveAdmin.register Blog do
  menu false
end
```

A simple vista parece no tener mucha utilidad pero lo importante aquí es que aunque no exista el menú, igual existen los endpoints. Algo que puede ser conveniente si queremos usar alguna ruta del admin como una API.

![](/files/rZelG8iJTgADf1unnnB2)

**Menú condicional**

Puede ser útil mostrar u ocultar un menú dependiendo de una condición. El caso más típico es el de roles. Por ejemplo:

`/app/admin/blogs.rb`

```ruby
ActiveAdmin.register Blog do
  menu if: -> { current_admin_user.super_admin? }
end
```

### Permisos (Pundit)

En Platanus usamos Active Admin con el [adapter de Pundit](https://activeadmin.info/13-authorization-adapter.html#using-the-pundit-adapter) para autorizar recursos. Si al registrar un nuevo recurso en AA, no tienes creado el policy de ese recurso, observarás un error así:

![](/files/RIcXPtgwiNu90T8aYgsn)

Si esto ocurre, agrega el policy correspondiente y define los permisos para cada una de las acciones del CRUD:

```ruby
class BlogPolicy < ApplicationPolicy
  def index?
    true
  end

  def show?
    true
  end

  def create?
    true
  end

  def new?
    create?
  end

  def update?
    true
  end

  def edit?
    update?
  end

  def destroy?
    true
  end
end
```

> Si todavía no estás en la instancia del proyecto en la cual tienes que preocuparte por permisos deja todas las acciones en true.

Es importante mencionar que si una acción no tiene permisos, esta desaparecerá del menú y links, etc. Por ejemplo:

```ruby
class BlogPolicy < ApplicationPolicy
  def show?
    false
  end
end
```

Al tratar de acceder a `http://localhost:3000/admin/blogs/303`

![](/files/xsqT9GPHk1UMTvmmtjDs)

Se puede ver además como el link a "Ver" desapareció:

![](/files/cYm8EfKY2fzArvKVsuKw)

### Action Items

Los `action_item`s son botones que se pueden agregar a las vistas del recurso. Por ejemplo, el siguiente código mostrará un botón (link), solo en la vista `index`, para ir al listado de administradores.

```ruby
action_item :go_to_admins, only: [:index] do
  link_to("Ver Administradores", admin_admin_users_path)
end
```

![](/files/CMUGtiuCCnrkRPFTrBdb)

> Ten en cuenta que puedes decidir en qué vistas aparecerá el botón usando la opción only.

### Member Action

Las `member_action`s son acciones extra que se pueden agregar al controller del recurso. Por ejemplo, el siguiente código:

```ruby
member_action :send_mail, method: :post do
  # Ejecuta algún código. Por ejemplo enviar el blog por mail a n usuarios.

  redirect_to admin_blogs_path
end
```

sumará el endpoint `/admin/blogs/:id/send_mail` a los endpoints del CRUD.

![](/files/eFZcxtp0AtIgw0Ul1iBA)

Ten en cuenta que se puede utilizar `action_item`s para ejecutar estas nuevas acciones. Por ejemplo, el siguiente código agregará un botón en la vista del blog (`show`) desde el que se llamará a la acción `send_mail`.

```ruby
action_item :send_mail, only: [:show] do
  link_to("Enviar a Admin Users", send_mail_admin_blog_path(resource), method: :post)
end
```

Una alternativa a los `action_item`s es agregar la acción al listado del index así:

```ruby
index do
  # ...
  actions do |blog|
    link_to("Enviar", send_mail_admin_blog_path(blog), method: :post)
  end
end
```

![](/files/oo30HuPyjsG7P6RhdBN1)

### Collection Action

Es lo mismo que una `member_action` pero sin apuntar a un recurso en particular sino a la colección. Por ejemplo, la siguiente `member_action`:

```ruby
member_action :send_mail, method: :post do
  # ...
end
```

creará el siguiente endpoint: `POST /admin/blogs/:id/send_mail`. En cambio, esta `collection_action`:

```ruby
collection_action :send_mails, method: :post do
  # ...
end
```

generará este: `POST /admin/blogs/send_mails`

La idea entonces con esto es que las `member_action`s se usen junto a `action_item`s en `show`, `new` y `update` y las `collection_action` con `action_item`s en el `index`.

> Ten en cuenta que dentro de una member\_action podrás acceder al recurso actual (en nuestro ejemplo un Blog de id x) usando el método resource. En cambio, en una collection\_action, podrás acceder al listado de recursos (lista de Blogs en el ejemplo), que está mostrando el index en ese momento, usando el método collection.

### Display Name

Si prestaste atención a la imagen del formulario de la sección "Uso básico", seguro notaste que el selector de usuarios no muestra correctamente el nombre de los mismos:

![](/files/SuN1y8eIcsaBr9QmRvoQ)

esto se debe a que Active Admin espera que los recursos tengan definido el método: `:display_name` para que puedan ser representados como "String". Si el recurso no lo implementa, simplemente llamará a `to_s` mostrando como resultado lo que vemos en el selector. Entonces, para solucionar esto, podemos hacer lo siguiente:

```ruby
class User < ApplicationRecord
  def display_name
    email
  end
end
```

![](/files/sDbYoYOjtuNfAcvDv3fN)

> Ten en cuenta que display\_name (o cualquiera de las otras opciones) se utilizará en varios lugares: en el título de un recurso, en los links de las rows/columns y, como vimos, en los selectores.

### Vistas custom

Hay veces que necesitamos agregar nuevos endpoints con HTML a medida. Para hacer esto hacer lo siguiente:

1. Agregar la `member_action`:

   ```
   member_action :preview, method: :get do
     @blog = resource
   end
   ```
2. Agregar el `action_item`:

   ```
   action_item :preview, only: [:show] do
     link_to("Previsualizar", preview_admin_blog_path(resource))
   end
   ```
3. Agregar la vista custom:

   Agregamos el HTML para nuestra nueva `member_action` en `/app/views/admin/blogs/preview.html.erb`

   ```html
   <h1><%= @blog.title %></h1>
   <p><%= @blog.body %></p>
   ```

   ![](/files/yiVSMMWpSfJxCFnxXzPf)

   Ten en cuenta que puedes agregar el archivo con extensión `.arb` en vez de `.erb` y usar la gema [Arbre](https://activeadmin.github.io/arbre/) que es el DSL que Active Admin utiliza para dibujar sus vistas. El siguiente código:

   `/app/views/admin/blogs/preview.html.arb`

   ```ruby
   h1 { resource.title }
   para { resource.body }
   ```

   sería equivalente a lo de `/app/views/admin/blogs/preview.html.erb` en Arbre.

### Link de download para archivos

> Para manejo de archivos se supone el uso de Shrine

Suponiendo que el blog tiene un archivo `image`:

1. Agregar la `member_action`:

   ```ruby
   member_action :download, method: :get do
     blog = Blog.find(params[:id])
     send_file blog.image.download
   end
   ```
2. Agregar el `action_item`:

   ```ruby
   action_item :download, only: :show, if: -> { blog.image.present? } do
     link_to 'Download', download_admin_blog_path(blog)
   end
   ```
3. Agregar link a las `actions` en el index

   ```ruby
   index do
     column :id
     .
     .
     .
     actions defaults: true do |blog|
       link_to 'Download Image', download_admin_blog_path(blog)
     end
   end
   ```

### JavaScript en una vista

Si necesitas agregar alguna pequeña inteligencia en una vista de Active Admin revisa nuestra guía sobre [AlpineJS](https://www.notion.so/js/alpine/README.md).

### Active Admin Addons

Es una [gema](https://github.com/platanus/activeadmin_addons) construida en Platanus sobre Active Admin y la utilizamos para facilitar algunas features comunes. Por ejemplo: [inputs con select2](https://github.com/platanus/activeadmin_addons/blob/master/docs/select2_default.md), [selector de booleanos en index/show](https://github.com/platanus/activeadmin_addons/blob/master/docs/toggle_bool.md), [selector de colores](https://github.com/platanus/activeadmin_addons/blob/master/docs/color-picker.md), etc.

### Inputs custom

Muchas veces en un formulario de Active Admin necesitamos un input custom. Por ejemplo: selectores de moneda, de colores, con búsqueda ajax, etc. Por esto, Active Admin ofrece una forma sencilla de agregar estos controles. Para mostrarte cómo funciona te mostraré en pasos como agregar un input que escribe con `console.info` lo que se escribió en input en el evento focus out.

1. Agregar el archivo con mi input en `/app/inputs/logger_input.rb`.

   > Ten en cuenta que el nombre del archivo debe tener la forma: \[nombre\_control]\_input y lo mismo para el nombre de la clase, pero en CamelCase.

   ```
   class LoggerInput < ActiveAdminAddons::InputBase
     def render_custom_input
       concat(label_html)
       concat(builder.text_field(method, input_html_options))
     end
   end
   ```
2. Agregar el js con la lógica del focus out en `/app/assets/javascripts/admin/inputs/logger_input.js`

   ```
   $(document).ready(function(){
     $('.logger-input').each(function(i, el) {
       $(el).on( "focusout", function(event) {
         console.info(event.currentTarget.value);
       });
     });
   });
   ```
3. Incluir el js en `/app/assets/javascripts/active_admin.js`:

   ```
   //= require active_admin/base
   //= require admin/inputs/logger_input
   ```
4. Usar en el form:

   ```
   ActiveAdmin.register Blog do
     form do |f|
       f.inputs do
         f.input :title, as: :logger
       end

       f.actions
     end
   end
   ```

### Filtros custom

Para manejar la búsqueda de los filtros ActiveAdmin por debajo usa [Ransack](https://github.com/activerecord-hackery/ransack). Esta gema usa [distintos sufijos](https://github.com/activerecord-hackery/ransack#search-matchers) para indicar distintos tipos de búsqueda. Además, nos permite hacer búsquedas con respecto a [atributos de asociaciones](https://github.com/activerecord-hackery/ransack#associations).

Por ejemplo, si queremos buscar blogs por nombre de usuario:

```ruby
filter :user_name_cont
```

Aquí `_cont` indica que se entregarán los resultados que contengan el valor dado. Podríamos haber usado `_eq` si quisieramos un match perfecto, por ejemplo.

### Utilizando Vue

Muchas veces lo que se puede hacer con AA es un poco riguroso en cuanto a las necesidades del cliente, por lo que pueden haber situaciones en las cuales necesitemos incorporar Vue.js en AA. A continuación se explica una guía paso a paso de cómo agregar este en AA.

Para esto tenemos 2 opciones:

1. Hacer una vista custom como se mencionó anteriormente y ahí usar Vue.
2. Usar directamente un componente en la vista de admin o en un vista html.arb.

### Primer caso.

1. Se crea el componente vue que queremos utilizar, por ejemplo `massive-edit.vue`.
2. Luego debemos registrar el componente globalmente. Acá necesitamos usar un archivo diferente al `application.js` que utilizamos normalmente, para este caso usaremos un archivo llamado `admin_application.js` que se debe encontrar en la misma carpeta que el `application.js`, si no se encuentra debe crearlo, la convención para registrar los componentes que se usan en admin es de `snake_case`, siguiendo con el ejemplo anterior el archivo se vería así:

   ```javascript
   import Vue from 'vue/dist/vue.esm';
   import MassiveEdit from '../views/products/massive-edit.vue';

   Vue.component('massive_edit', MassiveEdit);

   document.addEventListener('DOMContentLoaded', () => {
     if (document.getElementById('wrapper') !== null) {
       return new Vue({
         el: '#wrapper',
       });
     }

     return null;
   });
   ```
3. Si queremos utilizar filtros, i18n o tailwind en los archivos Vue que utilizaremos en AA, debemos importarlos en este archivo también, tal como lo hacemos normalmente. Cabe destacar que si tenemos algo importado en `application.js` no va a funcionar en los componentes que se utilicen en AA, si no que debemos importarlos nuevamente en `admin_application.js`, acá un ejemplo del archivo completo:

   ```javascript
   import { camelizeKeys } from 'humps'; // filtro camelize
   import Vue from 'vue/dist/vue.esm';
   import MassiveEdit from '../views/products/massive-edit.vue';
   import i18n from '../plugins/i18n'; // i18n
   import '../css/application.css'; // tailwind

   Vue.component('massive_edit', MassiveEdit);
   Vue.filter('camelizeKeys', camelizeKeys);

   document.addEventListener('DOMContentLoaded', () => {
     if (document.getElementById('wrapper') !== null) {
       return new Vue({
         el: '#wrapper',
         i18n,
       });
     }

     return null;
   });
   ```
4. Luego podemos ir a cualquier vista dentro de `app/views/admin` y usar el componente como lo hacemos normalmente:

   ```html
    <massive_edit :props="@props.to_json"> </massive_edit>
   ```

### Segundo caso.

1. Hacer los mismos pasos anteriores.
2. Instalar la clase `vue_component` desde nuestra gema potassium. Esto agrega un par de inicializadores a nuestra app. Se debe escribir `potassium install` y escoger `vue_component`.
3. Luego en el archivo `initializers/active_admin.rb` debemos importar `vue_componment` y hacer build del componente registrado anteriormente:

   ```
       require "vue_component.rb"

       AUTO_BUILD_ELEMENTS = %i{
         massive_edit
       }

       component_creator(AUTO_BUILD_ELEMENTS)
   ```

   La función component\_creator recibe los nombres de los componentes y los convierte a html que puede procesar AA mediante la gema [Arbre](https://github.com/activeadmin/arbre). Lo anterior se debe poner fuera del `ActiveAdmin.setup`.
4. Utilizar el componente en la vista, puede ser de las siguientes formas:

   a. html.arb:

   ```ruby
   massive_edit(prop1: prop1, prop2: prop2)
   ```

   b. admin

   ```ruby
   index do
     massive_edit(prop1: prop1, prop2: prop2)
   end
   ```

## Recursos útiles

* [Active Admin Docs](https://activeadmin.info/documentation.html)
* [Active Admin Addons Docs](https://github.com/platanus/activeadmin_addons)


# Active Admin Addons

Es una gema que desarrollamos en Platanus y sirve para resolver algunas cuestiones que Active Admin no resuelve. Por ej:

* Selectores ajax.
* Manejo de imágenes.
* Manejo de booleanos.
* En fin, todo lo que [aquí](https://github.com/platanus/activeadmin_addons#what-you-get) aparece.

## ¿Por qué la usamos?

Para no tener que resolver a mano problemas comunes que sabemos tener en Active Admin.

## ¿Cómo la usamos?

### Instalación

Viene con [Potassium](https://github.com/platanus/potassium) pero se puede instalar manualmente siguiendo la instrucciones [aquí](https://github.com/platanus/activeadmin_addons#installation).

### Uso básico

Por suerte le hemos puesto mucho cariño al README de la gema por lo que mejor consultar [ahí](https://github.com/platanus/activeadmin_addons#what-you-get).

### Recursos útiles

* [Repositorio en Github](https://github.com/platanus/activeadmin_addons)
* [Presentación Platanus](https://www.youtube.com/watch?v=k0ewc_5DDHA) sobre addons


# Pundit

[Pundit](https://github.com/varvet/pundit) es la gema de Ruby que utilizamos para dar permisos de acceso en nuestras aplicaciones.

## ¿Por qué la usamos?

* Para encapsular la lógica de permisos en un solo lugar (las policies) y separarla de los recursos (controllers y modelos).
* Porque es simple de entender y configurar.
* Porque se integra bien con Active Admin.

## ¿Cómo la usamos?

### Instalación

Podemos instalar la gema con [potassium](https://github.com/platanus/potassium) al crear un proyecto o luego así:

![](/files/e0bjjsdbYt3t84E8xvxA)

### Uso básico

Autorizar un recurso con Pundit sigue este camino:

![](/files/RJ6jmIDMCZyigYk2xRH7)

**Punto 1**

Un usuario hace una request a un recurso. En este caso `User#1` intenta acceder a un blog post a través de la url `GET /blog_posts/2`

**Punto 2**

La solicitud llega a `BlogPostsController#show` action.

```ruby
class BlogPostsController < ApplicationController
  def show
    authorize blog_post
    respond_with blog_post
  end

  private

  def blog_post
    @blog_post ||= BlogPost.find(params[:id])
  end
end
```

La primera línea que se ejecuta es `authorize(blog_post)`. Este método `authorize` viene con Pundit y lo que hace es buscar dentro de `/app/policies` **una cuyo nombre contenga la clase del recurso que pasamos a `authorize` como primer parámetro + el sufijo Policy**. En este caso como el objeto `blog_post` es una instancia del modelo `BlogPost`, Pundit buscará la policy `BlogPostPolicy` en `/app/policies/blog_post_policy.rb`. Si no la encuentra, lanzará el error `Pundit::NotDefinedError` y deberás agregarla. Si la encuentra, Pundit **internamente** hará algo así:

```ruby
BlogPostPolicy.new(current_user, blog_post).show?
```

Donde:

1. `BlogPostPolicy` es la policy que se infiere a partir de la clase del recurso. En el ejemplo se infiere a partir de `BlogPost.find(3)`.
2. `current_user` es el usuario logueado. En el ejemplo: `User.find(1)`.
3. `blog_post` es el recurso sobre el que decidimos si el usuario puede acceder o no. En el ejemplo: `BlogPost.find(params[:id])` o `BlogPost.find(3)` que es lo mismo.
4. `show?` es un método en la policy que tiene la lógica de permisos para la acción `BlogPostsController#show`. Es importante tener en cuenta que esto también funciona por convención. Si la acción del controller es `show` en la policy Pundit buscará internamente **un método de instancia con el mismo nombre + ?** → `show?`

**Punto 3**

Como dijimos en el punto anterior, al encontrar la `BlogPostPolicy` se busca y ejecuta el método de instancia `show?`. Este método define la lógica de permisos para un recurso específico (una instancia del modelo `BlogPost`) y una acción de controller específica (`show` en este caso).

Entonces, si tenemos la policy:

```ruby
class BlogPostPolicy < ApplicationPolicy
  def show?
    user.blog_posts.where(id: record.id).any?
  end
end
```

y la siguiente información en la DB:

![](/files/fSAm9oAbNlHlBmswOtMD)

Veamos qué ocurre con los siguientes flujos:

* `User#1` intenta acceder a `/blog_posts/2`.

  En este caso:

  * `user`: es igual a `current_user` o lo que es igual `User.find(1)`
  * `blog_posts`: es una colección (de Active Record) de `BlogPost`s que contiene dos instancias. Una con id 1 y otra con id 2. Esto porque `user` es dueño de esos dos posts según la info que definimos más arriba.
  * `record`: es el recurso. En el ejemplo `BlogPost.find(2)`

  Con lo anterior podemos ver si `User#1` tiene acceso sobre `BlogPost#2` en la acción `BlogPostsController#show`

  ```ruby
  class BlogPostPolicy < ApplicationPolicy
    def show?
      user.blog_posts.where(id: record.id).any? #=> true
      # User.find(1).blog_posts.where(id: BlogPost.find(2).id).any? #=> true
      # User#1 que es dueño de los blog posts 1 y 2 contiene a 2? #=> true
    end
  end
  ```

  Como resulta ser que sí tiene permisos, el método `authorize` de `BlogPostsController#show` devuelve `true` y permite ejecutar `respond_wih blog_post` devolviendo la información requerida por `User#1`.
* `User#2` intenta acceder a `/blog_posts/3`.

  En este caso ocurre algo similar al ejemplo anterior porque `User#2` es dueño del `BlogPost#3`.
* `User#1` intenta acceder a `/blog_posts/3`.

  En este caso el código en `show?` evalúa `false` y Pundit lanza la exception `Pundit::NotAuthorizedError` que impide que `User#1` acceda al recurso `BlogPost#3`

  ```ruby
  class BlogPostPolicy < ApplicationPolicy
    def show?
      user.blog_posts.where(id: record.id).any? #=> false
      # User.find(1).blog_posts.where(id: BlogPost.find(3).id).any? #=> false
      # User#1 que es dueño de los blog posts 1 y 2 contiene a 3? #=> false
    end
  end
  ```

### Integración con Active Admin

Normalmente en nuestros proyectos Platanus tenemos dos dominios bien definidos:

1. La app.
2. El admin o back office.

En el primero normalmente el usuario logueado es una instancia de `User`. En cambio, en el segundo, es un `AdminUser`. Algo también común, no solo en los proyectos Platanus, es que los permisos sobre un mismo recurso (supongamos una instancia de `BlogPost`) son bien distintos en un dominio u otro. Por ej es frecuente que un usuario admin pueda acceder a recursos de otros usuarios pero este comportamiento es poco frecuente del lado de la app.

Con este nuevo panorama, modifiquemos la policy del ejemplo para autorizar un recurso en ambos dominios.

Habíamos dicho que para que `User#1` acceda a `/blog_posts/2` la policy se ve así:

```ruby
class BlogPostPolicy < ApplicationPolicy
  def show?
    user.blog_posts.where(id: record.id).any? #=> true
    # User.find(1).blog_posts.where(id: BlogPost.find(2).id).any? #=> true
    # User#1 que es dueño de los blog posts 1 y 2 contiene a 2? #=> true
  end
end
```

Ahora supongamos que `AdminUser#1` quiere acceder a `/admin/blog_posts/2` utilizando la misma policy.

```ruby
class BlogPostPolicy < ApplicationPolicy
  def show?
    user.blog_posts.where(id: record.id).any? #=> NoMethodError: undefined method `blog_posts'
    # AdminUser.find(1).blog_posts.where(id: BlogPost.find(2).id).any?
  end
end
```

Es hacer esto devuelve un error ya que la variable `user` de `show?` en vez de contener una instancia de `User` ahora contiene una de `AdminUser`. El problema con esto es que como `AdminUser` no tiene la relación `blog_posts` definida, el código falla. La forma de arreglar esto es agregar lógica de acceso entendiendo que la variable `user` a veces será una instancia de `User` (cuando se acceda desde el dominio de la app) pero otras de `AdminUser` (cuando se acceda desde back office). La policy corregida se ve así:

```ruby
class BlogPostPolicy < ApplicationPolicy
  def show?
    case user
    when User
      user.blog_posts.where(id: record.id).any?
    when AdminUser
      true
    else
      raise 'invalid user type'
    end
  end
end
```

Ahora el código funciona porque antes de evaluar el permiso estamos viendo si el recurso se está accediendo a través de un `User` o un `AdminUser`. Si bien esta solución es “aceptable”, mantener esta estrategia complicaría la lógica de las policies ya que todas arrastrarían el problema de “si es admin hacer x pero si es user hacer y”. Para evitar esto, en Platanus **usamos policies diferentes para la app y el admin.** El código con este cambio queda de la siguiente manera:

En `/app/policies/blog_post_policy.rb` seguimos poniendo, igual que antes, los permisos para el dominio de la app.

```ruby
class BlogPostPolicy < ApplicationPolicy
  def show?
    user.blog_posts.where(id: record.id).any?
  end
end
```

En cambio para el admin, agregamos una nueva policy para el mismo recurso (`BlogPost`) dentro de `app/policies/back_office/blog_post_policy.rb`

```ruby
class BackOffice::BlogPostPolicy < BackOffice::DefaultPolicy
  def show?
    true
  end
end
```

De esta manera se simplifica la lógica ya que del lado de la app `user` siempre será una instancia de `User` en cambio en el admin, siempre será `AdminUser`.

### Pundit Admin Adapter

Como mencioné anteriormente, una de las ventajas de usar Pundit es que [se integra bien con Active Admin](https://activeadmin.info/13-authorization-adapter.html) que es el framework de administración que usamos en Platanus.

En la práctica, esto significa que para autorizar un recurso en la back office solo se debe definir la policy y Active Admin hará el resto. Por ej: si tengo el recurso `Team` y la policy:

```ruby
class BackOffice::TeamPolicy < BackOffice::DefaultPolicy
  def show?
    false
  end
end
```

Active Admin esconderá automáticamente la opción “Ver” de ese recurso y la ruta `/admin/teams/:id` no existirá.

![](/files/aKD51IfQ5g9rd9JHxOhG)

Algo importante a tener en cuenta es que al trabajar con [acciones custom](https://activeadmin.info/8-custom-actions.html) (aquellas definidas por `member_action` o `collection_action`) Active Admin manejará la visibilidad de la ruta pero los links habrá que manejarlos manualmente. Por ej si tengo:

```ruby
ActiveAdmin.register Team do
  member_action :import_form, method: :get do
    render("admin/import_form", locals: { team: resource })
  end

  action_item :import_form, only: [:show] do
    link_to "Ir al import form", import_form_admin_team_path(resource)
  end
end
```

y la policy:

```ruby
class BackOffice::TeamPolicy < BackOffice::DefaultPolicy
  def import_form?
    false
  end
end
```

la ruta `/admin/teams/3/import_form` no existirá pero el link en el header sí:

![](/files/OdefcPMMlogFXpcboREc)

Para corregir esto, se debe agregar una condición al `action_item` así:

```ruby
action_item :import_form, only: [:show], if: proc { authorized?(:import_form, resource) } do
  link_to "Ir al import form", import_form_admin_team_path(resource)
end
```

De esta manera, solo se muestra el link si `authorized?(:import_form, resource)` evalúa `true`. Como ya se imaginarán, `authorized?` es un método de Active Admin que conecta con Pundit y permite preguntar para una acción específica (en este caso `:import_form` definida por la `member_action`) si el admin logueado puede acceder a `resource` (en este caso una instancia de `Team` → `resource = Team.find(3)`) o no.

### Default Policy

Para controlar el acceso \*\*por default \*\*a los recursos es que agregamos una clase base a las policies. Entonces, las de la app heredarán de `ApplicationPolicy` → `/app/policies/application_policy.rb` y las de admin de `BackOffice::DefaultPolicy` → `/app/policies/back_office/default_policy.rb`.

Por ej, si tengo la policy:

```ruby
class BackOffice::BlogPostPolicy < BackOffice::DefaultPolicy
end
```

con la siguiente clase base:

```ruby
class BackOffice::DefaultPolicy
  # ...

  def index?
    false
  end

  def show?
    false
  end

  def create?
    false
  end

  def new?
    create?
  end

  def update?
    false
  end

  def edit?
    update?
  end

  def destroy?
    false
  end

  # ...
end
```

si como un admin user intento acceder a `/admin/blog_posts/3`, no podré hacerlo porque al no tener `BackOffice::BlogPostPolicy` definida la acción `show?` se accederá \*\*por defecto \*\*al método `:show?` definido en `BackOffice::DefaultPolicy`, evaluará `false` y no me dejará acceder a la vista.

> ℹ️ Dependiendo del tipo de aplicación y lo sensible que sea el tema seguridad en la misma, nos convendrá o no hacer que las clases base sean más o menos restrictivas.

### Preguntas frecuentes

**¿Cómo manejar un admin que puede ver todo y otro que tiene acceso restringido?**

Supongamos que tenemos el modelo `BlogPost` que tiene un atributo `deleted_at` que se llena cuando alguien borra el blog. Supongamos además que queremos que desde la back office, un `AdminUser` con rol “super admin” pueda ver absolutamente todos los blogs pero uno con rol “supervisor” solo pueda ver aquellos que no fueron borrados. Para lograr esto haremos lo siguiente:

1. Agregar scopes a `BlogPost` para poder obtener fácilmente recursos borrados.

   ```ruby
   class BlogPost < ApplicationRecord
     scope :published, -> { where(deleted_at: nil) }
   end
   ```
2. Definir un atributo rol en el modelo `AdminUser` así:

   ```ruby
   class AdminUser < ApplicationRecord
     enum role: { supervisor: 0, super_admin: 1 }, _default: 'supervisor', _prefix: true
   end
   ```
3. Modificar la policy para definir las reglas de acceso para cada rol:

   ```ruby
   class BackOffice::BlogPostPolicy < BackOffice::DefaultPolicy
     def show?
       scope = admin_user.role_supervisor? ? :published : :all
       BlogPost.send(scope).where(id: record.id).any?
     end
   end
   ```

   En el código anterior se puede ver que se buscará el blog post (`record`) específico en la colección completa en el caso de que el `AdminUser` logueado (`admin_user`) sea un `super_admin` o en la colección filtrada (solo aquellos que no fueron borrados) en el caso de que sea `supervisor`.

**¿Cómo testear una policy con RSpec?**

Tomemos como ejemplo la policy de la pregunta anterior:

```ruby
class BackOffice::BlogPostPolicy < BackOffice::DefaultPolicy
  def show?
    scope = admin_user.role_supervisor? ? :published : :all
    BlogPost.send(scope).where(id: record.id).any?
  end
end
```

Pasos a seguir:

1. Agregar `require "pundit/rspec"` al archivo `spec/rails_helper.rb` para tener los helpers de RSpec específicos de Pundit si es que no viene ya con Potassium.
2. Agregar la policy en `spec/policies/back_office/blog_post_policy_spec.rb`
3. Definir en `let`s todo aquellos que puede variar:

   1. `admin_user`
   2. `record`

   ```ruby
   describe BackOffice::BlogPostPolicy do
     subject { described_class }

     let(:role) { "super_admin" }
     let(:admin_user) { create(:admin_user, role: role) }
     let(:deleted_at) { nil }
     let(:record) { create(:block_post, deleted_at: deleted_at) }

     # ...
   end
   ```
4. Probar los permisos sobre una acción variando los `let`s.

   ```ruby
   describe BackOffice::BlogPostPolicy do
     subject { described_class }

     let(:role) { "super_admin" }
     let(:admin_user) { create(:admin_user, role: role) }
     let(:deleted_at) { nil }
     let(:record) { create(:block_post, deleted_at: deleted_at) }

     permissions :show? do
       context "with super_admin role" do
         let(:role) { "super_admin" }

         it { expect(subject).to permit(admin_user, record) }

         context "with deleted record" do
           let(:deleted_at) { DateTime.current }

           it { expect(subject).to permit(admin_user, record) }
         end
       end

       context "with supervisor role" do
         let(:role) { "supervisor" }

         it { expect(subject).to permit(admin_user, record) }

         context "with deleted record" do
           let(:deleted_at) { DateTime.current }

           it { expect(subject).not_to permit(admin_user, record) }
         end
       end
     end
   end
   ```

**¿Qué significa el error `Pundit::NotDefinedError`?**

![](/files/olTfsehjlSGsIgCxW85B)

Significa que nos falta agregar la policy para el recurso que acabamos de agregar. Se arregla agregando la policy y definiendo permisos para todas las acciones (que pueden ser las que vienen por default de `BackOffice::DefaultPolicy`)

```ruby
class BackOffice::TeamMemberPolicy < BackOffice::DefaultPolicy
end
```

**¿Por qué en active admin no puedo acceder a un recurso?**

![](/files/GP0H6lab4PdyiHTLJCWd)

Posiblemente porque el permiso está evaluando `false`. Por ej si vemos el error al entrar a `/admin/team_members/666` debemos revisar en `BackOffice::TeamMemberPolicy` si el permiso en `show?` está evaluando `true` o `false`. Si resulta que la acción no está definida en esa policy habrá que revisar `BackOffice::DefaultPolicy`

## Recursos útiles

* [Github de Pundit](https://github.com/varvet/pundit)
* [Receta](https://github.com/platanus/potassium/blob/master/lib/potassium/recipes/pundit.rb) de potassium (puede ser útil para ver qué se instala)


# Shrine

[General](/stack/ruby_rails/shrine/general)

[Manejo y procesamiento de imágenes](/stack/ruby_rails/shrine/manejo_y_procesamiento_de_imagenes)


# General

[Shrine](https://github.com/shrinerb/shrine) es una gema para manejar de manera simple la tarea de subir y adjuntar archivos.

## ¿Por qué la usamos?

Anteriormente usábamos Paperclip, hasta que fue deprecada en favor de la solución incluida en Rails, ActiveStorage. Dado esto, nos cambiamos a ActiveStorage (AS) para no quedarnos con una herramienta sin soporte.

Sin embargo, hay algunos detalles que se manejan mejor en Shrine, por ejemplo:

* Shrine tiene un diseño modular, utilizando `plugins` para las distintas funcionalidades que ofrece. Esto permite cargar solo las que en verdad se usen
* Shrine promueve separación de responsabilidades, introduciendo el concepto de `Uploader`. Estos se encargan de la lógica de subida de un tipo de archivo en particular
* AS no tiene validaciones, por ejemplo de tamaño o tipo de archivo. En Shrine se pueden agregar fácilmente usando el plugin [validation\_helpers](https://shrinerb.com/docs/plugins/validation_helpers)
* Recién en Rails 6.1 se está dando soporte a acceso público de archivos, con Shrine se puede definir a nivel de configuración o por `Uploader`
* Con Shrine se pueden setear `default_url` por Uploader, para cuando el archivo es `nil`

## ¿Cómo la usamos?

### Instalación

La gema viene instalada si el proyecto se generó usando [Potassium](https://github.com/platanus/potassium) con la opción `Shrine` elegida como *storage*. También se incluyen un par de uploaders que sirven como base. Si el proyecto se generó sin una opción de storage, se puede agregar corriendo `potassium install file_storage` y seleccionando `Shrine`.

### Ejemplo básico

> Para estos ejemplos se utilizó la versión 3.2.1 de Shrine

Supongamos que tenemos un `Uploader` de imágenes genérico, que podría verse así:

```ruby
class ImageUploader < Shrine
  plugin :validation_helpers

  Attacher.validate do
    validate_mime_type %w[image/jpeg image/jpg image/png image/svg+xml image/gif]
  end
end
```

Aquí tenemos el plugin [validation\_helpers](https://shrinerb.com/docs/plugins/validation_helpers), que nos permite usar `validate_mime_type` para verificar que el tipo del archivo corresponda efectivamente a una imagen.

Con esto ya podemos empezar a incluir imágenes en los modelos. Si queremos un `attachment` llamado `photo` habría que agregar la columna `photo_data` a la tabla (tipo `text` o `jsonb`) y agregar lo siguiente en el modelo:

```ruby
include ImageUploader::Attachment(:photo)
```

> El nombre de la columna siempre debe incluir el sufijo \_data

### Ejemplo: heredando de un uploader

Ahora, imaginemos que se quiere agregar una imagen de perfil para los usuarios. Podríamos usar el `ImageUploader` definido antes, pero se quieren un par de cosas extra para esta `profile_picture`:

* Límite de peso, 5 MB. Pueden haber muchas imágenes de perfil distintas en una misma vista y no queremos que quede muy pesada
* El usuario puede elegir no tener una foto de perfil
* Comúnmente se usará la imagen en dos tamaños

Como esta sigue siendo una imagen y queremos mantener la validación definida en `ImageUploader`, vamos a definir un nuevo uploader que herede de este:

```ruby
class ProfilePictureUploader < ImageUploader
end
```

**Límite de peso**

Para esto recurrimos nuevamente al `validations_helper`, esta vez usando `validate_max_size`. Para mantener las validaciones de la clase padre, [se debe llamar a ](https://shrinerb.com/docs/plugins/validation#inheritance)[`super()`](https://shrinerb.com/docs/plugins/validation#inheritance), quedando así:

```ruby
Attacher.validate do
  super()
  validate_max_size 5*1024*1024
end
```

**Sin foto de perfil**

En estos casos queremos poner una imagen por defecto, que indique claramente la ausencia de la foto de perfil. Digamos que tenemos una imagen para este propósito en `/app/assets/images/no-profile-picture.png`. Podemos usar el [plugin ](https://shrinerb.com/docs/plugins/default_url)[`default_url`](https://shrinerb.com/docs/plugins/default_url) para usarla siempre que no haya una imagen de perfil:

```ruby
plugin :default_url

Attacher.default_url do |**options|
  ActionController::Base.helpers.image_url('no-profile-picture.png')
end
```

`ActionController::Base.helpers.image_url` nos ayuda a obtener la url final de uno de los assets del proyecto.

Con esto, todo usuario cuya `profile_picture` sea `nil` retornará la url del asset al hacer `user.profile_picture_url`.

**Distintos tamaños**

Vamos a procesar la imagen para tener dos tamaños aparte del original: `small` y `medium`. Shrine tiene dos opciones para realizar el procesamiento: dinámicamente [cuando se pide](https://shrinerb.com/docs/plugins/derivation_endpoint) la transformación (on-the-fly) o [con anterioridad](https://shrinerb.com/docs/plugins/derivatives), guardando el resultado. Para este ejemplo usaremos la segunda opción.

Como prerequisito necesitamos agregar la gema [image\_processing](https://github.com/janko/image_processing). Luego podemos definir las `derivatives` para los tamaños `small` y `medium`:

```ruby
require "image_processing/vips"

...

plugin :derivatives

Attacher.derivatives do |original|
  vips = ImageProcessing::Vips.source(original)

  {
    small:  vips.resize_to_limit!(300, 300),
    medium: vips.resize_to_limit!(500, 500),
  }
end
```

Después en el controlador se debe gatillar la creación de estas derivadas:

```ruby
user = User.new(..., profile_picture: file)

if user.valid?
  user.profile_picture_derivatives! if user.profile_picture_changed?
  user.save
end
```

Y para acceder a la url de una de las derivadas:

```ruby
User.last.profile_picture_url(:small)
```

**Resultado**

Con todo esto, nuestro `Uploader` que hereda de `ImageUploader` quedaría así:

```ruby
require "image_processing/vips"

class ProfilePictureUploader < ImageUploader
  plugin :default_url
  plugin :derivatives

  Attacher.validate do
    super()
    validate_max_size 5*1024*1024
  end

  Attacher.default_url do |**options|
    ActionController::Base.helpers.image_url('no-profile-picture.png')
  end

  Attacher.derivatives do |original|
    vips = ImageProcessing::Vips.source(original)

    {
      small:  vips.resize_to_limit!(300, 300),
      medium: vips.resize_to_limit!(500, 500)
    }
  end
end
```

Y para agregar el attachment al modelo de usuario:

```ruby
include ProfilePictureUploader::Attachment(:profile_picture)
```

### Recursos útiles

* [Documentación oficial](https://shrinerb.com/): muy buena, con guías y secciones explicando los distintos plugins
* [Repo en Github](https://github.com/shrinerb/shrine)
* [Direct S3 Upload](https://github.com/shrinerb/shrine/wiki/Adding-Direct-S3-Uploads) / [Direct App Upload](https://github.com/shrinerb/shrine/wiki/Adding-Direct-App-Uploads): ambos sirven para subir un archivo antes de que se le haga submit a un form. La diferencia radica en que el de S3 lo sube directamente a AWS, mientras que el otro lo sube a un `upload_endpoint` de la aplicación. Para ambientes que no tienen un bucket S3 (como `development`, en que se guardan los archivos en el filesystem) habría que usar Direct App Upload
* [Demo Direct Upload + Vue](https://drive.google.com/file/d/1fwrZ1tLZa_xeSp2j57iKFgjNlgDxXAM9/view?usp=sharing): presentación al equipo de Platanus para introducir Shrine. Se arma un componente Vue para manejar el Direct Upload que puede ser usado dentro de un form de Rails
* [Testing](https://shrinerb.com/docs/testing): en la sección de [Test data](https://shrinerb.com/docs/testing#test-data) dan un ejemplo de un helper que puede ser usado en las factories. En la sección de [Acceptance tests](https://shrinerb.com/docs/testing#acceptance-tests) dan un ejemplo de como agregar un archivo como parámetro en tests de controladores usando `Rack::Test::UploadedFile`

### Recursos útiles para plataneros

* [Implementación Direct Upload](https://github.com/platanus/gret/pull/22): PR implementando Direct Upload para ser usado en ActiveAdmin


# Manejo y procesamiento de imágenes

## Motivación

Consideremos una vista con imágenes subidas por usuarios, donde tenemos menos control sobre lo que suben. Si las mostramos tal cuál fueron subidas con un `<img>` y nada más, podríamos tener una carga no muy atractiva, algo así:

[Ver video](https://github.com/platanus/la-guia/blob/master/stack/ruby_rails/shrine/assets/manejo-y-procesamiento-de-imagenes-1.qt)

Hay varios problemas con esto:

* Mientras las imágenes no se han cargado se ve un espacio vacío
* Cuando se van cargando hay un momento que se ven a la mitad
* Se demoran harto en cargar todas

En esta sección de la guía vamos a ver algunas cosas que se pueden hacer con Shrine para que potencialmente la carga sea más amigable:

[Ver video](https://github.com/platanus/la-guia/blob/master/stack/ruby_rails/shrine/assets/manejo-y-procesamiento-de-imagenes-2.qt)

Las cosas que veremos acá, también [fueron implementadas en Potassium](https://github.com/platanus/potassium/pull/398), ahí en el PR puedes ver más detalles también.

## [Blurhash](https://blurha.sh/)

Del video anterior, probablemente lo que más llama la atención son esas versiones borrosas de las imágenes que aparecen antes de que las imágenes mismas se muestren. Eso se logra con Blurhash: una herramienta que permite representar una imagen como un string que luego se decodifica en el frontend y se obtiene algo que se pueden pintar en un `<canvas>` rápidamente.

![](/files/g0dYPGZyHp5x9aTpVdf2)

Con esto se evita el espacio en blanco y la carga por partes que se ven en el primer video.

Para usar blurhash con Shrine, necesitamos procesar la imagen para obtener el string y guardarlo en la metadata del archivo. Usaremos el [plugin add\_metadata](https://shrinerb.com/docs/plugins/add_metadata) para agregar la metadata. Para procesar la imagen, usaremos [ruby-vips](https://github.com/libvips/ruby-vips) un procesador de imágenes alternativo a ImageMagick que ha mostrado tener mejor performance y menor uso de memoria. Esto se ve así:

```ruby
class ImageUploader < Shrine
  add_metadata :blurhash do |io, derivative: nil, **|
    if derivative.nil?
      Shrine.with_file(io) do |file|
        image = Vips::Image.new_from_file(file.path, access: :sequential)

        # Transformamos el archivo a un tamaño más pequeño para acelerar el procesamiento
        image = image.resize(100.0 / image.width)

        # image.to_a entrega un arreglo con los pixeles en formato rgba, Blurhash requiere solo rgb
        flat_rgb_pixels = []
        image.to_a.each do |row|
          row.each { |pixel| flat_rgb_pixels.concat(pixel[0..2])  }
        end

        # Finalmente obtenemos el string, que se guarda en file.metadata['blurhash']
        Blurhash.encode(image.width, image.height, flat_rgb_pixels)
      end
    end
  end
end
```

Con esto ya tenemos un código que podemos usar en el front para renderear un placeholder mientras se carga la imagen.

## Derivatives

Hasta ahora podemos evitar que nuestra vista tenga espacios vacíos en un principio y que luego las imágenes se empiecen a cargar por partes, pero todavía nos gustaría que el usuario pueda verlas lo más rápido posible. Para esto usaremos derivatives para disminuir el peso de las imágenes, de dos maneras:

1. Tener distintos tamaños. Así el front puede usar el tamaño que más se adecúe al uso que se le dará en una vista y no tiene que cargar una imagen de mayor peso. Esto ya lo vimos en [los ejemplos de Shrine](https://www.notion.so/platanus/general.md#ejemplo-heredando-de-un-uploader)
2. Tener imágenes en formato `webp`, un formato más liviano que `png` o `jpg` con muy poca perdida de calidad

Para esto usaremos nuevamente `vips`. Si quisieramos tener variaciones de tamaño pequeño, tanto en el formato original como en formato `webp`, tendríamos que hacer lo siguiente:

```ruby
class ImageUploader < Shrine
  Attacher.derivatives do |original|
    vips = ImageProcessing::Vips.source(original)
    {
      sm: vips.resize_to_limit!(426, 240),
      webp_sm: vips.convert('webp').resize_to_limit!(426, 240),
    }
  end
end
```

## ¿Cómo se ve esto en Potassium?

En Potassium agregamos algunos archivos y configuraciones para facilitar el uso de estas cosas. La idea es que sirva como un ejemplo y base que se pueda customizar según sea necesario.

## Backgrounding

El procesamiento de una imagen para sacar sus derivatives puede ser costoso en recursos y tiempo. Por esto, usamos el plugin de `backgrounding` para que este procesamiento [se encole en un job](https://github.com/platanus/potassium/blob/master/lib/potassium/assets/config/shrine.rb#L43).

Hacer esto resulta en que hay un momento entre que se sube una imagen y que esta se procesa, en que las derivatives no existen. Por lo tanto, si se pidiera una url para ellas en una vista, habría un problema. Para evitar esto, usamos una combinación de dos plugins para tener un fallback:

1. [Derivation Endpoint](https://shrinerb.com/docs/plugins/derivation_endpoint): nos permite generar una url on-the-fly con una variación redimensionada de la imagen según parámetros de `height` y `width`. [Usado acá](https://github.com/platanus/potassium/blob/master/lib/potassium/assets/app/uploaders/cover_image_uploader.rb#L28)
2. [Default Url](https://shrinerb.com/docs/plugins/default_url): Nos permite definir una url por defecto cuando el archivo solicitado no existe. [En nuestro caso](https://github.com/platanus/potassium/blob/master/lib/potassium/assets/app/uploaders/cover_image_uploader.rb#L34), lo usamos para entregar una url generada con el derivation endpoint anterior cuando se pide una derivative

## [CoverImageUploader](https://github.com/platanus/potassium/blob/master/lib/potassium/assets/app/uploaders/cover_image_uploader.rb)

Uploader de ejemplo que incluye todo lo que ya habíamos mencionado:

* Inclusión de blurhash a la metadata
* Derivatives de 3 tamaños distintos, cada uno en `jpg` y en `webp`
* Url default para derivatives faltantes usando la derivation url

## [ImageHandlingAttributes](https://github.com/platanus/potassium/blob/master/lib/potassium/assets/app/serializers/concerns/image_handling_attributes.rb)

Concern de serializer incluido en el `BaseSerializer`. Agrega un método `add_image_handling_attributes` que permite a cualquier serializador agregar `attributes` para el blurhash y para las urls de las derivatives de una imagen.

Dado un attachment de nombre `image` que usa el `CoverImageUploader`, se puede usar así:

```ruby
add_image_handling_attributes(
  attachment_name: :image,
  derivatives: CoverImageUploader::DERIVATIVES.keys,
  include_original_image: true
)
```

Se usa la constante `CoverImageUploader::DERIVATIVES` para indicar que quiero agregar al objeto serializado todas las derivatives.

Si se tuviera solo una derivative `sm`, esto agregaría algo así al json resultante:

```ruby
{
  # ...
  image_blurhash: 'LUG%rif*rwayI:jZ#qju0~azS_oe',
  image: {
    sm: { url: 'someurl.com/bla' },
    original: { url: 'someurl.com/ble' }
  }
}
```

## [ImageHandlingUtilities](https://github.com/platanus/potassium/blob/master/lib/potassium/assets/config/initializers/shrine/plugins/image_handling_utilities.rb)

Plugin custom de Shrine que se usa en el `CoverImageUploader`. Agrega algunos métodos de instancia y de clase al modelo que use ese uploader. Para un attachment de nombre `image`, estos son:

* `image_blurhash`: retorna el blurhash sacado de la metadata
* `generate_image_derivatives`: genera todas las derivatives definidas en el Uploader. Si el archivo ya tenía algunas derivatives definidas, estas serán reemplazadas.

  Puede ser útil si se agregan derivatives a un uploader que no tenía, o si se cambia la definición de estas en uno que sí tenía. En estos casos se tendría que reprocesar los archivos ya existentes, por ejemplo, llamando a este método en un job.

  Método de clase asociado: `generate_all_image_derivatives`
* `generate_image_metadata`: genera toda la metadata del attachment.

  Útil si se agrega metadata nueva que se necesita en records existentes.

  Método de clase asociado: `generate_all_image_metadata`
* `generate_image_derivatives_and_metadata`: lo mismo que ambos métodos anteriores juntos, pero la gracia es que se preocupa de abrir el archivo solo una vez para procesar derivatives y metadata.

  Método de clase asociado: `generate_all_image_derivatives_and_metadata`

## Frontend

Hasta ahora hemos visto cómo usar Shrine para generar estas cosas que nos pueden ayudar a mejorar nuestra experiencia de carga de imágenes. Lo que sigue es cómo usar esto en nuestras vistas. Esta parte no viene en Potassium, pero acá se explicará que se necesitaría para implementarlo con Vue 3.

## Renderear blurhash

Para mostrar la imagen borrosa asociada al blurhash, necesitamos un componenete que maneje la decodificación del código y el pintado de los pixeles resultantes en un canvas. Para esto necesitamos agregar el paquete de blurhash:

```bash
yarn add blurhash
```

Luego, el componente se vería así:

```javascript
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { decode } from 'blurhash';

interface Props {
  blurhash: string,
}
const props = defineProps<Props>();

const INITIAL_CANVAS_SIZE = 32;

const canvas = ref<HTMLCanvasElement | null>(null);

onMounted(() => {
  const pixels = decode(props.blurhash, INITIAL_CANVAS_SIZE, INITIAL_CANVAS_SIZE);
  const imageData = new ImageData(pixels, INITIAL_CANVAS_SIZE, INITIAL_CANVAS_SIZE);
  const context = canvas.value?.getContext('2d');
  context?.putImageData(imageData, 0, 0);
});
</script>

<template>
  <canvas
    ref="canvas"
    :width="INITIAL_CANVAS_SIZE"
    :height="INITIAL_CANVAS_SIZE"
  />
</template>
```

> 💡 Notar que estamos seteando un tamaño fijo de 32px para el canvas. Esto es porque se necesita un tamaño explícito para decodificar el blurhash y renderearlo en el canvas, pero de todas maneras después se puede cambiar su tamaño con css. Por otro lado, usar un canvas pequeño ayuda con el performance. [Esto es lo que recomienda blurhash](https://github.com/woltapp/blurhash#how-fast-is-encoding-decoding)

## Transición de blurhash a imagen

Para determinar cuando pasar del canvas con el blurhash a la imagen, necesitamos un componente que renderee ambos en el mismo espacio, pero mostrando solo el blurhash. Luego, cuando la imagen esté totalmente cargada, intercambie la opacidad de ambos elementos. Para determinar cuándo hacer esto, usamos el evento `@load` de `<img>`. También necesitamos considerar el formato webp. Tiene buena compatibilidad con browsers modernos, pero para mayor cobertura usaremos un `<picture>` que permita usar el formato webp solo si el browser lo permite. En el caso contrario, se usaría como fallback un src `jpg`:

```javascript
<script setup lang="ts">
import { ref } from '@vue/reactivity';
import BlurhashCanvas from './blurhash-canvas.vue';

interface Props {
  src: string,
  webpSrc?: string,
  blurhash?: string,
}
withDefaults(defineProps<Props>(), {
  webpSrc: undefined,
  blurhash: undefined,
});

const isLoaded = ref(false);
</script>

<template>
  <div
    class="relative"
  >
    <blurhash-canvas
      v-if="blurhash"
      :blurhash="blurhash"
      class="absolute w-full h-full transition-opacity duration-500"
      :class="isLoaded ? 'opacity-0' : 'opacity-100'"
    />
    <picture>
      <source
        ref="webpSource"
        type="image/webp"
        :srcset="webpSrc"
      >
      <source
        ref="jpegSource"
        type="image/jpeg"
        :srcset="src"
      >
      <img
        ref="image"
        class="w-full h-full transition-opacity duration-500"
        :class="isLoaded ? 'opacity-100' : 'opacity-0'"
        v-bind="$attrs"
        :src="src"
        loading="lazy"
        @load="isLoaded = true"
      >
    </picture>
  </div>
</template>
```

> 💡 El `loading="lazy"` es un atributo del `<img>` que le dice al browser que solo cargue la imagen si es actualmente visible, o está cerca de serlo. [Acá más información sobre lazy loading](https://web.dev/browser-level-image-lazy-loading/)

Con esto ya podemos usar este componente para renderear nuestras imágenes de una manera más amigable.


# Pry

[Pry](https://github.com/pry/pry) es una consola para ruby que apunta a ser un reemplazo a IRB. En particular ofrece una mejor experiencia añadiendo funcionalidades que no están presentes en la consola built-in, como syntax highlighting, navegación en clases y debugging.

## ¿Por qué lo usamos?

La consola es más cómoda que IRB y tiene ciertos añadidos que mejoran la experiencia. Además pry nos permite introducir breakpoints en el código sin esfuerzo. Esto sirve para debuggear fácilmente en cualquier sector del código, ya sean controladores, servicios, comandos, modelos, inicializadores, etc. El debugging es muy importante en el proceso de desarrollar aplicaciones y ahorra muchísimo tiempo.

Por otro lado, la integración de pry con los proyectos es transparente y sin complicaciones, por lo que esta herramienta es ideal para ser incluida.

## ¿Cómo lo usamos?

Pry viene incluido por defecto en Potassium, por lo que los proyectos generados incluyen un archivo de configuración `.pryrc` que, entre otras cosas, agrega alias para ciertas funcionalidades. En proyectos que no lo tengan, se puede instalar usando `potassium install pry [--force]` con el cuidado de revisar los cambios en git.

Al ejecutar `bundle exec rails c` ya se está usando pry, sin embargo, una forma que entrega mucho valor a la hora de debuggear es introduciendo breakpoints en el código. Para hacerlo, en cualquier parte del código se puede introducir el siguiente statement: `binding.pry`. Con eso, cuando corra el servidor y la aplicación llegue a ese punto, la ejecución se verá pausada y en la consola se podrá explorar y acceder al scope y variables que tiene la aplicación en ese momento.

## Ejemplo

Supongamos que queremos debuggear el método show del siguiente controlador:

```ruby
class CarsController < Api::V1::BaseController
  def show
    respond_with car
  end

  private

  def car
    @car ||= Car.find(params.require(:id))
  end
end
```

Para hacerlo debemos agregar un breakpoint, de forma que el método show quede así:

```ruby
def show
  binding.pry
  respond_with car
end
```

Al ir a esa ruta, en este caso un `GET` al endpoint `/api/v1/cars/<id>`, la ejecución se detendrá cuando llegue a esa línea y en consola donde esté corriendo el servidor, se verá lo siguiente:

![](/files/PJjgBuxNqFa9Lxtd954r)

Una vez ahí, hay acceso a todo el scope que tiene ese método, y pry nos permite, entre otras cosas, lo siguiente:

* Explorar los métodos y atributos disponibles en el contexto ejecutando `ls`, en este caso todo lo presente dentro del método del controlador.
* Ver los valores de las variables a las que se puede acceder. En este caso podremos por ejemplo explorar el valor de `params` y las llaves que contiene.
* Avanzar por línea con `next` o el alias `n`.
* Avanzar por llamado a función con `step` o el alias `s`.
* Salir del breakpoint con `continue` o `c` y seguir en la ejecución.

Acá un ejemplo de debugging completo:

![](/files/YzfDSBfr9AxDuXnKB5kf)

Se puede ver que pry muestra no solo la línea donde se detuvo la ejecución, sino que también lo que la rodea, y el nombre del scope donde está parado. En este caso se ve que en primera instancia se detuvo en la línea 9, dentro del controlador señalado como `#<Api::V1::CarsController>`.

Como nota adicional, es importante que estos breakpoints no deben aparecer nunca en código que se va a subir y publicar, se deben mantener solo de manera temporal para development. De todas maneras contamos con la ayuda de linters configurados para que aleguen cuando se nos esté pasando una de esas líneas a un commit.

## Recursos Útiles

* [Pry cheat sheet](https://gist.github.com/lfender6445/9919357): [@lfender6445](https://gist.github.com/lfender6445) tiene una guía rápida con algunos de los comandos útiles que provee pry.
* [Repositorio](https://github.com/pry/pry): tiene documentación, guía de instalación y una [wiki](https://github.com/pry/pry/wiki) bastante completa, sin embargo cubre casos de uso mucho más profundos/complejos. Los ejemplos que ya se incluyen en esta guía cubren la mayor parte de los casos de uso comunes.


# Strong Migrations

[Strong Migrations](https://github.com/ankane/strong_migrations) es una gema para evitar acciones inseguras a la hora de aplicar migraciones de schema en las bases de datos.

## ¿Por qué la usamos?

Muchas veces al crear migraciones en Active Record, se pueden generar errores inesperados o incluso *downtime* de las aplicaciones dado que pueden generar locks en tablas enteras. Strong migrations ayuda a detectar ese tipo de potenciales errores de manera temprana y ofrece alternativas para realizar los mismos cambios de manera segura.

## ¿Cómo la usamos?

La gema strong migrations viene incluida por defecto en los proyectos generados con Potassium que cuenten con base de datos. En proyectos que no lo tengan, basta con agregar la gema al `Gemfile` y correr el generador `rails generate strong_migrations:install`, como se señala [aquí](https://github.com/ankane/strong_migrations#installation). Alternativamente se puede usar potassium para agregar una base de datos al proyecto con `potassium install db [--force]`, que a su vez incluirá la gema.

Al correr cualquier migración, strong migrations revisará si la migración es segura de acuerdo a los lineamientos propuestos en la gema. En caso de no serlo arroja una excepción, de modo que las migraciones inseguras no se puedan ejecutar correctamente. Dicho error además incluye las instrucciones para realizar la migración de la manera recomendada y segura.

## Casos típicos

Existen una serie de migraciones que son potencialmente peligrosas detalladas en la documentación de la gema, junto con sugerencias de mejores prácticas y verificaciones particulares para el caso de postgres.

Algunas operaciones peligrosas que es muy común realizar de manera insegura son las siguientes:

* [Eliminar una columna](https://github.com/ankane/strong_migrations#removing-a-column)
* [Agregar una columna con un valor por defecto](https://github.com/ankane/strong_migrations#adding-a-column-with-a-default-value)
* [Modificar una columna para que sea NOT NULL](https://github.com/ankane/strong_migrations#setting-not-null-on-an-existing-column)
* [Añadir una referencia a un modelo (foreign key)](https://github.com/ankane/strong_migrations#adding-a-reference)

## Proyectos chicos

En algunos casos las verificaciones que realiza esta gema pueden ser innecesarias si el proyecto está en una etapa temprana o es muy acotado, debido a que ciertos problemas solo surgen cuando el tráfico de las tablas es grande y el tamaño también. Lo anterior también aplica para proyectos más avanzados pero que deseen modificar tablas de la base de datos que no cuenten con información, o que presentan un uso muy limitado.

Si efectivamente es el caso y se cree que los riesgos de correr dichas migraciones son bajos o nulos, la gema ofrece la opción de realizar la migración de todas maneras. Por ejemplo si se desea renombrar una columna, strong migrations alerta sobre los riesgos, pero se puede marcar la operación como segura de la siguiente forma:

```ruby
class RenameSomeColumn < ActiveRecord::Migration[6.0]
  def change
    safety_assured { rename_column :users, :some_column, :new_name }
  end
end
```

Sin embargo, es bueno tener estas prácticas incorporadas en los equipos de desarrollo, para cuando la aplicación efectivamente se enfrente a escalas donde sea importante seguir las recomendaciones.

### Recursos útiles

* [Repositorio](https://github.com/ankane/strong_migrations): tiene la documentación y las migraciones que detecta la gema. Para cada operación se especifica la forma insegura de realizarla, junto con la razón y la forma recomendada de hacerla. También se detallan sugerencias y configuraciones propias de la gema.


# Data Migrate

[Data Migrate](https://github.com/ilyakatz/data-migrate) es una gema que introduce el concepto de migraciones de data, que corren junto a las migraciones normales de rails que modifican el *schema*.

## ¿Por qué la usamos?

El propósito principal de las migraciones normales de Rails es cambiar el *schema* de la aplicación. Sin embargo, a veces es necesario hacer cambios a la data misma, y a veces esos cambios deben estar coordinados con un cambio al *schema*. Se podría manipular la data en una misma migración corriente de Rails, pero `data_migrate` nos permite separar esa responsabilidad y mantenerlo más ordenado.

## ¿Cómo la usamos?

### Instalación

La gema viene instalada si el proyecto se generó usando [Potassium](https://github.com/platanus/potassium). También se incluyen configuración necesaria para que [annotate](https://github.com/ctran/annotate_models) se corra al usar las tasks que nos da la gema. Si por alguna razón el proyecto no tiene la gema, se puede agregar, junto a la configuración mencionada, corriendo `potassium install data_migrate`.

### Generar nueva migración de data

```bash
rails g data_migration backfill_some_column_in_some_model
```

> Warning: Si se crea la migración de datos junto a una de schema hay que asegurarse que no compartan el mismo nombre. Si se llamaran igual, las clases creadas en ambas migraciones también tendrían el mismo nombre y Rails se podría confundir. Ver este issue para más detalles.

### Corriendo las migraciones de schema junto a las de data

> El README de la gema describe todos los comandos que agrega la gema. Como se menciona ahí, también se pueden ver corriendo rake -T data.

Hay que tener en mente que en vez de correr `rake db:migrate`, para correr ambos tipos de migraciones juntas se debe usar `rake db:migrate:with_data`. Este comando también está incluido en el archivo `bin/release` en nuestros proyectos, para que se corra en heroku al hacer deploy.

Usar este comando es importante ya que si se corrieran primero las de *schema* y luego las de *data* podrían haber problemas. Para entender esto consideremos el siguiente ejemplo:

Teníamos un usuario con una columna string `address`. Recientemente se cambió el modelo de datos y `address` pasó a ser su propio modelo `Location` con información adicional. Para esto se realizaron tres migraciones:

1. Una de schema que genera la tabla para el modelo `Location` y agrega la referencia a la tabla de usuarios
2. Una de data que para cada usuario le crea una `Location` y parsea el contenido de la columna `address` al formato del nuevo modelo
3. Una de schema que elimina la columna `address` de los usuarios

Si se corrieran las migraciones por separado usando `rake db:migrate` y luego `rake data:migrate` (orden 1 -> 3 -> 2) la migración de data se caería ya que se eliminó la columna `address` antes. Para esto usamos `rake db:migrate:with_data` que las corre todas en orden de creación.

### Posibles problemas relacionados al deploy

Hay algunos casos en que pueden haber problemas con la aplicación en staging o producción:

1. Cuando ocurre un problema y es necesario restaurar un backup y correr las migraciones que se hayan generado entre la fecha del backup y el presente. Esto puede generar problemas con las migraciones de datos si en ellas se accede a cosas que existían a nivel de código cuando se generó el backup pero con la versión actual del código ya no existen. Este no es un problema exclusivo de la gema, siempre que se manipule data en migraciones puede suceder esto.

   Para evitar lo anterior hay un par de alternativas:

   * Definir un modelo "temporal" en la migración de data, asociado a la tabla que se use, y usar este exclusivamente. Esto nos independiza del modelo real, y nos obliga a usar solo lo que esté efectivamente definido en la tabla al momento de correr la migración de data. Es decir, no se correrán validaciones ni callbacks del modelo original, ni tampoco se podrán usar scopes definidos ahí. Este es el *approach* que mencionan en la [guía de estilo de rails de rubocop](https://github.com/rubocop-hq/rails-style-guide#define-model-class-migrations).

     Como ejemplo, digamos que tenemos un usuario que pasa de tener una columna `boolean` que indica si es gerente o no, a tener un string `role`:

     ```ruby
     class BackfillRoleInUsers < ActiveRecord::Migration[6.0]
       class MigrationUser < ApplicationRecord
         self.table_name = :users
       end

       def up
         MigrationUser.all.each do |user|
           user.update!(role: user.is_manager ? 'manager' : 'worker')
         end
       end

       def down
         raise ActiveRecord::IrreversibleMigration
       end
     end
     ```
   * Usar `ActiveRecord::Base.connection.execute(query)` para correr una consulta SQL directamente, donde `query` es el string con esa query. Esto también nos obligaría a usar solo lo que existe en DB al momento de correr la migración.
2. Cuando se corre una migración que crea una nueva columna, y una migración de data a continuación que hace backfill de esa columna. A veces pasa que la migración de data se corre sin problemas, pero los cambios en verdad no se aplicaron. Este problema es especialmente peligroso, porque es posible que en local este problema no ocurra, pero sí en staging/production. Esto pasa porque Rails cachea la información de las columnas al empezar las migraciones, y en la migración de datos se usa ese cache, por lo que el modelo no tiene esa nueva columna, entonces no sabe como guardar ese valor, pero tampoco falla porque sí existe la columna a nivel de DB (esta es una deducción de lo que hemos podido observar en casos que esto ha ocurrido). Esto puede ocurrir incluso si se hace el modelo temporal del paso anterior (suponemos que esto implica que el cache es a nivel de la tabla, no del modelo). Para evitar este problema, la recomendación es \*\*siempre empezar las migraciones de datos reseteando la información de las columnas de todos los modelos que vayamos a usar, usando \*\*[**reset\_column\_information**](https://api.rubyonrails.org/classes/ActiveRecord/ModelSchema/ClassMethods.html#method-i-reset_column_information). Con esto, la migración del punto anterior quedaría así:

   ```ruby
   class BackfillRoleInUsers < ActiveRecord::Migration[6.0]
     class MigrationUser < ApplicationRecord
       self.table_name = :users
     end

     def up
       MigrationUser.reset_column_information 
       MigrationUser.all.each do |user|
         user.update!(role: user.is_manager ? 'manager' : 'worker')
       end
     end

     def down
       raise ActiveRecord::IrreversibleMigration
     end
   end	
   ```

### Recursos útiles

* [Repo](https://github.com/ilyakatz/data-migrate)
* [Otro ejemplo de posible problema en deploy](https://medium.com/@jeffcoh23/why-you-should-avoid-activerecord-when-using-ruby-on-rails-data-migrate-gem-2651739395d9): caso detallado en que hay problemas por diferencias entre el código que se usa en la migración de data y el *codebase* del proyecto al momento de correrla en producción. Explica también como reemplazar el código problemático por SQL directo


# Active Job

[Es un framework que viene con Rails](https://guides.rubyonrails.org/active_job_basics.html) y nos permite definir tareas (jobs) para ejecutar en un "queuing backend".

> 💡 Puedes ver la [presentación sobre Jobs](https://www.youtube.com/watch?v=P-Vqh5z5418) que hicimos en Platanus o continuar leyendo sobre el tema aquí en la guía.

### ¿Para qué los usamos?

Usamos jobs como Rails sugiere:

> These jobs can be everything from regularly scheduled clean-ups, to billing charges, to mailings. Anything that can be chopped up into small units of work and run in parallel, really.

pero en Platanus además los usamos para resolver la **lógica de negocios** de nuestra aplicación. Es decir, no solo para tareas triviales. El motivo de esto es evitar antipatrones como "fat models" o "fat controllers" que ocurren cuando nos vemos obligados a elegir un modelo o un controller para poner business logic. Para dejar esto más claro, veamos un ejemplo:

Supongamos que tenemos los modelos:

```ruby
class User < ApplicationRecord
  has_many :bank_movements
end

class BankMovement
  belongs_to :user
end
```

y queremos agregar lógica para generar un reporte de movimientos bancarios de un usuario.

¿Dónde podrían esta lógica?

Una opción sería ponerla en el modelo `User`:

```ruby
class User < ApplicationRecord
  has_many :bank_movements

  def generate_report
    # lógica para generar el reporte
  end
end
```

La otra opción sería:

```ruby
class BankMovement < ApplicationRecord
  belongs_to :user

  def self.generate_report(user)
    # lógica para generar el reporte
  end
end
```

Pero la verdad es que en ambos casos no "se siente" muy correcto, ¿no?

Ya los imagino pensando cosas como: "¿donde voy a meter el próximo reporte? ¡la clase `User` se volverá gigante!"

Una posible solución a este problema (la que hemos decidido utilizar en Platanus) es poner esta lógica en jobs. Algo así:

```ruby
class GenerateUserBankMovementsReportJob < ApplicationJob
  def perform(user)
    # lógica para generar el reporte
  end
end
```

Con lo anterior logramos encapsular la lógica de creación del reporte y evitar que modelos como `User` o `BankMovement` empiecen a crecer y a tener múltiples responsabilidades.

> 💡 Vale la pena aclarar que hasta no hace mucho tiempo atrás usábamos [comandos de power types](https://github.com/platanus/power-types#commands) para resolver este asunto pero, actualmente, dejamos de utilizarlos por considerar que podíamos lograr lo mismo usando solamente los jobs de Rails.

### ¿Cómo los usamos?

Primero creamos el job con el generador:

```bash
bundle exec rails g job generate_user_bank_movements_report
```

Hacer esto generará dos archivos:

En job `app/jobs/generate_user_bank_movements_report_job.rb`:

```ruby
class GenerateUserBankMovementsReportJob < ApplicationJob
  queue_as :default

  def perform(*args)
    # Do something later
  end
end
```

y su test: `spec/jobs/generate_user_bank_movements_report_job_spec.rb`

```ruby
require 'rails_helper'

RSpec.describe GenerateUserBankMovementsReportJob, type: :job do
  pending "add some examples to (or delete) #{__FILE__}"
end
```

Luego, para ejecutar la tarea:

```ruby
GenerateUserBankMovementsReportJob.perform_now(user)
```

¡Eso es todo!

Ahora supongamos que la generación del reporte es una tarea "pesada" y queremos ejecutarla en background. Es decir, no queremos que se procese inmediatamente sino que queremos mandarla a una cola para que se ejecute luego. Esto se hace llamando a `perform_later` en vez de `perform_now` así:

```ruby
GenerateUserBankMovementsReportJob.perform_later(user)
```

Como decíamos, el código anterior no ejecutará inmediatamente la tarea sino que:

* Persistirá (serialize) el job en algún medio definido por el "queuing backend" (sidekiq, delayed\_job, etc.) que estemos usando. Por ejemplo: sidekiq utiliza redis y delayed\_job postgres o mysql.
* Cuando el "queuing backend" decida, recuperará (deserialize) el job y ejecutará la tarea.

### Instalación

ActiveJob viene con Rails pero en Platanus usamos [Potassium](https://github.com/platanus/potassium) para modificar algunas cosas e instalar sidekiq como queuing backend. Si generaste el proyecto con [Potassium](https://github.com/platanus/potassium) seguramente ya tendrás todo configurado pero, si no es así, puedes ejecutar: `potassium install background_processor`.

El instalador:

* Agrega el archivo `config/initializers/sidekiq.rb` con la configuración básica de sidekiq: conexión con Redis, autenticación del panel de control, etc.
* Agrega el archivo `config/sidekiq.yml` que permite configurar colas, prioridades y concurrencia entre otras cosas. Por ejemplo:

  ```yaml
  production:
    :concurrency: 5
  :queues:
    - critical
    - default
    - low
  ```

  En el archivo anterior, se configuró que en poducción podrán correr a la vez un máximo de 5 jobs (`concurrency: 5`), que habrán 3 colas (`critical`, `default` y `low`) y que `critical` será la más prioritaria (debido al lugar que ocupa en la lista y no al nombre de la cola).
* En los archivos de environment, agrega la opción `config.active_job.queue_adapter` con los valores:
  * `:async` en `config/environments/development.rb`: para correr jobs en RAM. Esto nos sirve en ambiente de desarrollo pero no para producción ya que un reinicio del server eliminará los jobs que tengamos pendientes de ser ejecutados.
  * `:test` en `config/environments/test.rb`: para obtener helpers que nos ayuden a testear jobs fácilmente.
  * `:sidekiq` en `config/environments/production.rb`: para correr jobs con un "backend serio". En Platanus usamos [Sidekiq](https://github.com/mperham/sidekiq)
* Modifica el archivo `Procfile` y le agrega la línea `worker: bundle exec sidekiq`. Esto nos permitirá levantar sidekiq en un worker de Heroku cuando estemos en ambiente de producción.

### Active Job

Como les expliqué al inicio, Active Job es un framework que nos permite definir tareas pero, además, es una "wrapper" del queuing backend. La utilidad de esto es que podemos definir jobs independientemente del backend que utilicemos.

![](/files/G9vEIoiLADR7f1QnaRsV)

Entonces, cuando escribimos por ej:

```ruby
class GenerateUserBankMovementsReportJob < ApplicationJob
  queue_as :default

  def perform(*args)
    # Do something later
  end
end
```

lo que estamos haciendo es definir un Job de ActiveJob y no nos interesa si por debajo lo ejecutará [sidekiq](https://github.com/mperham/sidekiq), [delayed\_job](https://github.com/collectiveidea/delayed_job) o lo que sea.

El mismo job definido en [Sidekiq](https://github.com/mperham/sidekiq), por fuera de ActiveJob, se vería así:

```ruby
class GenerateUserBankMovementsReportJob
  include Sidekiq::Worker

  def perform(*args)
    # Do something
  end
end
```

Definir tareas y configurar cosas por fuera de ActiveJob es algo que deberíamos evitar, ya que al hacerlo nos volvemos dependientes del backend y, si el día de mañana decidimos usar otro ([delayed\_job](https://github.com/collectiveidea/delayed_job) por ejemplo), romperemos alguna funcionalidad.

### Configuraciones de un Job

En la sección de instalación definimos distintas *queues*. En caso que tengamos distintas prioridades para ciertos procesos, podemos especificar la cola a usar con la opción `queue_as`.

También podemos especificar la política en caso de que no se logre ejecutar de manera correcta el job. Esto lo hacemos especificando la opción `retry`. ActiveJob por defecto tiene una política de **reintentar** 5 veces, cada una separada por 3 segundos. Luego de esto se usa la implementación por defecto de Sidekiq, en el que se vuelven a encolar estos jobs pero con un [delay exponencial](https://github.com/mperham/sidekiq/wiki/Error-Handling#automatic-job-retry).

Además podemos especificar que el `Job` se **descarte** en caso de una excepción en específico.

A modo de ejemplo, un proceso que use las dos opciones descritas puede ser definido de la siguiente manera:

```ruby
class ReallyImportantJob < ActiveJob::Base
  queue_as :critical
  discard_on CustomAppException

  def perform(*args)
    # ...
  end
end
```

Las que nombre son configuraciones de las más frecuentes. Para ver más información relacionada con esto, recurre a [la guía de Rails](https://guides.rubyonrails.org/active_job_basics.html).

### Formas de encolar un Job

* **Ejecutar lo antes posible:** en cuanto la cola definida se libere se ejecutará el job. Es importante mencionar aquí que aunque la cola esté vacía, el job correrá de manera asíncrona.

  ```ruby
  GenerateUserBankMovementsReportJob.perform_later(user)
  ```
* **Ejecutar en un momento dado:** se ejecutará el job después del tiempo definido.

  ```ruby
  GenerateUserBankMovementsReportJob.set(wait_until: Date.tomorrow.noon).perform_later(user)
  ```
* **Ejecutar pasado cierto tiempo:** se ejecutará después del plazo dado.

  ```ruby
  GenerateUserBankMovementsReportJob.set(wait: 1.week).perform_later(user)
  ```
* **Ejecutar inmediatamente:** corre el job de manera inmediata, bloqueando la ejecución de tu aplicación. Ojo, esto no llega a Sidekiq, se ejecuta antes.

  ```ruby
  GenerateUserBankMovementsReportJob.perform_now(user)
  ```

### Panel de control de Sidekiq

La gema ofrece una vista donde se puede monitorear el estado de los jobs en la aplicación. Si vas a `config/routes.rb`, vas a ver algo del estilo `mount Sidekiq::Web => '/queue'`. Esto indica que la vista puede ser accedida desde `http://localhost:3000/queue`.

![](/files/axdE2UzTcpNKuIAnHxa3)

> El password para poder ingresar al dashboard estará definido en la variable de entorno: SIDEKIQ\_ADMIN\_PASSWORD.

### Mails

ActiveJob se integra muy bien con [ActionMailer](https://guides.rubyonrails.org/action_mailer_basics.html) y nos permite mandar mails a la cola de manera simple. Por ejemplo:

Si tengo el mailer:

```ruby
class RecruitingProcessMailer < ApplicationMailer
  def personal_interview_mail(recruiting_process)
    # ...
  end
end
```

puedo mandarlo a sidekiq de la siguiente manera:

```ruby
Recruiting::ProcessMailer.personal_interview_mail(recruiting_process).deliver_later
```

sin la necesidad de escribir un job específico para esto.

Es importante destacar además que los mails son agregados a la cola `mailers` y que potassium configura esto en `config/sidekiq.yml`.

```yaml
production:
  :concurrency: 5
:queues:
  - mailers
```

### Trabajos recurrentes

Ponte en el caso en que quisieras mandar un correo a los usuarios todos los días a las 8:00hrs con un chiste para acompañar su café. Para esto podemos usar la gema `sidekiq-scheduler`. Esta también viene en la configuración de Potassium. Pero también puedes agregarla ejecutando `potassium install schedule`.

Supongamos que tenemos nuestro Job definido:

```ruby
class SendUsersAJokeJob < ApplicationJob
  queue_as :default

  def perform
    # Get a funny meme and send it to all users.
  end
end
```

Podemos definir la recurrencia de este Job usando un [Cron](https://crontab.guru/). Estos se definen en `config/sidekiq.yml`

```yaml
:schedule:
  SendUsersAJokeJob:
    cron: '0 8 * * * *' # Runs all days at 8:00 hrs.
    class: HelloWorld
```

### Recursos útiles

* [Presentación Platanus sobre el tema](https://www.youtube.com/watch?v=P-Vqh5z5418)
* [Sidekiq Docs](https://github.com/mperham/sidekiq/wiki/Getting-Started)
* [Sidekiq + ActiveJob](https://github.com/mperham/sidekiq/wiki/Active+Job)
* [Sidekiq-history](https://github.com/russ/sidekiq-history)
* [ActiveJob](https://edgeguides.rubyonrails.org/active_job_basics.html)
* [Buenas prácticas](https://github.com/mperham/sidekiq/wiki/Best-Practices)
* [Active Job Log](https://github.com/platanus/active_job_log)


# Gems

Para crear una gema, en vez de usar el generador de `bundler`, podemos utilizar [gemaker](https://github.com/platanus/gemaker) que hace lo mismo pero, además:

* Permite elegir entre dos tipos de gemas: gema de Ruby o engine de Rails.
* Modifica el `README` para adaptarse al estándar de Platanus.
* Agrega en un archivo `CHANGELOG.md` para animarnos a documentar que se hizo en cada versión de la gema.
* Modifica el archivo de licencia nombrando a Platanus en él.
* Agrega la estructura básica para que nuestra gema tenga una CLI.
* Configura el ambiente de test utilizando RSpec.
* Agrega un generador para instalar la gema.
* Configura Circle CI.

```bash
gemaker new my_gem
```

Al correr el comando anterior gemaker, antes de crear la gema, nos hará una serie de preguntas para:

* Dejar listo (o casi listo) archivos como el `README` o el `.gemspec`.
* Que seleccionemos aquella funcionalidad que es opcional y que necesitemos en nuestra gema. Por ej: podríamos no necesitar un CLI o un instalador o querer que nuestra gema sea una extensión de Rails (engine) o no.


# Engines - Modularización en Rails

> 💡 Antes de arrancar, quiero aclarar que la siguiente guía habla desde la perspectiva de <https://github.com/platanus/nest> ya que es el único proyecto donde estamos experimentando con engines.

Para modularizar nest, estamos usando [engines de Rails](https://guides.rubyonrails.org/engines.html).

Se debe tener en cuenta que el concepto es el mismo que el de la [guía de rails](https://guides.rubyonrails.org/engines.html) pero en nest estamos poniendo los módulos/engines dentro del repositorio de la main app.

También es importante aclarar que consideramos la "main app" a todo lo que está directamente bajo el root del proyecto. Por ej: un modelo en `app/models/user.rb` es un modelo de la main app. En cambio un modelo en `engines/recruiting/app/models/recruiting/user.rb` es un modelo del engine de reclutamiento.

## Generador

Para facilitar la tarea de crear nuevos engines usamos [Metagem](https://github.com/platanus/metagem) de la siguiente manera:

```bash
bin/rails g metagem:new_engine engine_name
```

Ejemplo:

```bash
bin/rails g metagem:new_engine recruiting
```

Contesta las preguntas y terminarás teniendo la estructura base de tu engine.

> Si modificas algo en algún engine, por favor traspasa ese conocimiento a Metagem. La idea de tener un generador es que este vaya recogiendo el conocimiento común y así evitar que otros devs tengan que enfrentarse a los mismos problemas.

## Conceptos generales

### Diferencia entre engines y gemas

### Gemas

* Son código encapsulado Ruby. Una librería.
* Se utilizan para extraer alguna funcionalidad específica.
* Se puede utilizar en proyectos que no sean Rails.
* NO tienen código Rails: controllers, modelos, etc.
* Ejemplos de gemas: `httparty`, `remove_bg`, `pry`, etc.

### Engines

* Los engines también son gemas de Ruby.
* Se utilizan para extender Rails.
* Tienen la misma estructura de directorios (app, config, etc) que una app Rails.
* En Platanus las utilizamos para extraer funcionalidades (módulos) completas.
* Ejemplos de engines: `devise`, `paranoia`, `active admin`, etc.

### Estructura

### Main App

Es donde está todo el código que es común a todos los módulos y aquí es donde se conectarán todos los feature engines.

### Feature Engine

Existirán n feature engines. Uno por cada funcionalidad independiente de Platanus. Los feature engines deben pensarse como **funcionalidad que se puede desconectar de la main app sin romperla.** Por ejemplo el módulo de reclutamiento es un ejemplo de feature engine.

### Testing

Los tests de una funcionalidad de la main app van dentro de /spec en cambio los tests de una funcionalidad de un engine va dentro de engines/engine\_name/spec. Por ej: engines/recruiting/spec/models/recruiting/process\_spec.rb es el test de un modelo del módulo/engine de reclutamiento

### Initializer

Se utilizan para configurar gemas. Para agregar configuración al engine, primero deberás definir el atributo en `engines/tu_engine/lib/tu_engine.rb`. Por ejemplo: supongamos que queremos pasar un token al engine `SlackUtils`.

Primero agregamos el atributo `api_token` en `engines/slack_utils/lib/slack_utils.rb` así:

```ruby
require "slack_utils/engine"

module SlackUtils
  extend self

  attr_accessor :api_token

  def configure
    yield self
    require "slack_utils"
  end
end
```

Luego, en el initializer del engine (ubicado en `/app/config/initializers/slack_utils.rb` siguiendo el ejemplo):

```ruby
EnginesUtil.initialize_engine(:slack_utils) do |config|
  config.api_token = "XXX"
end
```

Después, dentro del engine, podrás usarlo así:

```ruby
SlackUtils.api_token #=> "XXX"
```

### Referenciar gemas locales en otras gemas/engines locales.

Es común que pase que en un engine que definimos localmente (dentro de la main app) queramos poner como dependencia a otro engine o gema local. Para explicarles cómo se hace supongamos que tengo el engine de reclutamiento (recruiting) que requiere el engine slack\_utils:

Primero, en el `Gemfile` de recruiting agregaremos la referencia a la gema así:

```ruby
source '<https://rubygems.org>'
git_source(:github) { |repo| "<https://github.com/#{repo}.git>" }

gemspec

gem "slack_utils", path: "../../engines/slack_utils"
```

> Como se puede ver, se hace una referencia a un path local.

y en el `recruiting.gemspec`:

```ruby
spec.add_dependency "slack_utils"
```

De esta manera, al hacer `bundle install`, el `Gemfile.lock` referenciará al path local en vez de a una gema publicada en rubygems.

> Es importante destacar que las dependencias entre engines se hacen a nivel de gemspec/Gemfile y a través del initializer.

### Particularidades de los Engines

### Ubicación

Los engines se crean dentro de `/engines` y se agregan automáticamente en el `Gemfile` de la app principal. Los tests van dentro de `engines/tu_engine/spec`

### Isolated namespace

Los engines ejecutan un método `isolate_namespace MyEngine` así:

```ruby
module Recruiting
  class Engine < ::Rails::Engine
    isolate_namespace Recruiting
    # ...
```

Ese método se encarga de aislar controllers, models, routes, etc. dentro de un namespace para evitar colisiones de nombres y overrides. Por ejemplo, al crear un nuevo modelo, este se creará dentro del namespace y la tabla tendrá como prefijo el nombre del engine.

Se creará:

```ruby
module Recruiting
  class ProcessType < ApplicationRecord
    # ...
```

en vez de:

```ruby
class ProcessType < ApplicationRecord
  # ...
```

y la tabla se llamará `recruiting_process_types` en vez de simplemente `process_types`

### Extender clases

Es común que el engine extienda models, controllers, etc. existentes. Estas extensiones se agregan dentro del engine en el directorio que corresponda. Por ejemplo: si dentro del engine fdbk quiero extender el modelo `TeamMember` esa extensión debería ponerla acá: `engines/nombre_engine/app/models/nombre_engine/nombre_modelo_ext.rb` -> `engines/fdbk/app/models/fdbk/team_member_ext.rb`. Si en cambio quiero extender por ej un observer, debería ponerlo acá: `engines/nombre_engine/app/observers/nombre_engine/nombre_observer_ext.rb` -> `engines/fdbk/app/observers/fdbk/team_member_observer_ext.rb`.

Por ej, para extener el modelo `TeamMember` los pasos a seguir son:

1. Hacer que el modelo nos indique que está listo para ser extendido.

   Esto se hace dispatcheando el evento \*\*al final \*\*de la clase definida en `app/models/team_member.rb`

   ```ruby
   class TeamMember < ApplicationRecord
     #...

     ActiveSupport.run_load_hooks("TeamMember", self)
   end
   ```
2. Definir la extensión.

   Como es un modelo, debo agregarla acá: `engines/tu_engine/app/models/tu_engine/team_member_ext.rb` y se ve algo así:

   ```ruby
   module TuEngine
     module TeamMemberExt
       extend ActiveSupport::Concern

       included do
         def hola
           puts "soy un método que agregó el engine"
         end
       end
     end
   end
   ```
3. Extender la funcionalidad.

   Se hace dentro de `engines/tu_engine/lib/tu_engine/extensions.rb`

   ```ruby
   ActiveSupport.on_load("TeamMember") do
     include TuEngine::TeamMemberExt
   end
   ```

   El código anterior indica que una vez que se ejecute el evento `TeamMember`, extienda la funcionalidad del modelo `TeamMember` incluyendo la extensión `TuEngine::TeamMemberExt`.

Pasa saber si una funcionalidad va dentro de la main app o de una extensión tienen que imaginarse que ponen ese método en la main app y contestarse la siguiente pregunta: "si borrara el engine el método que quedó en la main app sigue funcionando o se rompe". Si la respuesta es: "se rompe" es que va en una extensión. Por ej: supongamos que agrego la siguiente relación a `TeamMember`:

```ruby
class TeamMember < ApplicationRecord
  has_many :fdbk_schedules, class_name: "::Fdbk::Schedule", dependent: :nullify
end
```

Como ven es una relación que apunta a una tabla del engine Fdbk. Entonces me hago la pregunta: "¿Si borro el engine Fdbk esa relación se rompe?" la respuesta en este caso sería que sí ya que al borrar el engine se borraría la tabla `fbdk_schedules` y por lo tanto se rompería la relación. Para que esto no suceda, la extensión se coloca dentro del engine Fdbk y si el día de mañana se borrara (junto a la tabla `fbdk_schedules`) se borraría también la relación `has_many :fdbk_schedules` y todo (lo de la main app) quedaría intacto.

### Activar código de un feature engine en la main app (core)

Para eso se puede utilizar el helper `EnginesUtil`.

```ruby
# como bloque
EnginesUtil.with_available_engine(:nombre_de_tu_engine) do
  # ejecuto acá código que debe correrse solo si el engine está cargado
end
```

```ruby
# como condicional
if EnginesUtil.available_engine?(:nombre_de_tu_engine)
  # ejecuto acá código que debe correrse solo si el engine está cargado
end
```

Ejemplo:

```ruby
EnginesUtil.with_available_engine(:recruiting) do
  # ...
end
```

En general cuando estoy trabajando en un engine debo intentar por todos los medios poner vistas, modelos, etc. dentro del engine. El problema es que muchas veces eso no se podría hacer. Supongamos el caso de una navbar que vive en la main app. Si quiero agregar un link a alguna vista de reclutamiento no me queda otra que agregarlo dentro de la main app, no? Bueno, en casos como estos se puede usar los métodos mencionados anteriormente. Por ej: en el dashboard de active admin que vive dentro de la main app, se agrega info de los diferentes engines solo si están activos. Este es el código:

```ruby
ActiveAdmin.register_page "Dashboard" do
  menu priority: 1, label: proc { I18n.t("active_admin.dashboard") }


  content title: proc { I18n.t("active_admin.dashboard") } do
    panel I18n.t("active_admin.pages.dashboard.search_platanus_github") do
      render partial: 'search_github_form'
    end

    if EnginesUtil.available_engine?(:recruiting)
      render("admin/dashboard/recruiting_processes")
    end

    if EnginesUtil.available_engine?(:fdbk)
      render("admin/dashboard/fdbk_sessions")
    end

    panel I18n.t("active_admin.pages.dashboard.tasks") do
      render partial: 'tasks'
    end
  end
end
```

Con el código anterior, si se borrara el engine de reclutamiento, esta vista `admin/dashboard/recruiting_processes` se borarría también. Si no existiera la condición `if EnginesUtil.available_engine?(:recruiting)` daría un error. Como si existe la condición, simplemente no se mostrará info relacionada al reclutamiento y ya.

### Agregar modelos, controllers, etc.

Los engines tienen la misma estructura que una rails app. Entonces si quieres que tu engine agregue por ejemplo un job, harás lo mismo que harías en la main app pero dentro del engine. Es decir, lo agregarás dentro de: `/engines/recruiting/app/jobs/recruiting/assignation_job.rb`

```ruby
module Recruiting
  class AssignationJob < Recruiting::ApplicationJob
    # ...
  end
end
```

Ten en cuenta que:

1. Tu job heredará de `TuEngine::ApplicationJob`. En este caso: `Recruiting::ApplicationJob`
2. Se definirá dentro del namespace `TuEngine`. En este caso: `Recruiting`.

> Lo 2 puntos anteriores se aplican para controllers, modelos, etc.

### Creación de modelos y migraciones

Los modelos para los engines se crean ejecutanto el siguiente comando en el root del proyecto:

```bash
bin/rails g model engine_name/model_name --engine-model
```

Como se puede ver, la única diferencia con la creación de un modelo normal de Rails es la opción `--engine-model`

Por ej:

```bash
bin/rails g model guides/media_file type:string name:string identifier:string file_data:text section:references  --engine-model
```

Algo a tener en cuenta es que, como metagem está en desarrollo, hoy en día los modelos colocan las migraciones donde corresponde pero si luego creamos más migraciones, estas se crearán en la main app y deberemos moverlas a mano a `engines/nombre_engine/db/migrate/xxx.rb` -> ej: `engines/guides/db/migrate/20220927140455_create_guides_sections.rb`

### Rutas

Las rutas de un engine se definen dentro del engine pero, para que estén disponibles dentro de la main app, deberás montarlas en `/config/routes.rb` así:

```ruby
Rails.application.routes.draw do
  mount Recruiting::Engine, at: '/recruiting'

  #...
```

En el engine `/engines/recruiting/config/routes.rb`

```ruby
Recruiting::Engine.routes.draw do
  get  '/dashboard' => 'dashboard#index'
  root 'dashboard#index'
end
```

Un detalle importante a considerar es que desde main app se tiene acceso a los helpers de los engines disponibles según su nombre de ruta que se puede obtener con los comandos anteriores. Por ejemplo:

```ruby
# app/views/shared/navbar/_right_menu.html.erb (desde main_app)

link_to recruiting.recruiting_index_url
```

Y, también, desde los engines, se tiene acceso a `main_app`, con lo que se tiene acceso directo a los helpers de rutas del `main_app`. Por ejemplo:

```ruby
# engines/recruiting/app/views/recruiting/shared/_main_header.html.erb (desde recruiting)

link_to "Ingresar", main_app.recruiting_new_user_session_path
```

### Testing

* Al ejecutar `export ENABLED_ENGINES='' bin/rspec spec` se correrán **solo los tests de la main app con todos los engines prendidos**.
* Al ejecutar `export ENABLED_ENGINES=false bin/rspec spec` se correrán **solo los tests de la main app con todos los engines apagados**.
* Al ejecutar `export ENABLED_ENGINES='engine_name' bin/rspec engines/engine_name/spec` -> ej: `export ENABLED_ENGINES='recruiting' bin/rspec engines/recruiting/spec` se correrán los **test de un engine específico**. En este caso los de reclutamiento.
* Al ejecutar `export ENABLED_ENGINES='guides' bin/rspec engines/recruiting/spec` \*\*veremos el error \*\***`uninitialized constant Recruiting`** porque estamos tratando de correr los tests para reclutamiento activando solo el engine `guides`.

Quizás la forma más cómoda de correr los tests localmente es dejar la variable de entorno seteada en vacío -> `ENABLED_ENGINES=''` para que estén todos los engines habilitados ya que igual, en CircleCI, los tests correrán de manera independiente para asegurar que no tienen dependencias indebidas.

Si hacemos esto último entonces:

* Al ejecutar `bin/rspec spec` se correrán **solo los tests de la main app con todos los engines prendidos.**
* Al ejecutar `bin/rspec engines/recruiting/spec` se correrán **solo los tests del engine de reclutamiento con todos los engines prendidos.**

También puedes usar `bin/guard` para que los tests corran cuando modifiques las clases o sus tests. Recuerda tener `ENABLED_ENGINES=''`.

## Front related

Las siguientes secciones no están muy chequeadas ya que todavía estamos explorando el tema modularización y engines. Solo se han dejado en esta guía como futura referencia.

### Webpacker

Cada `engine` debe tener su propio pack en `main_app`, como, por ejemplo:

* `app/javascript/packs/recruiting/recruiting.js`

Ahí, debe definir las siguientes cosas:

* Dependencias JS definidas en `package.json` de `main_app`
* Punto de entrada a SCSS definidos en `javascript/stylesheets` del `engine`
* Punto de entrada a Assets definidos en `javascript/assets` del `engine`
* Componentes Vue definidos en `javascript/components` del `engine`

Es bueno definir un custom alias del directorio `javascript` del `engine`:

```javascript
// config/webpack/custom.js

'@recruiting': path.resolve(__dirname, '..', '..', 'engines/recruiting/app/javascript')
```

Para así, luego usar de forma más limpia en el Pack del `engine`:

```javascript
// Pack: app/javascript/packs/recruiting/recruiting.js (partial)
import '@recruiting/stylesheets/recruiting/recruiting';

Vue.component('component-name', () => import('@recruiting/components/recruiting/component-name'));
```

```javascript
// Pack: app/javascript/packs/recruiting/recruiting.js (partial)
require.context('@recruiting/assets/recruiting', true);
```

### Componentes Vue

Los componentes Vue deben quedar en la carpeta `javascript/components` del `engine`.

Ejemplo:

* `engines/recruiting/app/javascript/components/recruiting`

Y deben ser referenciados desde su Pack particular como se mencionó anteriormente.

### SCSS

Un detalle importante al escribir SCSS, es utilizar apropiadamente las referencias a assets.

Por ejemplo, para usar `url()` debes considerar lo siguiente:

* Si el asset está en el `engine`, debes usar un path relativo, como: `url('../../path/to/asset.png')`
* Pero, si el asset está en `main_app`, debes usar este tipo de path partiendo desde `app/javascript` como referencia: `url('~path/to/asset.png')`

### Assets

Assets en el pipeline de Sprockets, se pueden usar directamente igual que en `main_app`. Siguiendo la misma estructura de directorio y configurando el `initializer` respectivo.

Como ejemplo, considera el caso de Discovery:

* `engines/recruiting/config/initializers/assets.rb`
* `engines/recruiting/app/assets/images/defaults/recruiting/topic/image.png`


# JavaScript

[Vue](/stack/javascript/vue)

[AlpineJS](/stack/javascript/alpinejs)


# Vue

[General](/stack/javascript/vue/general)

[Pinia](https://github.com/platanus/la-guia/blob/master/stack/javascript/vue/pinia.md)

[Testing](/stack/javascript/vue/testing)


# General

[Vue.js](https://vuejs.org/v2/guide/) es un framework progresivo orientado a la construcción de interfaces de usuario, siendo capaz también de servir en la contrucción de SPAs.

La documentación es excelente y debería ser el primer paso en caso de cualquier duda.

### Tips

La regla en general en Platanus es que solo puede haber una fuente de la verdad para el estado de la aplicación, determinada por quién maneja la paginación.

* Si la paginación la maneja Rails, Rails debería manejar el estado de la aplicación, siendo Vue un suplemento para agregar elementos dinámicos.
* Si la paginación la maneja Vue (mediante un router, por ejemplo), el estado de la aplicación lo debería manejar Vue (con Vuex probablemente), usando Rails principalmente como API.

Hay casos en que se pueden mezclar, como por ejemplo un componente complejo (un wizard o tour, por ejemplo) puede tener su propio estado interno dentro de una página de Rails, pero lo ideal es elegir uno de estos caminos por proyecto para evitar confusiones.


# Testing

Para realizar tests unitarios dentro de Rails y Vue se puede usar [Jest](https://jestjs.io/) y [Vue Test Utils](https://vue-test-utils.vuejs.org/). Como siempre, la [documentación de Vue](https://vuejs.org/v2/guide/unit-testing.html) es un buen punto de partida.

En proyectos nuevos Potassium ya incluye los archivos y la configuración necesaria para empezar a testear. En proyectos antiguos sigue los siguientes pasos para tener un ambiente listo para realizar pruebas de manera local.

```bash
> yarn add jest vue-jest babel-jest @vue/test-utils jest-serializer-vue babel-core@^7.0.0-bridge.0 --dev
```

```json
// https://vue-test-utils.vuejs.org/guides/#testing-single-file-components-with-jest
package.json
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch"
  },
  "jest": {
    "roots": [
      "app/javascript"
    ],
    "moduleDirectories": [
      "node_modules",
      "app/javascript"
    ],
    "moduleNameMapper": {
      "^@/(.*)$": "app/javascript/$1"
    },
    "moduleFileExtensions": [
      "js",
      "json",
      "vue"
    ],
    "transform": {
      "^.+\\\\.js$": "<rootDir>/node_modules/babel-jest",
      ".*\\\\.(vue)$": "<rootDir>/node_modules/vue-jest"
    },
    "snapshotSerializers": [
      "<rootDir>/node_modules/jest-serializer-vue"
    ]
  },
```

***

Para ejecutar los tests en un ambiente CI (como CircleCI), copia los cambios correspondientes de [bin/ci\_build](https://github.com/platanus/potassium/blob/ce9aa9e1ddd19c344b74afe5dfa3a4c7af866176/lib/potassium/assets/bin/cibuild.erb), [.circleci/config.yml](https://github.com/platanus/potassium/blob/ce9aa9e1ddd19c344b74afe5dfa3a4c7af866176/lib/potassium/assets/.circleci/config.yml.erb) y [docker-compose.ci](https://github.com/platanus/potassium/blob/ce9aa9e1ddd19c344b74afe5dfa3a4c7af866176/lib/potassium/assets/docker-compose.ci.yml)

***

```bash
> yarn test
 FAIL  config/webpack/test.js
  ● Test suite failed to run

    Your test suite must contain at least one test.
```

Un ejemplo de test básico usando el componente que instala el setup:

`App.vue`

```javascript
<template>
  <div id="app">
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  data: function () {
    return {
      message: "Hello Platanus!"
    }
  }
}
</script>

<style scoped>
p {
  font-size: 2em;
  text-align: center;
}
</style>
```

`App.spec.js`

```javascript
import { shallowMount } from '@vue/test-utils';
import App from './app.vue';

describe('app.vue', () => {
  it('displays message on load', () => {
    const wrapper = shallowMount(App);
    expect(wrapper.find('p').text()).toEqual('Hello Platanus!');
  });
});
```

```
 PASS  app/javascript/app.spec.js
  app.vue
    ✓ displays message on load (25ms)
```


# AlpineJS

## ActiveAdmin + AlpineJS

AlpineJS es una excelente alternativa para agregar algo de inteligencia a nuestras vistas de ActiveAdmin. En la mayoría de los casos podemos definir todo lo que usamos directamente en nuestro recurso de ActiveAdmin, sin necesidad de archivos JS externos.

## Instalando AlpineJS

```bash
# Si estamos usando Shakapacker, Webpack 5+ u otro bundler moderno
yarn add alpinejs

# Si estamos usando Webpacker
yarn add alpinejs@2
```

Después tenemos que agregar lo siguiente al archivo donde ActiveAdmin se inicializa, normalmente el que tiene la linea `import '@activeadmin/activeadmin';`

```javascript
import '@activeadmin/activeadmin';

import Alpine from 'alpinejs';

window.Alpine = Alpine;
Alpine.start();
```

Si estás usando Alpine 2 (por Webpacker), tienes que usar lo siguiente:

```javascript
import '@activeadmin/activeadmin';

import 'alpinejs'
```

> 💡 Todos los ejemplos en esta guía y el repositorio asociado usan AlpineJS 3 pero deberían funcionar en AlpineJS 2.

## Intro

Un componente de AlpineJS es un elemento HTML con el atributo (también llamado directiva) `x-data` con todas las variables que vamos a usar dentro de un objeto de javascript.

```html
<div x-data="{open: false}"></div>
```

Para que esto funcione en ActiveAdmin, tenemos que agregar el atributo `x-data` a `inputs`, `input` o cualquier otro elemento "wrapper"

```ruby
f.inputs 'x-data':  CGI.escapeHTML("{...#{f.resource.attributes.to_json}}") do
  f.input :name
end
```

> 💡 Tenemos que usar `CGI.escapeHTML` para evitar que el objeto producido por Rails no escape del atributo `x-data`, lo que normalmente pasa por una comilla doble.

> 💡 `f.resource.attributes` nos da acceso a todos los atributos del modelo. En vez de usarlo, también puedes declarar los valores a mano, siempre recordando que el resultado final debe ser un objeto válido de javascript.

> 💡 Si un ejemplo en la guía no incluye el atributo `x-data`, de todas maneras se asume que fue declarado

Una vez que hemos inicializado el componente con `x-data` podemos empezar a usar las otras directivas de AlpineJS.

```ruby
f.inputs 'x-data':  CGI.escapeHTML("{...#{f.resource.attributes.to_json}}") do
  f.input :name, input_html: { 'x-model': 'name' }
end
```

## Formatear el valor de un campo al escribir

Casos de uso: Números de teléfono, valores en formato de moneda local, números de cédula de identidad, etc.

Para empezar tenemos que agregar la función para formatear en el mismo archivo donde inicializamos Alpine y exponer la función a la página en ActiveAdmin.

```javascript
window.Alpine = Alpine;
Alpine.start();

window.formatters = {
  // Formats a number to currency
  currency: new Intl.NumberFormat('es-CL', { style: 'currency', currency: 'CLP' }),
  // Removes everything that's not a number from a string
  numberCleaner(value) {
    return value.replaceAll(/\\D/g, '');
  },
};
```

Después, dentro de nuestro recurso en ActiveAdmin, podemos usar `x-on-input` (o `@input`) para formatear el valor cada vez que escribimos un valor en el campo de texto.

> 💡 Para este ejemplo en especifico, tenemos que limpiar el número (para obtener `1000` en vez de `1.000`), asi que ejecutamos `numberCleaner` antes de \`format.

```ruby
f.input :amount, input_html: {
  'x-model': 'amount',
  'x-on:input': 'amount = formatters.currency.format(formatters.numberCleaner($event.target.value));'
}
```

Si actualizamos la página el monto no estará formateado porque el formateador solo se ejecuta con el evento `input`. Para arreglar esto tenemos que editar el valor iniciar en `x-data`.

```ruby

f.inputs 'x-data': CGI.escapeHTML("{ amount: formatters.currency.format('#{f.resource.attributes['amount']}')}") do

  f.input :amount, input_html:
    'x-model': 'amount',
    'x-on:input': 'amount = formatters.currency.format(formatters.numberCleaner($event.target.value));
    '
```

En el caso que el atributo del modelo sea un `integer` en la base de datos, no podremos guardar el valor formateado. Para lograr guardar el valor actualizado pero mantener el formato al editarlo tenemos que agregar un par de cosas para tener dos campos: el campo formateador y el campo "real" que se guarda en la base de datos.

En el modelo agregamos:

```ruby
class FormatFieldExample < ApplicationRecord
  attr_accessor :active_admin_amount
end
```

> 💡 `attr_accessor` es necesario ya que ActiveAdmin no permite mostrar valores que no existen como campos en su formulario.

Después, en el archivo de ActiveAdmin, agregamos `amount` directamente y además agregamos el campo `active_admin_amount` formateado a `x-data`.

```ruby
f.inputs 'x-data': CGI.escapeHTML("{
    amount: #{f.resource.attributes['amount']},
    active_admin_amount: formatters.currency.format('#{f.resource.attributes['amount']}')},
  }") do
```

Reemplazamos el campo `amount` por `active_admin_amount` y en el evento `x-on:input` agregamos que también se actualice `amount`.

```ruby
  f.input :active_admin_amount, input_html:
    'x-model': 'active_admin_amount',
    'x-on:input': '
      active_admin_amount = formatters.currency.format(formatters.numberCleaner($event.target.value));
      amount = formatters.numberCleaner(active_admin_amount);
    '
```

Finalmente agregamos un campo oculto con el `amount` real para que se guarde en la base de datos como número.

```ruby
f.input :amount, as: :hidden, input_html: {
  'x-bind:value': 'amount'
}
```

[ejemplo](https://github.com/platanus/activeadmin-alpinejs-examples/blob/main/app/admin/format_field_examples.rb)

## Validar un campo

Casos de uso: Prevenir que un formulario se pueda guardar si un valor no es válido, mostrar cuando un campo es obligatorio o tiene un valor inválido.

Al igual que en el ejemplo anterior, tenemos que agregar la función de validación a la variable `window` para que esté disponible en la página de ActiveAdmin.

```ruby
import { rutValidate } from 'rut-helpers';

window.validators = {
  // Formats a value to the standard RUT format.
  rut: rutValidate
};
```

En este ejemplo queremos cambiar la clase CSS del campo cuando el valor no es válido. Para esto necesitamos usar `x-bind:class`(o `:class`) para que la clase `error` sea agregada dinámicamente cuando `validators.rut(rut)` sea `false`:

```ruby
f.input :rut, input_html: {
  'x-model': 'rut',
  'x-bind:class': '{error: !validators.rut(rut)}'
}
```

Si también queremos desactivar el botón para guardar, podemos editar la acción `submit` para agregar el atributo `disabled`. `x-bind:disabled` (o `:disabled`) automáticamente agregan el atributo cuando la validación falla.

```ruby
f.actions do
  f.action :submit, button_html: { 'x-bind:disabled': "!validators.rut(rut)" }
end
```

[ejemplo](https://github.com/platanus/activeadmin-alpinejs-examples/blob/main/app/admin/validate_field_examples.rb)

## Esconder y mostrar un campo

Para poder mostrar y esconder un campo podemos usar la directiva `x-show`.

Primero necesitamos un campo con `x-model` para tener acceso a su valor.

```ruby
f.input :has_description, input_html: {
  'x-model': 'has_description'
}
```

Después agregamos la directiva `x-show` al campo que queremos mostrar o esconder dependiendo del valor que tenga el campo `has_description`.

```ruby
f.input :description, wrapper_html: {
  'x-show': 'has_description'
}
```

> 💡 Tenemos que usar `wrapper_html` en vez de `input_html` para esconder toda la fila, tanto el `label` como el `input`.

[ejemplo](https://github.com/platanus/activeadmin-alpinejs-examples/blob/main/app/admin/toggle_field_examples.rb)

## Campos Select2

[ActiveAdmin Addons](https://github.com/platanus/activeadmin_addons) transforma todos los `select` para que usen Select2, para facilitar el uso de colección grandes o tags. Sin embargo, AlpineJS no tiene idea qué hacer con los elementos de Select2 y viceversa.

Para que funcionen los elementos `select` con atributos `x-model` tenemos que instalar [active-admin-alpine-fixes](https://www.npmjs.com/package/active-admin-alpinejs-fixes).

```bash
yarn add active-admin-alpine-fixes
```

Después tenemos que agregar el fix a la variable `window` para que esté disponible en la página de ActiveAdmin

```bash
import { select2 } from 'active-admin-alpine-fixes';

window.alpineFixes = { select2 };
```

Finalmente agregamos el fix a nuestro component agregando la directiva `x-init` para que ejecute el fix apenas el componente sea evaluado por el navegador.

```ruby
f.inputs 'x-init': 'alpineFixes.select2.init', 'x-data':  CGI.escapeHTML("{...#{f.resource.attributes.to_json}}") do
  f.input :choices, input_html: { 'x-model': 'choices' }
end
```

[ejemplo](https://github.com/platanus/activeadmin-alpinejs-examples/blob/main/app/admin/select2_examples.rb)

## Has Many

ActiveAdmin nos permite tener formularios anidados cuando un recurso tiene un `has_many`. Pero cuando hacemos click en el botón para crear un recurso nuevo en este formulario anidado ActiveAdmin usa jQuery para crear los campos nuevos y AlpineJS se confunde.

Para que funcionen tenemos que instalar [active-admin-alpine-fixes](https://www.npmjs.com/package/active-admin-alpinejs-fixes).

```bash
yarn add active-admin-alpine-fixes
```

Después tenemos que agregar el fix a la variable `window` para que esté disponible en la página de ActiveAdmin

```java
import { hasMany } from 'active-admin-alpine-fixes';

window.alpineFixes = { hasMany };
```

En nuestro componente tenemos que agregar el fix a la directiva `x-init` y en nuestro `x-data` tenemos que agregar explícitamente el recurso anidado. Dentro del `has_many` tenemos que usar `x-model` con el índice que nos da para que AlpineJS sepa a qué campo corresponde qué elemento en el arreglo.

```ruby
f.inputs 'x-init': 'alpineFixes.hasMany.init',
          'x-data': CGI.escapeHTML("{
            ...#{f.resource.attributes.to_json},
            children: #{f.resource.children.to_json}
          }") do
  f.has_many :children, allow_destroy: true do |co, i|
    # has_many index starts with 1 while javascript's starts with 0 so we subtract one
    co.input :name, input_html: {
      'x-model': "children[#{i - 1}].name"
    }
  end
end
```

[ejemplo](https://github.com/platanus/activeadmin-alpinejs-examples/blob/main/app/admin/has_many_examples.rb)

## Formularios Complejos (Solo AlpineJS 3 - Alpine.data)

Si nuestro formulario es muy complejo *o* tiene funcionalidad que puede ser fácilmente re-usada, podemos usar `Alpine.data` en nuestro javascript para declarar un objeto que puede ser usado en nuestro formulario sin tener que usar `window`. En otras palabras, podemos tener un archivo JS separado con todo lo que necesitamos.

```javascript
// activeadmin/complex_example.js

export default (attributes = {}) => {
  function init() {
    // We need to pass the Alpine context (this) so it can find the element
    select2.init.bind(this)();
  }

  const currencyFormat = new Intl.NumberFormat('es-CL', { style: 'currency', currency: 'CLP' });

  function numberCleaner(value) {
    return value.replaceAll(/\\D/g, '');
  }


  // We return an object that will be available inside our component
  return { ...attributes, init, currencyFormat, numberCleaner };
};
```

```java
import complexExample from './activeadmin/complex_example';

Alpine.data('complexExample', complexExample);
Alpine.start();
```

Una vez hecho lo anterior, podemos usar `complexExample` en nuestro `x-data`, el que recibe los atributos que necesitamos para inicializar el objeto que usa Alpine.

> 💡 Como complexExample todavía no se ejecuta, currencyFormat todavía no está disponible para ser usado en x-data. Puedes agregar la función a la variable window, procesar los atributos dentro de complexExample o, como en este caso, usar Ruby para lograr el mismo resultado.

```ruby
form do |f|
  f.inputs 'x-data': "complexExample(#{CGI.escapeHTML("{
      ...#{f.resource.attributes.to_json},
      active_admin_amount: '#{number_to_currency(f.resource.attributes['amount'])}'
    }")})" do
    f.input :name

    f.input :active_admin_amount, input_html: {
      'x-model': 'active_admin_amount',
      # We can use currencyFormat and numberCleaner directly since the are available inside
      # the data object returned by the complexExample function.
      'x-on:input': '
        active_admin_amount = currencyFormat.format(numberCleaner($event.target.value));
        amount = numberCleaner(active_admin_amount);
      '
    }

    f.input :choices, input_html: { 'x-model': 'choices' }

    f.input :amount, as: :hidden, input_html: {
      'x-bind:value': 'amount'
    }

    f.actions do
      f.action :submit
    end
  end
end
```

[ejemplo](https://github.com/platanus/activeadmin-alpinejs-examples/blob/main/app/admin/complex_examples.rb)


# CSS

En Platanus usamos [Tailwind CSS](https://tailwindcss.com/) para crear nuestros sitios. En casos específicos, y para la mantención de proyectos antiguos, se recomienda usar [BEM](https://www.notion.so/platanus/bem.md) para organizar el CSS realizado a mano.

**Nota**: Mientras Tailwind CSS resuelve muchos de los problemas relacionados a la creación de CSS, de todas maneras hay que saber cómo funciona para saber qué clases usar. Sobre todo es importante un conocimiento básico de Flexbox y Grid (debido a que arman el layout de todo lo demás) y cuándo es más adecuado usar uno u otro.

### Recursos Útiles

* [CSS Garden](https://cssgridgarden.com/): Juego para aprender a usar todas las propiedades de CSS Grid
* [Flexbox Froggy](https://flexboxfroggy.com/): Juego para aprender a usar todas las propiedades de Flexbox.


# Mobile

## React Native

Ocupamos [React Native](https://reactnative.dev/) para desarrollar proyectos móbiles. Para agilizar aún más las cosas ocupamos Expo, que nos entrega herramientas de desarrollo y deploy sobre React Native.

[Expo](/stack/mobile/expo)

[React Navigation](/stack/mobile/react_navigation)

[Redux](/stack/mobile/redux)

[Styling](/stack/mobile/styling)

[Recursos](/stack/mobile/recursos)


# Expo

[Expo](https://expo.io/) es un ecosistema de herramientas que facilitan el uso de React Native. Apoyan el desarrollo, build e incluso publicación a las stores. El proceso de build se refiere a generar la aplicación nativa en iOS (`.ipa`) o Android (`.apk` o `.aab`).

Las herramientas que ofrecen son:

* [Expo Client](https://expo.io/tools#client): aplicación para iOS y Android en la que podrás correr tu proyecto mientras desarrollas. Esto no requiere hacer un build. También permite compartir proyectos entre miembros del equipo antes de subirlos al store.
* [Expo CLI](https://expo.io/tools#cli): command line interface que permite montar un servidor para correr el proyecto en Expo Client localmente, hacer build y publicar los proyectos
* [Expo Snack](https://expo.io/tools#snack): herramienta online para correr Expo en el browser, permite probar y compartir pequeñas aplicaciones o ejemplos
* [Expo SDK](https://expo.io/tools#sdk): SDK que provee acceso a las APIS nativas como la cámara, ubicación, acelerómetro, notificaciones, y muchos más.

Actualmente los SDKs tienen [actualización trimestral](https://dev.to/expo/expo-sdk-37-is-now-available-69g#time-based-releases). Es importante mantenerse al día y [actualizar el proyecto](https://docs.expo.io/workflow/upgrading-expo-sdk-walkthrough/) con cada nuevo SDK. Primero porque estas actualizaciones suelen traer muchas novedades y arreglo de errores. Segundo porque la actualización paulatina es mucho más fácil que actualizar varias versiones al mismo tiempo y permite manejar los breaking changes de cada uno sin que se acumulen. Tercero porque con cada release se depreca el SDK más antiguo. Si tu proyecto ocupa un SDK deprecado las aplicaciones publicadas seguirán funcionando, pero no podrás seguir ocupando Expo Client o volver a hacer build.


# React Navigation

Una de las funcionalidades clave de cualquier aplicación mobile es poder navegar entre distintas pantallas, para esto ocupamos [React Navigation](https://reactnavigation.org/). La navegación es un poco más compleja que en una aplicación web porque las aplicaciones móviles suelen ocupar navegadores nesteados, por ejemplo ocupar un navegador drawer para las secciones principales y dentro de cada una de dichas secciones un navegador de stack. Los navegadores que implementa React Navigation son tres:

* [Stack Navigator](https://reactnavigation.org/docs/hello-react-navigation): Con este navegador cada vez que navegas a una nueva página, esta se agrega al historial como una [lista LIFO](https://www.geeksforgeeks.org/lifo-last-in-first-out-approach-in-programming/). Al ir atrás entonces la página actual sale del historial y se muestra la página desde la que se navegó.
* [Drawer Navigator](https://reactnavigation.org/docs/drawer-based-navigation): Este es el típico navegador de menú de hamburguesa, que está escondido y al mostrarse puedes elegir entre distintas secciones.
* [Tab Navigator](https://reactnavigation.org/docs/tab-based-navigation): Este navegador es el que muestra las secciones abajo y se puede cambiar entre estas.

Existe una cuarta forma útil de cambiar entre pantallas que es mediante el JSX de un componente. Por ejemplo con un operador ternario (en el `return` de un componente):

```
{
  condition ? (
    <Screen1 />
  ) : (
    <Screen2 />
  )
}
```

Esto se ocupa para cambiar de pantallas mediante código, es útil por ejemplo [para cambiar entre pantalla logeada y de login](https://reactnavigation.org/docs/auth-flow)


# Redux

En React y React Native se suele utilizar el concepto de store. Este es un estado global de la aplicación en el que se mantienen datos que se ocupan en varios lugares y por lo tanto no pertenecen al estado de un solo componente. Para esto ocupamos [Redux](https://redux.js.org/), posiblemente la herramienta más conocida y utilizada para esto.

Hay algunas herramientas complementarias que probablemente necesites:

* [Redux toolkit](https://redux-toolkit.js.org/): "redux con las baterias incluidas", este paquete incluye varias funciones para agilizar el desarrollo con Redux. Incluye funcionalidades que cumplen tareas muy comunes como configurar el store, crear una acción para cada reducer, o separar tu store en convenientes [slices](https://redux-toolkit.js.org/usage/usage-guide#creating-slices-of-state).
* [Redux saga](https://redux-saga.js.org/): redux no tiene la capacidad de manejar acciones asíncronas en sus reducers. Esto es sin embargo una funcionalidad muy usada, por ejemplo para obtener datos del servidor y luego guardarlos en el store. Para suplir esta falencia nace Redux Saga. Esta librería ocupa [generadores](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) y una de sus grandes cruzadas es ser testeable, que en general no es fácil con funciones asíncronas. Es altamente probable que lo necesites.

## Guías

[Crear y conectar una slice en Redux](/stack/mobile/redux/crear_y_conectar_una_slice_en_redux)


# Crear y conectar una slice en Redux

## Qué es una slice?

Una slice contiene una porción de data del estado de la aplicación, y métodos para procesar, cambiar y acceder a esa data.

Las slices de la app van en la carpeta `src/store/slices` con el nombre del modelo de datos que quieren representar, en camelCase. Por ejemplo, la slice que maneja los datos del usuario debería llamarse `user.ts` (o `currentUser.ts`) y su path de importación debería ser `src/store/slices/user` (o `@/store/slices/user` usando el alias `@`).

Hay 3 cosas importantes que proporciona una slice:

* El estado inicial de la data de ese modelo
* Cómo debería modificarse el estado de la data (`actions` y `reducers`)
* Shortcuts customizables para acceder a la data (`selectors`)

## Crear una slice

Importamos la función `createSlice` de la librería `redux-toolkit`, que simplifica muchísimo todo el proceso de definir la data y escribir los reducers. Primero definimos un estado inicial de la slice, y le damos un nombre. Vamos a crear una slice para un contador, que va a partir con cero como valor inicial:

```typescript
import { createSlice } from ''@reduxjs/toolkit';

const initialState = {
	value: 0,
};

const counterSlice = createSlice({
  name: 'counter',
  initialState,
	// otras cosas...
});
```

Ahora debemos especificar en la slice cómo va a cambiar el estado de la data (en este caso, del valor de contador). La función `createSlice` viene con un campo `reducers` en donde podemos especificar justamente eso.

En teoría, para cambiar la data necesitamos definir `actions` y `reducers`, pero la función `createSlice` nos permite preocuparnos solamente de escribir las funciones reducers y a partir de eso genera las actions correspondientes. Por ejemplo, si queremos tener un reducer que incremente el valor del contador en uno, debemos definirla de la siguiente manera:

```typescript
//...

const counterSlice = createSlice({
  name: 'counter',
  initialState,
	reducers: {
    increment: (state) => {
      state.value += 1;
		},
  }
});
```

Además, estas funciones pueden recibir información adicional de lo que deben actualizar a través del campo `payload` de una `action`. Por ejemplo, imaginemos que ahora queremos crear un reducer `incrementBy`, que le suma al contador un número variable:

```typescript
import { createSlice, type PayloadAction } from ''@reduxjs/toolkit';
//...
const counterSlice = createSlice({
  name: 'counter',
  initialState,
	reducers: {
    // ...
    incrementBy: (state, action: PayloadAction<number>) => {
			state.value += action.payload;
		},
  }
});
```

Agregando un reducer para disminuir el valor del contador por un número arbitrario, y también otro reducer para devolver a cero el contador, la versión final del archivo `src/store/slices/counter.ts` queda así:

```typescript
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';

const initialState = {
  value: 0,
};

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    reset(state) {
      state.value = 0;
    },
    incrementBy(state, action: PayloadAction<number>) {
      state.value += action.payload;
    },
    decrementBy(state, action: PayloadAction<number>) {
      state.value -= action.payload;
    },
  },
});
```

Ya tenemos una slice que tiene data inicial y provee formas de manipularla a través de los reducers que definimos, pero todavía no está conectada a la store de la app. Para eso debemos agregar los reducers que definimos al archivo `src/store/reducers.ts` en donde se encuentra el reducer de toda la aplicación (el `appReducer`).

El archivo `src/store/reducers.ts` se debería ver parecido a algo como esto:

```typescript
import { combineReducers } from '@reduxjs/toolkit';

import { userSlice } from '@/store/slices/user';
import { postsSlice } from '@/store/slices/posts';

const appReducer = combineReducers({
  user: userSlice.reducer,
  posts: postsSlice.reducer,
});

export default appReducer;
```

Aquí se encuentra el `appReducer`, que cómo bien dice su nombre, es el reducer que maneja los cambios de estado de toda nuestra aplicación. Este reducer se obtiene usando la función `combineReducers` de `redux-toolkit`.

Para agregar el slice que creamos, tenemos que dentro del objeto que se le pasa a `combineReducers` agregar el campo `counter`y que su valor sea el reducer de nuestra slice.

> ⚠️ OJO! el campo que se agrega a `combineReducers` tiene que coincidir con el nombre que se le dio a la slice dentro de `createSlice`

Finalmente el archivo con el `appReducer` queda así:

```typescript
import { combineReducers } from '@reduxjs/toolkit';

import { counterSlice } from '@/store/slices/counter';
import { userSlice } from '@/store/slices/user';
import { postsSlice } from '@/store/slices/posts';

const appReducer = combineReducers({
  counter: counterSlice.reducer,
  user: userSlice.reducer,
  posts: postsSlice.reducer,
});

export default appReducer;
```

Listo 🥳! Ahora tenemos acceso a la data, las acciones y los reducers que definimos en nuestro archivo original, y ya podemos llamarlos dentro de nuestros componentes de React 💯


# Styling

Para poder darle estilo a tus componentes y que tu aplicación se vea reluciente, existen distintas opciones:

* Tailwind: Es posible usar tailwind en React Native usando la librería `tailwind-rn`. Tenemos una guía sobre cómo utilizar la librería:

  [Usando Tailwind en React Native](/stack/mobile/styling/usando_tailwind_en_react_native)
* [Stylesheets](https://reactnative.dev/docs/stylesheet): este es el método propuesto por React Native. Los estilos se escriben en un objeto y se pasan como prop a cada componente.
* [Styled Components](https://styled-components.com/docs/basics#react-native): librería muy utilizada en React y que puede ser utilizada también en React Native. Los estilos se escriben en un multi-line string lo que lo hace un poco más parecido a CSS.


# Usando Tailwind en React Native

Para usar tailwind en los proyectos mobile, usamos la librería [tailwind-rn](https://github.com/vadimdemedes/tailwind-rn).

> 💡 Si usas una versión de `tailwind-rn` menor a la 4, hay varias cosas que funcionan muy diferente. La mayoría de lo que aparece en esta página no te será de utilidad. Se recomienda migrar usando [esta guía](https://github.com/vadimdemedes/tailwind-rn/blob/master/migrate.md).

Hay que hacer ciertos pasos previos para poder usar tailwind en los componentes de React Native. Si el proyecto se inició con [cavendish](https://github.com/platanus/cavendish), esto ya se encuentra configurado y puedes saltarte a la sección de uso.

## Setup

Se debe correr el comando `npx setup-tailwind-rn` que instala la mayoría de las dependencias. Luego el mensaje final del script contiene los siguientes pasos a seguir. De igual forma, estos pasos se encuentran descritos en el [README del repositorio de Github de la librería](https://github.com/vadimdemedes/tailwind-rn#install).

## Uso

Como tailwind no está soportado nativamente por React Native, la manera en qué esta librería hace que funcione es creando un archivo `tailwind.json` en la raíz del proyecto. El archivo va guardando las clases y hace la traducción al sistema de stylesheets de react native. El archivo puede verse algo como así:

```json
/*
 ...
*/

"items-center": {
		"style": {
			"alignItems": "center"
		}
	},
	"rounded": {
		"style": {
			"borderTopLeftRadius": 4,
			"borderTopRightRadius": 4,
			"borderBottomRightRadius": 4,
			"borderBottomLeftRadius": 4
		}
	},
	"rounded-full": {
		"style": {
			"borderTopLeftRadius": 9999,
			"borderTopRightRadius": 9999,
			"borderBottomRightRadius": 9999,
			"borderBottomLeftRadius": 9999
		}
	},
	"bg-gray-900": {
		"style": {
			"--tw-bg-opacity": 1,
			"backgroundColor": "rgb(17 24 39 / var(--tw-bg-opacity))"
		}
	},
/* 
 ...
*/
```

> 💡 Este archivo no tiene todas las clases existentes de tailwind de una. De hecho al iniciar el proyecto este archivo estará vacío. Si se intenta usar una clase de tailwind que no esté en este archivo,\*\* no va funcionar.\*\*

Cómo se actualiza este archivo? Hay dos formas:

1- Cuando quieras usar una nueva clase de tailwind que no tenías antes, usando el comando

````
```bash
yarn build:tailwind
```

se actualizara el archivo `tailwind.json` con todas las clases que se usan en el proyecto.

Esto se debe hacer cada vez que se quiera agregar una nueva clase y puede ser latero, sobre todo al inicio de un proyecto.  
````

2- Con el comando

```bash
yarn dev:tailwind
```

se lanza un servidor que escucha los cambios del proyecto, y cada vez que se agrega una nueva clase de tailwind, actualiza el archivo `tailwind.json`.

Esto es mucho más cómodo que la primera opción y es por eso **la opción que recomendamos usar.**

En los componentes se debe importar el hook `useTailwind` e invocarlo para obtener la variable `tailwind`. Finalmente, en la prop de `style` de los componentes se usa esta variable para poder usar las clases de tailwind en react native!

```typescript
import { View, Text } from 'react-native';
import { useTailwind } from 'tailwind-rn';

export default function MyTailwindComponent() {
  const tailwind = useTailwind();

  return (
    <View style={tailwind('bg-gray-900 flex-1 items-center justify-center')}>
      <View style={tailwind('bg-white h-40 w-40 p-2 rounded-full items-center justify-center')}>
        <Text style={tailwind('text-lg font-black')}>
          Tailwind in React Native!
        </Text>
      </View>
    </View>
  );
}
```

![](/files/xxW7cprWcDuKO9esyI8P)

## TLDR

* Inicia el servidor de tailwind con `yarn dev:tailwind`
* Importa el hook `useTailwind` de la librería `tailwind-rn`
* `const tailwind = useTailwind()` dentro del componente que estás escribiendo
* En la prop `style` de los componentes, escribir `tailwind('clases de tailwind aquí')`


# Recursos

Algunos recursos útiles que te pueden ayudar:

* [React](https://reactjs.org/): librería de Javascript en la que se basa React Native
* [React Native Directory](https://reactnative.directory/): repositorio de recursos para React Native
* [Expo Blog](https://blog.expo.io/)
* [React Native Blog](https://reactnative.dev/blog/)


# Resolviendo problemas (debugging)

## Resolviendo problemas (debugging)

A veces un `puts` o `console.log` no es la mejor manera de debuggear o ver qué está pasando en una parte del código. En esta sección veremos algunas herramientas que te pueden servir cuando estés desarrollando o buscando el origen de un bug escondido.

## Back - Rails

### Consola

Probablemente ya conoces el comando `rails s` para iniciar el server y ver en esa terminal el output mientras se usa la aplicación. Otro comando de Rails que puede ser útil cuando se quiere probar algo es `rails c`. Este comando abre una consola de rails en la que puedes correr cualquier comando de Ruby/Rails, y también llamar a clases (modelos, jobs, etc.) de tu aplicación.

> 💡 Ojo que si tienes una consola abierta y entremedio haces cambios en el código, esos cambios no se verán reflejados en la consola automáticamente. Debes correr manualmente `reload!`. Al igual que con el servidor, si tus cambios incluyen cambios en una migración o cambios a los initializers, deberás cortar y volver a abrir la consola de Rails, ahí no basta el `reload!`

### Pry

En la consola puedo llamar a métodos/clases de mi código, pero, qué pasa si quiero saber el valor de una variable o un método en un punto específico del código?

Una primera aproximación como mencioné antes podría ser el `puts`, pero hay una forma mejor: `binding.pry` , cortesía de la gema <https://github.com/pry/pry>. Se pone entremedio del código que queremos tener más detalles, igual como se haría con el puts. Luego, cuando la ejecución del código llega a esa línea, se pausa, y en la terminal aparece una consola. Igual que la consola con `rails c` , puedes ejecutar lo que quieras, pero ahora \*\*dentro del contexto en que se puso el binding.pry. \*\*O sea, puedes llamar a métodos privados, variables de instancia o locales, etc:

[Ver video](https://github.com/platanus/la-guia/blob/master/stack/assets/resolviendo-problemas-debugging-1.qt)

Como se ve al final del video, si quieres salir de la consola del `binding.pry` y volver a la ejecución normal del programa, basta con poner `c`. En verdad es un alias de `continue`, definido en los `.pryrc` de nuestros proyectos. Puedes encontrar más detalles de este y otros comandos en <https://github.com/deivid-rodriguez/pry-byebug>, en particular `next` (o `n`) y `step` (o `s`) pueden servir harto cuando se necesita hacer una ejecución más controlada del código.

### Problemas en producción/staging

De repente hay problemas en las apps ya deployeadas que no son fáciles de replicar en local. En esos casos, necesitamos sacar más información desde la app que ya está arriba. Aquí lamentablemente no podremos usar `binding.pry`, pero hay un par de cosas que te pueden servir:

#### Sentry

Nuestros proyectos están configurados para funcionar con [Sentry](https://docs.sentry.io/platforms/ruby/), que centraliza los errores que se lanzan. También está por lo general configurado para que mande notificaciones a un canal de Slack. Es un buen primer lugar para entender qué está pasando.

#### Logs en Heroku

Puedes ver los logs de la aplicación, junto a los de sus workers, con:

```
heroku logs --tail -a pl-nombre-proyecto-staging
```

El `--tail` hace que te sigan llegando los logs que se van generando, en un stream.

Con `-a` le dices a que app conectarse. El nombre de nuestras apps siguen por lo general el formato anterior, terminando en `staging` o `production` según corresponda.

Si quieres filtrar por logs del server o de un worker de sidekiq, puedes filtrar por nombre del dyno:

```

heroku logs --tail --dyno worker.1 -a nombre-de-la-app
```

Puedes ver el nombre de los dynos con `heroku ps -a nombre-de-la-app`.

#### Consola rails en heroku

Puedes correr también la consola de rails en el contexto de una app de heroku:

```
heroku run bundle exec rails c -a nombre-de-la-app
```

> 🚨 Ojo que las cosas que corras aquí van a afectar tu ambiente de staging/production, no es un ambiente sandbox inofensivo. Si creas records por ejemplo, eso se verá reflejado en la base de datos de la app

#### Monkeypatching en la consola

Ruby permite hacer fácilmente *monkeypatching* de una clase, en otras palabras, redefinir partes de esa clase “desde afuera” de dónde se define originalmente. Podemos usar eso para probar cosas en la consola de staging, poniendo puts para debuggear por ejemplo (la gema `pry` no está en ambientes de producción por si acaso). Una forma fácil de hacer eso es definir la clase nuevamente en consola, y redefinir solo los métodos que quiero modificar. El resto de la definición de la clase que no toqué (otros métodos, constantes, etc.) se mantendrán intactos.

Veamos un ejemplo. Digamos que tenemos la siguiente clase

```ruby
class MyJob < ApplicationJob
  def perform
    do_something
    do_something_else
  end

  private

  def do_something
    # do_something implementation
  end

  def do_something_else
    # do_something_else implementation
  end
end
```

Digamos que hay un error y sospecho que está en el método `do_something_else`. Puedo pegar en consola algo así:

```ruby
class MyJob < ApplicationJob
  private

  def do_something_else
    puts 'Some message to help me debug'
    # do_something_else implementation
  end
end
```

Luego puedo correr el Job en consola y se verá el puts o los cambios que haya hecho. Notar que no se necesitó redefinir el resto de la clase, pero si tengo que mantener la implementación de `do_something_else`.

> 💡 Los cambios que se hagan de esta manera solo se mantendrán **dentro de esa sesión de la consola de rails.** O sea que esos cambios no se verán reflejados en la aplicación cuando la usen los usuarios. Los cambios viven y mueren con ese `heroku run bundle exec rails c -a nombre-de-la-app`

## Front

### Debugger

Así como en Ruby está el `puts`, probablemente te suena que en javascript está el `console.log`. Y así como te mostramos `pry` como alternativa, en javascript también tenemos un símil: `debugger`. Lo mismo: se pone entremedio del código que queremos tener más detalles y luego, cuando la ejecución del código llega a esa línea, se pausa. El navegador aparece pausado también, y abre la pestaña Sources, mostrando la línea en que se paró la ejecución. Ahí puedes ir a la pestaña Console y ejecutar lo que sea necesario:

[Ver video](https://github.com/platanus/la-guia/blob/master/stack/assets/resolviendo-problemas-debugging-2.qt)

### Extensión Vue.js devtools

<https://devtools.vuejs.org/guide/installation.html>

Con esta extensión se agrega una nueva pestaña al devtools del navegador. En ella se puede ver el árbol de componentes presente en la vista actual, y detalles de cada componente (props, computed, etc.).

![](/files/Ojm5PuqU9IXwTlk7EGuW)

También puedes ver los eventos que han sido emitidos con sus parámetros en la tab Timeline:

![](/files/bnCwfVTkM8OqTYzRqG7i)


# Machine Learning

[Modelos comerciales de Inteligencia Articial](https://github.com/platanus/la-guia/blob/master/stack/machine_learning/modelos_comerciales_de_inteligencia_articial.md)


# Configuración de tu entorno local

[Instalación Base](/setup/configuracion_de_tu_entorno_local/instalacion_base)

[Tecnologías](/setup/configuracion_de_tu_entorno_local/tecnologias)

[Herramientas](/setup/configuracion_de_tu_entorno_local/herramientas)


# Instalación Base

[OSX](/setup/configuracion_de_tu_entorno_local/instalacion_base/osx)

[Windows](/setup/configuracion_de_tu_entorno_local/instalacion_base/windows)

[Linux](/setup/configuracion_de_tu_entorno_local/instalacion_base/linux)


# OSX

## General

Las siguientes son algunas cosas que puedes hacer e instalar para tener una base sólida y segura en la cual poder instalar todas las herramientas necesarias.

* Habilita el [FileVault](https://support.apple.com/en-us/HT204837) para encriptar los datos de tu disco duro.
* Instala los [Xcode Command Line Tools](http://railsapps.github.io/xcode-command-line-tools.html)
* Instala Homebrew como tu package manager. <https://brew.sh/>
* Instala los updates de sistema operativo que estén disponibles.

  > Recomendación: Instala la última versión del sistema operativo macOS.

## Homebrew

En Platanus utilizamos otras herramientas, algunas para ejecutar en el terminal y otras aplicaciones de escritorio. Además seguro que tu tienes tus preferencias en aplicaciones con las que te gusta trabajar.

### `brew install`

Como ya vimos antes, las herramientas para el terminal las podemos instalar con *brew*. Por ejemplo:

```bash
brew install git heroku imagemagick jq yarn
```

### `brew cask install`

También puedes instalar aplicaciones de escritorio con este comando especial de **brew**

```bash
brew cask install 1password captain docker google-chrome \\
  slack harvest iterm2
```

### `brew bundle`

Para mantener un poco de orden en que herramientas instalas con brew, puedes tener un archivo llamado `.Brewfile` en tu `$HOME` con todas las cosas que quieres instalar. Para las aplicaciones de los ejemplos de arriba seria

```
# $HOME/.Brewfile
brew 'git'
brew 'heroku'
brew 'imagemagick'
brew 'jq'
brew 'yarn'

cask '1password'
cask 'captain'
cask 'docker'
cask 'google-chrome'
cask 'slack'
cask 'harvest'
cask 'iterm2'
```

Luego ejecutar el siguiente comando para instalar las aplicaciones.

```bash
brew bundle --global
```


# Windows

Estos días la mejor manera de tener un ambiente de desarrollo en Windows es usando WSL 2, que genera una instalación de Linux integrada a Windows sin tener que reiniciar.

## Windows 11

1. En Windows Terminal ejecuta `wsl --install`
2. Instala Ubuntu desde la Windows Store <https://apps.microsoft.com/store/detail/ubuntu/9PDXGNCFSCZV>

## Windows 10

### Instalando Windows Terminal

Para instalar Windows Terminal, sigue las instrucciones [aquí](https://docs.microsoft.com/en-us/windows/terminal/get-started).

### Instalando WSL 2

> 💡 Si estás leyendo esto en el futuro (o estás dentro del Windows Insiders Program) instalar wsl puede ser tan fácil como ejecutar `wsl --install` en una consola con permisos de Administrador.

> 💡 Antes de empezar, asegúrate de tener una versión de Windows mayor o igual a **1903** o **Build 18362**. Puedes chequear esto ejecutando con `winver` en el buscador del menu de Windows.

```
<img src='assets/windows-1.png'/>
```

1. Activa WSL ejecutando lo siguiente en una consola de powershell con permisos de administrador:

   ![](/files/TIGy9Am4QAagSQB3Mqv7)

   ```
   dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
   ```
2. Reinicia y ejecuta lo siguiente para activar la virtualización necesaria para WSL 2:

   ```
   dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
   ```
3. Reinicia y descarga la actualización del kernel de linux de la siguiente url: <https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi>
4. En una consola de powershell ejecuta el siguiente comando para usar WSL 2 por defecto:

   ```
   wsl --set-default-version 2
   ```
5. Instala [Ubuntu desde la tienda de Microsoft](https://www.microsoft.com/store/apps/9n6svws3rx71) y crea un usuario cuando termine la instalación.
6. Para tener un terminal decente instala Windows Terminal desde la [Microsoft Store](https://aka.ms/terminal), desde [el repo](https://github.com/microsoft/terminal/releases) o usando scoop (`scoop install windows-terminal`) (ver [Instalando Utilidades](https://www.notion.so/platanus/Windows-a7204bb1aa4f4af597c4cb39fda4df6d#instalando-utilidades))

***

Es posible que sea necesario limitar la cantidad de memoria que WSL usa. Esto se puede lograr editando (o creando) el archivo `C:\\Users\\TU_USUARIO\\.wslconfig` con lo siguiente:

````
```plain text
[wsl2]
memory=4GB
```
````

## Ambiente de Desarrollo

Una vez instalado Windows Terminal, el ambiente de desarrollo funciona como cualquier otra instalación de Linux.

La mayoría de los proyectos de Platanus modernos usan Docker para los servicios como postgres o redis pero lo siguiente es una lista de los requisitos mínimos y como instalarlos desde Windows Terminal y la shell de Ubuntu:

* git (`g`it )
* rbenv (<https://github.com/rbenv/rbenv-installer>) y rbenv-aliases (<https://github.com/tpope/rbenv-aliases>)
* nodenv (<https://github.com/nodenv/nodenv-installer>) y nodenv-aliases (<https://github.com/nodenv/nodenv-aliases>)
* yarn (`npm install -g yarn` o <https://github.com/pine/nodenv-yarn-install> para no tener que instalarlo a mano con cada versión de node)
* dependencia para la gema pg (`sudo apt install libpq-dev`)
* dependencia vips (`sudo apt install libvips42 nip2-`)

> 💡 Si tienes Git for Windows instalado (con scoop: `scoop install git`) puedes usar Git Credential Manager de Windows para que se encargue de recordar tus credenciales de GitHub en WSL. `git config --global credential.helper /mnt/c/Users/TU_USUARIO/scoop/apps/git/current/mingw64/bin/git-credential-manager.exe`

> 💡 A pesar que WSL 2 lo permite, no te recomendamos clonar los proyectos dentro del filesystem de Windows (`/mnt/c` o similar) por temas de performance. Lo mejor es mantener los proyectos dentro del filesystem de Linux (`~/`). Si necesitas entrar a estas carpetas con File Explorer puedes hacerlo ejecutando `explorer.exe .` en la carpeta correspondiente o navegando a `\\\\wsl$\\Ubuntu-20.04\\home\\TU_USUARIO\\`. En Windows Terminal puedes configurar que siempre se abra en tu home en las opciones.

### Instalando Utilidades

Aparte de WSL 2, para instalar utilidades nativas de Windows puedes usar [scoop](https://scoop.sh/), un instalador para la linea de comando que hace muy fácil instalar binarios (como imagemagick, ffmpeg, etc) para que queden disponibles en el PATH de Windows.

> 💡 En teoría es posible usar scoop y Docker para instalar todo lo necesario ejecutar los proyectos sin necesidad de WSL 2 pero se aleja del setup estandar que usamos en Platanus y no podremos ayudarte si tienes algún problema.

## Links de Referencia

* <https://docs.microsoft.com/en-us/windows/wsl/install-win10>
* <https://docs.microsoft.com/en-us/windows/terminal/get-started>
* <https://docs.microsoft.com/en-us/windows/wsl/tutorials/wsl-containers>
* <https://docs.microsoft.com/en-us/windows/wsl/wsl-config#configure-global-options-with-wslconfig>


# Linux

## Linux

## Ubuntu 20.04+

### General

Las documentaciones oficiales de instalación para Linux suelen asumir que tenemos el sistema operativo actualizado, y por lo general utilizan las herramientas que ya vienen en Ubuntu.

De todas formas, siempre se sugiere actualizar los repositorios listados en `/etc/apt/sources.list`

```shell
sudo apt update
sudo apt upgrade
```

En futuras ediciones se añadirán guías para más distros de linux. Por ahora, refiere a las documentaciones oficiales.


# Tecnologías

[Ruby](/setup/configuracion_de_tu_entorno_local/tecnologias/ruby)

[Docker](/setup/configuracion_de_tu_entorno_local/tecnologias/docker)

[Node](/setup/configuracion_de_tu_entorno_local/tecnologias/node)


# Ruby

Para nuestros desarrollos en Ruby utilizamos el manejador de versiones [rbenv](https://github.com/rbenv/rbenv) y algunos plugins. La versión que usa cada proyecto está indicada en el `.ruby-version`

## OSX

### Previo a la instalación

Antes de empezar con esta instalación tienes que revisar si tienes `rvm` instalado y quitarlo de tu computador.

Para esto ejecuta:

```bash
rvm
```

y si dice que no existe, tu computador está listo para la instalación, y si aparece algo tienes que desinstalarlo con

```bash
rvm implode

gem uninstall rvm
```

Finalmente, revisa tus archivos `.bash_profile` o `.zshrc` y comprueba que no quedan lineas relacionadas con `rvm`.

### Instalación

```bash
# Instala rbenv
brew install rbenv

# Instala plugins
brew install ruby-build rbenv-vars rbenv-aliases rbenv-default-gems
```

Luego debes cargar rbenv en tu shell para que puedas acceder a las diferentes versiones. Para esto debes agregar la siguiente linea en tu `.bash_profile` o `.zshrc` dependiendo del shell que uses. Hay dos formas de hacerlo:

1. Ejecutar el siguiente, que agrega automáticamente la línea necesaria:
   * Si usas `.bash_profile`

     ```bash
     echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
     ```
   * Si usas `.zshrc`

     ```bash
     echo 'eval "$(rbenv init -)"' >> ~/.zshrc
     ```
2. Abrir `.bash_profile` o `.zshrc` y agregar la linea en el archivo usando el editor de preferencia:

   ```
   eval "$(rbenv init -)"
   ```

   Te sugerimos, reiniciar la shell para que se apliquen los cambios. Para comprobar que tenemos `rbenv` instalado correctamente, escribe en tu consola la siguiente linea:

   ```bash
   rbenv
   ```

   Debiera aparecer la versión de rbenv instalada y los comandos que hay para ejecutar.
3. Abrir `.bash_profile` o `.zshrc` y agregar la linea en el archivo usando el editor de preferencia:

   ```
   eval "$(rbenv init -)"
   ```

   Te sugerimos, reiniciar la shell para que se apliquen los cambios. Para comprobar que tenemos `rbenv` instalado correctamente, escribe en tu consola la siguiente linea:

   ```bash
   rbenv
   ```

   Debiera aparecer la versión de rbenv instalada y los comandos que hay para ejecutar.

### Posibles errores

Un posible error al instalar `rbenv` es no tener bien configurado el PATH de la shell.

## Windows

Para instalar `rbenv` con WSL2, sigue las instrucciones de Linux.

## Linux (Ubuntu)

```bash
# Instala rbenv
sudo apt update
sudo apt install rbenv -y
```

Luego debes cargar rbenv en tu shell para que puedas acceder a las diferentes versiones. Para esto debes agregar la siguiente linea en tu `.bashrc` o `.zshrc` (este archivo está normalmente en el directorio `$HOME`) dependiendo del shell que uses.

```bash
eval "$(rbenv init -)"
```

Puedes agregarlo con cualquier editor o hacerlo así (reemplaza `~/.bashrc` por `~/.zshrc` si usas zsh en lugar de bash):

```shell
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
```

Cierra la ventana del terminal y abre una nueva para que los cambios surjan efecto.

rbenv tiene varios [plugins](https://github.com/rbenv/rbenv/wiki/Plugins). Para instalarlos basta con dejarlos en `~/.rbenv/plugins/`. Recomendamos instalar los siguientes plugins: [`ruby-build`](https://github.com/rbenv/ruby-build), [`rbenv-vars`](https://github.com/rbenv/rbenv-vars), [`rbenv-aliases`](https://github.com/tpope/rbenv-aliases), [`rbenv-default-gems`](https://github.com/rbenv/rbenv-default-gems):

```bash
# ruby-build
git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build

# rbenv-vars
git clone https://github.com/rbenv/rbenv-vars.git $(rbenv root)/plugins/rbenv-vars

# rbenv-aliases
git clone https://github.com/tpope/rbenv-aliases.git $(rbenv root)/plugins/rbenv-aliases
# configuramos el alias automático (ver posibles errores)
rbenv alias --auto

# rbenv-default-gems
git clone https://github.com/rbenv/rbenv-default-gems.git $(rbenv root)/plugins/rbenv-default-gems
```

### Posibles errores

Para configurar los aliases automáticos del plugin `rbenv-aliases`, primero debemos instalar una versión de ruby utilizando `rbenv`. Si al ejecutar el comando `rbenv alias --auto` recibes un error relacionado a que la carpeta `versions` no existe, sigue con el último paso y luego ejecuta de nuevo el comando.

## Instalando versiones de ruby

Para instalar nuevas versiones de ruby, el plugin `ruby-build` nos permite usar el comando `rbenv install`:

```bash
# Actualizar las versiones de ruby disponibles para instalar
cd $(rbenv root)/plugins/ruby-build && git pull
# o en OSX
brew upgrade ruby-build

# Listar todos las versiones disponibles para instalar
rbenv install --list

# Instalar una version en particular
rbenv install 2.7.4
```

> Recomendación: Define alguna version de ruby que quieras para tener como global haciendo rbenv global 2.7.4 De esta manera no estarás usando la version de ruby que trae el sistema operativo. Esto hace que sea más seguro ya que no necesitas usar sudo para instalar gemas.

> Warning: Si no puedes usar rbenv o instalar gemas sin sudo, es muy probable que hayas dado los permisos equivocados en algún paso de la instalación. Esto no es seguro, por lo tanto te recomendamos eliminar todo y volver a hacer los pasos sin dar permisos de super usuario.

> TIP: Las versiones de ruby quedan instaladas en \~/.rbenv/versions.

> TIP: El plugin rbenv-default-gems tiene como objetivo instalar gemas automáticamente cuando instalas una nueva version de ruby. Para esto crea un archivo de texto \~/.rbenv/default-gems y agrega línea por línea el nombre de las gemas que quieres que se instalen. Buenos candidatos son bundler y rails.

### Ruby Aliases

Como estandar en Platanus usamos aliases para definir las versiones de ruby que utilizan los proyectos. De esta manera nos evitamos tener que actualizar los proyectos cada vez que instalas una nueva version de ruby. Esto nos ayuda en menos manteción y en menos uso de espacio en disco.

Los aliases son simplemente symbolic links de una version de ruby con un nombre en particular. Estos aliases podrían ser nombre arbitrarios, pero nosotros usamos la version de ruby sin el `patch`

Para manejar estos aliases, puedes usar el plugin de [rbenv-aliases](https://github.com/tpope/rbenv-aliases) que crea los alias automáticamente al instalar nuevas versiones de ruby.

```
# Listar los aliases
rbenv alias

2.2 => 2.2.7
2.3 => 2.3.4
2.4 => 2.4.1
```


# Docker

## OSX

Para instalar Docker en Mac encuentras las instrucciones [aquí](https://docs.docker.com/docker-for-mac/install/). Pon atención a la versión de procesador que tienes en tu Mac (Intel Chip o Apple Chip), porque las instrucciones cambian.

## Windows

### Instalación

1. Descarga [Docker Desktop](https://docs.docker.com/docker-for-windows/wsl/#download)
2. Abre Docker Desktop y
   1. en Settings > General activa "Use the WSL 2 based engine"
   2. en Settings > WSL Integration asegurate que todas las opciones estén activadas

      ![](/files/1sGcyk4Pwnm3HxSnsXvQ)

🛑 **Observación**: En el caso que tengas algún problema al momento usar Docker Desktop con WSL, [es importante dejar como por default la imagen de Ubuntu](https://docs.docker.com/desktop/windows/wsl/#enabling-docker-support-in-wsl-2-distros). Para poder realizarlo hay que hacer lo siguiente:

```bash
# Listamos las imagenes que tenemos en WSL
wsl.exe -l -v

# Poner por defecto la imagen que vamos a utilizar
wsl --set-default <distro-name>

# En el caso de que estés usando Ubuntu para WSL podrías utilizar:
wsl --set-default ubuntu
```

## Linux

Aquí encuentras las instrucciones para Ubuntu, pero si tienes otro SO basado en linux puedes buscar las instrucciones [aquí](https://docs.docker.com/engine/install/)

**Desinstalar las versiones antiguas de Docker**

```bash
sudo apt-get remove docker docker-engine docker.io containerd runc
```

> Nota: no hay problema si apt reporta que ninguna de esos paquetes estaba instalado.

Luego hay dos opciones para instalar Docker: desde repositorios oficiales o instalando los archivos manualmente.

En esta guía explicamos como instalar desde repositorios oficiales, que es más automatizado, y en [este link](https://docs.docker.com/engine/install/ubuntu/#install-from-a-package) encuentras las instrucciones para la instalación manual.

**Descargar versión estable de Docker**

```bash
sudo apt-get update
sudo apt-get install \\
   apt-transport-https \\
   ca-certificates \\
   curl \\
   gnupg \\
   lsb-release
```

Agrega la llave GPG oficial de docker

```bash
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
```

Con el siguiente comando configuramos el repositorio:

```bash
echo \\
"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \\
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
```

Nota: Este comando es específico a arquitecturas x86\_64 / amd64. Para otras arquitecturas (como armhf o arm64 puedes buscar el comando acá: <https://docs.docker.com/engine/install/ubuntu/>)

**Instalación de Docker**

```bash
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io
```

**Correr docker sin sudo**

Debemos hacer lo siguiente para poder usar Docker sin sudo:

Crea un grupo unix llamado `docker` y agrega tu usuario a este

```bash
sudo groupadd docker
```

```bash
sudo usermod -aG docker $USER
```

Corre el siguiente comando para activar los cambios:

```bash
newgrp docker
```

**Confirmar que quedó instalado**

```bash
docker run hello-world
```

Deberías ver algo así:

![](/files/CwZ5B83A5VPY4O5SYgiA)

Si no funciona intenta reiniciando tu computador.

**Instalar docker-compose**

Lo primero es revisar cuál es la última versión disponible en los [releases](https://github.com/docker/compose/releases) del repositorio oficial de docker-compose.

En la última actualización de esta guía, la última versión estable es la v2.14.2. Deberás reemplazar esta versión en la URL del siguiente comando:

```bash
sudo curl -L "https://github.com/docker/compose/releases/download/v2.14.2/docker-compose-"$(uname -s | tr '[:upper:]' '[:lower:]')"-$(uname -m)" -o /usr/local/bin/docker-compose
```

```bash
sudo chmod +x /usr/local/bin/docker-compose
```

Si quieres mas información sobre docker-compose la encuentras [aquí](https://docs.docker.com/compose/).


# Node

Para el uso de node utilizamos el manejador de versiones [nodenv](https://github.com/nodenv/nodenv) y algunos plugins. Siempre usamos la versión LTS, poniendo la `major` en el archivo `.node-version`. Es decir, se debe preferir escribir 10 en vez de 10.12.1.

Nodenv es realmente un clone de rbenv pero para node, por lo que funciona muy parecido. Toda la información de la sección de ruby aplica para node.

## OSX

### Instalación

```bash
# Instalar nodenv y node-build
brew install nodenv node-build
```

### TAPS

Taps son repositorios de donde brew puede buscar aplicaciones. Brew viene con el *tap* [homebrew-core](https://github.com/Homebrew/homebrew-core) incluido, pero se puede agregar más. En este caso tuvimos que agregar el tap de [nodenv](https://github.com/nodenv/homebrew-nodenv) para poder instalar los plugins de nodenv con brew.

### Agregar tap de nodenv

```bash
brew tap nodenv/nodenv
```

### Agregar plugins para nodenv

```bash
brew install nodenv-vars nodenv-aliases
```

Luego debes cargar nodenv en tu shell para que puedas acceder a las diferentes versiones. Para esto debes agregar la siguiente linea en tu `.bash_profile` o `.zshrc` dependiendo del shell que uses. Hay dos formas de hacerlo:

1. Ejecutar el siguiente, que agrega automáticamente la línea necesaria:
   * Si usas `.bash_profile`

     ```
     echo 'eval "$(nodenv init -)"' >> ~/.bash_profile
     ```
   * Si usas `.zshrc`

     ```
     echo 'eval "$(nodenv init -)"' >> ~/.zshrc
     ```
2. Abrir `.bash_profile` o `.zshrc` y agregar la linea en el archivo usando el editor de preferencia:

   ```bash
   eval "$(nodenv init -)"
   ```
3. Instalar yarn (`npm install -g yarn` o <https://github.com/pine/nodenv-yarn-install> para no tener que instalarlo a mano con cada versión de node)

## Windows

Para instalar `nodenv` con WSL2, sigue las instrucciones de Linux y en el paso 2 preocupate de usar el comando especial para WSL.

## Linux

Las instrucciones para instalar `nodenv` se obtuvieron del [repositorio oficial](https://github.com/nodenv/nodenv#basic-github-checkout) por si tienes alguna duda.

1. Clonar `nodenv`

   ```
   git clone https://github.com/nodenv/nodenv.git ~/.nodenv
   ```
2. Agrega `~/.nodenv/bin` a tu $PATH para usar los comando en la shell.
   * Si usas bash:

     ```bash
     echo 'export PATH="$HOME/.nodenv/bin:$PATH"' >> ~/.bash_profile
     ```
   * Si usas Zsh:

     ```bash
     echo 'export PATH="$HOME/.nodenv/bin:$PATH"' >> ~/.zshrc
     ```
   * Si usas Windows con WSL:

     ```bash
     echo 'export PATH="$HOME/.nodenv/bin:$PATH"' >> ~/.bashrc
     ```
3. Configura `nodenv` en tu shell

```bash
~/.nodenv/bin/nodenv init
```

Te deberá aparecer un mensaje similar a este, que se hará en el siguiente paso:

```bash
# Load nodenv automatically by appending
# the following to ~/.bashrc:
eval "$(nodenv init -)"
```

1. Añade `~/.nodenv/bin` a `$PATH`
   * En **Ubuntu 20.04** y **WSL2**:

     `$ echo 'export PATH="$HOME/.nodenv/bin:$PATH"' >> ~/.bashrc`
   * En **bash**:w

     `$ echo 'export PATH="$HOME/.nodenv/bin:$PATH"' >> ~/.bash_profile`
   * En **Zsh**:

     `$ echo 'export PATH="$HOME/.nodenv/bin:$PATH"' >> ~/.zshrc`
   * En **Fish shell**:

     `$ set -Ux fish_user_paths $HOME/.nodenv/bin $fish_user_paths`
2. Instalar **node build**

   ```shell
   # https://github.com/nodenv/node-build#installation
   # macOS
   $ brew install node-build

   # Cualquiera de las 2 opciones siguientes debería servir para Ubuntu 20.04+ y WSL2
   # As a nodenv plugin
   $ mkdir -p "$(nodenv root)"/plugins
   $ git clone https://github.com/nodenv/node-build.git "$(nodenv root)"/plugins/node-build

   # As a standalone program
   $ git clone https://github.com/nodenv/node-build.git
   $ PREFIX=/usr/local ./node-build/install.sh
   ```
3. Reinicia tu shell para que se apliquen todos los cambios.
4. Verifica que `nodenv` se instaló correctamente con el siguiente script llamado nodenv-doctor:

   ```bash
   curl -fsSL https://github.com/nodenv/nodenv-installer/raw/master/bin/nodenv-doctor | bash
   ```

   Con este script deberías ver algo así:

   ![](/files/3Sb1mZWXGaHMGaSaqC3V)

   Si se encuentra algún error de instalación, refiere a la documentación oficial.
5. Instalar plugins necesarios (es posible que ya se te hayan instalado algunos)
   * [nodenv-vars](https://github.com/nodenv/nodenv-vars#installation):

     ```bash
      mkdir -p $(nodenv root)/plugins
      cd $(nodenv root)/plugins
      git clone https://github.com/nodenv/nodenv-vars.git
     ```
   * [nodenv-aliases](https://github.com/nodenv/nodenv-aliases#installation):

     ```bash
     git clone https://github.com/nodenv/nodenv-aliases.git $(nodenv root)/plugins/nodenv-aliases
     ```
   * [node-build](https://github.com/nodenv/node-build#installation):

     ```bash
     mkdir -p "$(nodenv root)"/plugins
     git clone https://github.com/nodenv/node-build.git "$(nodenv root)"/plugins/node-build
     ```
   * Instalar yarn (npm install -g yarn o <https://github.com/pine/nodenv-yarn-install> para no tener que instalarlo a mano con cada versión de node)

## Instalando versiones de node

Para instalar nuevas versiones de node:

```bash
# Actualizar las versiones de node disponibles para instalar
cd $(nodenv root)/plugins/node-build && git pull
# o en OSX
brew upgrade node-build

# Listar todos las versiones disponibles para instalar
nodenv install --list

# Instalar una version en particular
nodenv install 12.19.1

# Establecer una version global de node
nodenv global 12.19.1
```


# Herramientas

En esta sección podrás ver todas las herramientas que utilizamos día a día en Platanus. Encontrarás tips, plugins, configuraciones, etc.

[Linters](/setup/configuracion_de_tu_entorno_local/herramientas/linters)

[Editores](/setup/configuracion_de_tu_entorno_local/herramientas/editores)

[Git](/setup/configuracion_de_tu_entorno_local/herramientas/git)


# Linters

## Linters

Usamos `rubocop` para Ruby, `eslint` para JS y `stylelint` para CSS. Actualmente incluimos estos linters como dependencias de desarrollo en nuestros proyectos tanto en el `package.json` como el `Gemfile`. Debido a esto, no se necesita instalar nada aparte y basta con correr `yarn install` y `bundle install`.

Además de los linters, cada proyecto incluye archivos con sus reglas.

> En el Gemfile verás que la versión de rubocop está restringida. Hay que tener cuidado al modificarla ya que hasta hace un tiempo no seguían Semantic Versioning y es puede pasar que un cambio en el minor introduzca breaking changes como cambio de nombre de alguna regla que podría estar definida en el proyecto.

### Tips

* Antes de hacer un *commit* recuerda revisar los warnings de los linters
* Deberías fijarte solo en los warnings sobre el código que estás tocando, no es necesario corregir todos los problemas que tenga un archivo antes de agregar una feature en este
* Si se debe arreglar un warning puntual sobre un código de tú rama que ya se *commiteo* una opción es hacer el cambio con un `rebase` o `fixup`, cambiando el commit en el que se introdujo el problema. Esto para dejar la historia más limpia evitando los commits del tipo `style(): fix linter warnings`

### Plugins

Para correr los linters de manera más cómoda, podemos instalar los siguientes plugins:

### VScode

* [Ruby](https://marketplace.visualstudio.com/items?itemName=Shopify.ruby-lsp)
  * Puedes hacer que al guardar un archivo `.rb` se autocorrijan la mayoría de las infracciones. Para que esto funcione, debes incluir lo siguiente en el `settings.json` de tu VSCode (hint: para abrirlo presiona `cmd+shift+p` y busca el comando "Preferences: Open Settings (JSON)):

    ```json
    "[ruby]": {
      "editor.formatOnSave": true,
      "editor.defaultFormatter": "Shopify.ruby-lsp",
    },
    ```
* [ES](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
  * Puedes hacer que al guardar un archivo `.js`/ `.vue` se autocorrijan la mayoría de las infracciones, incluyendo el orden de las clases de tailwind si el proyecto incluye el plugin correspondiente. Para que esto funcione, debes incluir lo siguiente en el `settings.json` de tu VSCode (hint: para abrirlo presiona `cmd+shift+p` y busca el comando "Preferences: Open Settings (JSON)):

    ```json
    "editor.codeActionsOnSave": {
      "source.fixAll.eslint": true
    },
    ```
* [stylelint](https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint)

### Sublime

* [Linter](https://github.com/SublimeLinter/SublimeLinter3)
* [Ruby](https://github.com/SublimeLinter/SublimeLinter-rubocop)
* [ES](https://github.com/roadhump/SublimeLinter-eslint)
* [stylelint](https://github.com/SublimeLinter/SublimeLinter-stylelint)

### Vim

Configurar los linters para que funcionen solo con vim puede ser un poco enredado. Una alternativa es usar VSCode con sus extensiones de linters y [esta extensión de Vim](https://github.com/VSCodeVim/Vim), alcanzando así un punto medio entre la experiencia de uso de un editor más moderno y las *features* de vim.


# Editores

[IDE/Editores de Código](/setup/configuracion_de_tu_entorno_local/herramientas/editores/ide_editores_de_codigo)


# IDE/Editores de Código

[Visual Studio Code](/setup/configuracion_de_tu_entorno_local/herramientas/editores/ide_editores_de_codigo/visual_studio_code)

[Sublime Text](/setup/configuracion_de_tu_entorno_local/herramientas/editores/ide_editores_de_codigo/sublime_text)


# Visual Studio Code

Poco a poco Visual Studio Code ha superado a Sublime como el editor de texto más usado en Platanus.

## Extensiones

### Relacionadas al stack

* [Ruby](https://marketplace.visualstudio.com/items?itemName=Shopify.ruby-lsp)
* [Ruby Solargraph](https://marketplace.visualstudio.com/items?itemName=castwide.solargraph)
* Vue: qué extensión usar depende de la versión de Vue del proyecto. Activa una de estas dos:
  * [Volar](https://marketplace.visualstudio.com/items?itemName=vue.volar) y [Sublime Text](/setup/configuracion_de_tu_entorno_local/herramientas/editores/ide_editores_de_codigo/sublime_text) para Vue 3
  * [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur) para proyectos en Vue 2
* [Vue 3 Snippets](https://marketplace.visualstudio.com/items?itemName=hollowtree.vue-snippets)
* [Vue Peek](https://marketplace.visualstudio.com/items?itemName=dariofuzinato.vue-peek)
* [Jest](https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest)
* [MJML](https://marketplace.visualstudio.com/items?itemName=attilabuti.vscode-mjml)
* [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig): los proyectos vienen con un `.editorconfig` que nos ayuda a tener algunas configuraciones básicas consistentes. Este plugin se encarga de leerlo y aplicar esas configuraciones a tu editor
* [Expo Tools](https://marketplace.visualstudio.com/items?itemName=byCedric.vscode-expo): esta herramienta provee autocompletado para los archivos de configuración en proyectos mobile con Expo
* [Tailwind Intellisense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)
* [Tailwind Docs](https://marketplace.visualstudio.com/items?itemName=austenc.tailwind-docs)

### Auxiliares

* [Git Lens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens)
* [Trailing Spaces](https://marketplace.visualstudio.com/items?itemName=shardulm94.trailing-spaces)
* [Project Manager](https://marketplace.visualstudio.com/items?itemName=alefragnani.project-manager)
* [Liveshare](https://marketplace.visualstudio.com/items?itemName=MS-vsliveshare.vsliveshare-pack)
* [Error Lens](https://marketplace.visualstudio.com/items?itemName=usernamehw.errorlens)
* [Highlight Matching Tag](https://marketplace.visualstudio.com/items?itemName=vincaslt.highlight-matching-tag)
* [Color Highlight](https://marketplace.visualstudio.com/items?itemName=naumovs.color-highlight)
* [DotENV](https://marketplace.visualstudio.com/items?itemName=mikestead.dotenv)
* [file-icons](https://marketplace.visualstudio.com/items?itemName=file-icons.file-icons)
* [Markdown All in One](https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one)
* [Rainbow CSV](https://marketplace.visualstudio.com/items?itemName=mechatroner.rainbow-csv)
* [indent-rainbow](https://marketplace.visualstudio.com/items?itemName=oderwat.indent-rainbow)
* [Open in Github](https://marketplace.visualstudio.com/items?itemName=sysoev.vscode-open-in-github)
* [String Manipulation](https://marketplace.visualstudio.com/items?itemName=marclipovsky.string-manipulation)

Si no lo has hecho todavía, revisa [la sección de linters](https://www.notion.so/linters.md).

### OSX

### Comandos más usados

* `cmd + p`: Busca cualquier archivo del workspace abierto
* `cmd + shift + p`: Busca cualquier comando disponible y lo ejecuta

Para más información, mirar aquí: <https://code.visualstudio.com/docs/getstarted/keybindings>

### Windows

<https://code.visualstudio.com/shortcuts/keyboard-shortcuts-windows.pdf>

```
### WSL 2

Se puede conectar de manera "nativa" a WSL 2, sin tener que preocuparse de paths, compatibilidad de extensiones o performance, usando la extensión [Remote-WSL](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl).
```

### Linux

<https://code.visualstudio.com/shortcuts/keyboard-shortcuts-macos.pdf>


# Sublime Text

## Extensiones

### Genéricas

* [**EditorConfig**](https://github.com/sindresorhus/editorconfig-sublime): Nos ayuda a mantener un estilo de código (espacios o tabs, sangrado, etc) consistente en los proyectos de Platanus.
* [**SublimeLinter**](http://www.sublimelinter.com/en/latest/): Framework para aplicar linting al código.
* [**GitSavvy**](https://github.com/divmain/GitSavvy): Interfaz para administrar git desde el editor. Muy útil para agregar solo *hunks* (partes de los cambios de un archivo) a los commits, manejar ramas, hacer rebase, diffs, etc.
* [**SideBarEnhancements**](https://github.com/titoBouzout/SideBarEnhancements): Agrega múltiples funciones para manejar archivos y carpetas a la sidebar.
* [**Unicode Character Highlighter**](https://packagecontrol.io/packages/Unicode%20Character%20Highlighter): Destaca caracteres como el [espacio duro](https://es.wikipedia.org/wiki/Espacio_duro) que OSX inserta al usar `⌥` + `ESPACIO` para hacer más facil su eliminación.

  ![](/files/Aspo6dalrko2bEvhAwCP)
* [**DocBlockr**](https://github.com/spadgos/sublime-jsdocs): Hace más fácil la creación de comentarios. [Ejemplos](https://github.com/spadgos/sublime-jsdocs#docblock-completion).
* [**Aligntab**](https://github.com/randy3k/AlignTab): Ayuda a alinear variables.

  ![](/files/7EbRSdUJauUSMiUiXVtO)
* [**GitGutter**](https://github.com/jisaacks/GitGutter): Muestra las lineas que han sido editadas con respecto al último commit de git.
* [**GitHubinator**](https://github.com/ehamiter/GitHubinator): Abre la linea seleccionada en GitHub.
* [**MarkdownEditing**](https://packagecontrol.io/packages/MarkdownEditing): Agrega soporte Markdown a Sublime Text.

### HTML/CSS

* [**Emmet**](https://github.com/sergeche/emmet-sublime): Permite escribir HTML y CSS mediante abreviaciones.

  ![](/files/jbA9nXVmedK9E1e6CFJ5)
* [**SCSS**](https://packagecontrol.io/packages/SCSS) y [**Sass**](https://packagecontrol.io/packages/Sass): Agregan soporte para Sass.
* [**Color Highlighter**](https://github.com/Monnoroch/ColorHighlighter): Permite previsualizar los colores en el CSS.

  ![](/files/5zcYosTawlDCvqn9EKhz)
* [**AutoPrefixer**](https://github.com/sindresorhus/sublime-autoprefixer): Agrega automáticamente prefijos propietarios a propiedades de CSS.

  ![](/files/11Q2oJ7VFczIsgFYtg62)

### JavaScript

* [**SublimeLinter-eslint**](https://github.com/SublimeLinter/SublimeLinter-eslint): Hace linting directamente en el editor usando ESlint (y archivos `.eslintrc.json`).
* [**AngularJS**](https://github.com/angular-ui/AngularJS-sublime-package): Agrega "ir a definición", autocompletado de funciones, entre otras cosas.
* [**AngularJS Snippets (John Papa)**](http://www.johnpapa.net/angularjs-snippets-for-sublime-visual-studio-and-webstorm/): Agrega snippets basados en la [guía de estilo](https://github.com/johnpapa/angular-styleguide) para AngularJs de John Papa.

### OSX

### Atajos de teclado útiles:

* `⌘`+`d` permite duplicar la selección actual creando cursores múltiples

  ![](/files/GYNfRNZyMLtr3dKFu3cZ)
* `CTRL`+`⌘`+`g` busca la selección actual en todo el documento.

  ![](/files/VIN77dr4jBOX9JcGIQFz)
* `⌘`+`⇧`+`l` separa la seleccion actual en lineas independientes

  ![](/files/9QUjZs6jOwXSQo1RTCvv)
* `⌘`+`j` une la selección en una sola linea.

  ![](/files/FGKU9yG3cqoM8nqOFcbH)

### Windows

TODO

### Linux

TODO


# Git

Utilizamos [git](https://git-scm.com/) para el control de versiones junto con [github](https://github.com/platanus)

Configuralo para que se pueda autenticar con GitHub y para tener permisos en los repositorios de Platanus:

* Configura tu username en Git para que los commits tengan el autor y commiter correcto [Github Guide](https://help.github.com/articles/setting-your-username-in-git/)
* Configura Git para que guarde en el Keychain tu password, de esta manera no te lo preguntará cada vez que haces pull o push. [Github Guide](https://help.github.com/articles/caching-your-github-password-in-git/)

> TIP: Siempre clona los repositorios usando https y no ssh. De esta manera usaras el puerto 80 que esta abierto en todas partes, me ha pasado mas de una vez que en cafés o coworks esta cerrado el puerto 22.

```
> NOTE: Alternativamente se puede usar un VPN, como Tailscale para circunventar este tipo de situaciones ([https://tailscale.com/](https://tailscale.com/): La instalación no dura más de 10 minutos).
```


# Configuración de proyectos

[Getting Started](/setup/configuracion_de_proyectos/getting_started)

[Heroku](/setup/configuracion_de_proyectos/heroku)

[Rails](/setup/configuracion_de_proyectos/rails)

[Circle CI](/setup/configuracion_de_proyectos/circle_ci)

[Vue](/setup/configuracion_de_proyectos/vue)

[Apple App Store](/setup/configuracion_de_proyectos/apple_app_store)

[Google Play](/setup/configuracion_de_proyectos/google_play)

[Expo](/setup/configuracion_de_proyectos/expo)

[S3](/setup/configuracion_de_proyectos/s3)

[Git](/setup/configuracion_de_proyectos/git)

[Cloudflare](/setup/configuracion_de_proyectos/cloudflare)

[Sendgrid](/setup/configuracion_de_proyectos/sendgrid)

[Dominio + Mailing](/setup/configuracion_de_proyectos/dominio_mailing)

[Google Tag Manager, Analytics, Search Console, etc.](/setup/configuracion_de_proyectos/google_tag_manager_analytics_search_console_etc)

[Crear un bucket de S3](/setup/configuracion_de_proyectos/crear_un_bucket_de_s3)

[SlackBot](/setup/configuracion_de_proyectos/slackbot)

[Google BigQuery](https://github.com/platanus/la-guia/blob/master/setup/configuracion_de_proyectos/google_bigquery.md)


# Getting Started

> Soy nuev@ y ya tengo asignado mi primer pitch! Qué tengo que hacer para levantar el proyecto y tener todo listo para ponerme a programar?

Esto es lo que queremos responder con esta sección:

1. Asegúrate de haber revisado la sección de configuración local y que tengas andando tu ambiente con todo instalado (node, ruby, docker, plugins de tu editor, etc).
2. Clona el repositorio y muevete a la nueva carpeta:

   ```bash
   git clone https://github.com/platanus/<project-name>.git
   cd project-name
   ```
3. Corre `bin/setup`. Esto te deja instaladas las gemas y paquetes que necesite el proyecto, además de correr el setup de la base de datos. Puedes revisar el archivo para ver qué exactamente se está corriendo
   * A veces puede salir el error `Error: remote staging not found in git remotes` después de correr `bin/setup`. Si te aparece, probablemente es porque no tienes acceso al heroku del proyecto. Pídele acceso al encargado del proyecto y cuando lo tengas corre `bin/setup_heroku` para reintentar el paso que falló
4. Para no empezar de 0 con una base de datos vacía, los proyectos tienen un `Makefile` con un par de comandos útiles para traernos datos de staging:
   * Puedes correr `make backup-staging` para generar un nuevo backup en la base de datos de staging. Esto nos asegura que tengamos los datos más actualizados de staging
   * `make restore-from-staging` toma el último backup de staging y lo copia en tu base de datos local
5. Si todo salió bien, con esto deberías estar listo para correr el proyecto. Corre los siguientes comandos en paralelo en pestañas separadas:
   * `bin/rails s`: levanta el servidor. Si vas a `localhost:3000` en el navegador verías la página
   * `bin/webpack-dev-server` , `bin/webpacker-dev-server` en proyectos más nuevos, o `bin/vite dev` en proyector aún más nuevos: permite que cada vez que se guarde un archivo js/vue, se recargue la página automáticamente

     > 💡 Puedes configurarte un alias para no tener que pensar en cual de los tres comando usar: `alias bds="bin/webpack-dev-server || bin/webpacker-dev-server || bin/vite dev"`
   * `bundle exec guard`: cada vez que guardas un archivo ruby se ejecutan los tests correspondientes a ese archivo. Alternativamente, puedes correr todos los tests de manera manual usando `bin/rspec`
     * Si quieres correr un `it`, `context` o `describe` en particular, ignorando otros archivos y los demás ejemplos en el mismo archivo, puedes agregar una `f` al comienzo de este. Esto funciona tanto para `guard` como para `rspec`. Esto es solo posible gracias a [filter\_run\_when\_matching](https://relishapp.com/rspec/rspec-core/v/3-6/docs/filtering/filter-run-when-matching), por lo que debe estar configurado en el proyecto para poder usarlo. Recuerda borrar la `f` antes de hacer el PR!
   * `bin/rails c`: abre la consola de rails. En ella puedes probar cosas, por ejemplo, buscar o crear records. Puedes correr cualquier código Ruby/Rails, llamar a modelos/jobs/clients definidos en el proyecto, etc. No es estrictamente necesario, pero puede ser muy útil

> 💡 Es posible que haciendo el setup te encuentres con un error como el siguiente:

````
> gyp verb 'which' failed Error: not found: python2

Si te ocurre, puede que tengas `yarn` instalado con `brew`, y en ese caso no se le puede indicar que use `python2`. Para solucionarlo, puedes instalar yarn a través de `npm` corriendo lo siguiente:

```bash
npm config set python /usr/bin/python
brew uninstall yarn
npm install -g yarn
nodenv rehash
```
````

Ahora, cada vez que quieras levantar o volver a trabajar en el proyecto, puede que tengas que hacer alguna de estas cosas:

1. Debes asegurarte de tener la DB corriendo. En Platanus tenemos las bases de datos de nuestros proyectos dockeridas. Esto quiere decir que no corre en el `postgres` que tengas directamente instalado en tu computador, si no que corre en un postgres que está dentro de un container de docker. Para correr el container debes usar el comando `docker-compose up -d`
   * Este paso no lo tuviste que hacer en el setup inicial explícitamente ya que está incluído dentro de las cosas que hace el `bin/setup`
   * Para ver cuáles containers están prendidos, puedes correr `docker container ls`. Si quieres una alternativa más "visual" puedes usar [Captain](https://getcaptain.co/) en OSX
2. Si alguien agregó cambios nuevos a master, es bueno traerlos frecuentemente a tu rama, así se resuelven periódicamente los conflictos que puedan aparecer. Para esto, usa rebase. Corre **en tu rama** `git pull origin master` para traerte los últimos cambios, y luego `git rebase -i master`. Esto te mostrará los commits que has agregado en tu rama y que quedarían sobre los de master. Si se encuentra un conflicto, el rebase para en el commit que lo contiene y te deja corregirlo antes de indicarle que siga
3. Si alguien más está trabajando en el proyecto, puede que se hayan agregado nuevas gemas o paquetes. Para eso tendrías que correr `bundle install` y/o `yarn install`
4. Si alguien más está trabajando en el proyecto, puede que se hayan agregado nuevas migraciones. Para eso tendrías que correr `bundle exec rails db:migrate:with_data`. Si quieres saber por qué el `with_data` puedes ver la sección de:

   [Data Migrate](https://github.com/platanus/la-guia/blob/master/setup/configuracion_de_proyectos/stack/ruby_rails/data_migrate.md)
5. Si hiciste un PR y te pidieron cambios, por lo general aplicamos esos cambios usando rebase. [En este post de nuestro blog](https://plata.news/blog/manteniendo-la-historia-limpia-usando-git-rebase/) tenemos más info sobre rebase y cómo lo usamos para mantener la historia limpia. También revisa [este post](https://fle.github.io/git-tip-keep-your-branch-clean-with-fixup-and-autosquash.html) sobre fixup, otra herramienta del rebase que usamos para esto
6. Si los puntos anteriores no aplican y solo quieres volver a levantar el proyecto, basta con que repitas el paso 5 anterior (`bin/rails s`, `bin/webpack-dev-server` y `bundle exec guard`), no debes correr el `bin/setup` de nuevo ni nada


# Heroku

Heroku es el servicio que usamos para "hostear" la aplicación web y todos los servicios extras que necesitemos (Base de Datos, repositorio de archivos, etc...). Acá estará el admin, el API y la página de cara a los clientes.

## Crear una cuenta

Para crear una cuenta debes ingresar a <https://signup.heroku.com/>

* Ingresar los datos en el formulario (en la pregunta Primary Development language elegir Ruby)
* Revisar email de confirmación
* Activar cuenta haciendo click en el link de activación en el email
* Elegir un password

A continuacion te recomendamos [crear un team](https://www.notion.so/platanus/Heroku-9cd0be0ca6994ca386733c99d0e53ce3#crear-un-team) para dar acceso a los desarrolladores de platanus.

## Team

Los teams en Heroku nos dan mayor flexibilidad para administrar las aplicaciones.

## Crear un team

Para crear un team debemos ingresar a [heroku](https://id.heroku.com/login).

* Hacer login
* Click en *Personal apps*
* Click en *Create Team*

![](/files/vr0x7dBxf5pOcatWADQp)

A continuación deberás asignar un nombre al team y agregar la tarjeta de crédito con la que se pagará la cuenta de Heroku asociada las aplicaciones del team.

1. Seleccionar un nombre.
2. Agregar tarjeta de crédito
3. Crear Team

Luego de esto vas a poder seleccionar el team haciendo click en *Personal apps*

![](/files/8WH6xKmsNP6Hxd1zYLWM)

### Agregar miembros a un team

Para poder administrar las aplicaciones del team, es necesario dar persmisos de administración desde el [dashboard](https://dashboard.heroku.com/)

* Seleccionar el team
* Ir al tab *Access*
* Click en botón *Invite Member*
* Agregar el mail <heroku@platan.us>
* Seleccionar *admin* el la opción *Role*
* Click en *Save changes*

![](/files/lOAiPiE7GlQEYRLkLSag)

### Transferir una app a un team

Una vez creado el team, puedes transferir una aplicación existente al team, para que pueda ser administrada por sus miembros.

* Seleccionar la aplicación a transferir
* Ir al tab *settings*
* Scroll hasta la sección *Transfer Ownership*
* Seleccionar el team debajo el encabezado *Teams & Organizations*
* Click en el botón *Transfer*
* Confirmar haciendo click en el botón *Transfer* del diálogo de confirmación.

![](/files/bdSslcmQG0MDtEwvBuxI)


# Rails

## Crear un proyecto nuevo

Para empezar un projecto nuevo, asegurate de tener instalada la última version de [potassium](https://github.com/platanus/potassium)

```bash
gem install potassium
```

Luego crea un nuevo proyecto con potassium contestando las preguntas y finalmente corre el script de setup:

```bash
potassium create <project-name>
```

### Proyecto existente

Si vas a empezar a trabajar en un proyecto Rails que alguien ya creó.

```bash
git clone platanus/<project-name>
cd <project-name>
bin/setup
```

Si necesita agregar funcionalidad a un proyecto existente puedes usar el command `install <recipe>` de potassium

```bash
cd <project-name>
potassium install pundit
```

### Manejo de variables de entorno

Los proyectos siguen el patrón [12 factor](http://12factor.net/config) para configurar la aplicación en diferentes ambientes sin tener que cambiar el código.

En el caso de nuestro ambiente de **desarrollo y testing** no queremos tener keys de diferentes proyectos en nuestro ambiente, es por eso que usamos la gema `dotenv` para leer y setear variables de ambiente definidas en archivos al cargar nuestro proyecto.

El objetivo es que al clonar la app podamos correr los test y que estos pasen sin tener que configurar nada.

* `.env.development`: En este archivo pondremos todas las variables que son necesarias para que la aplicación corra. Este archivo estara versionado junto con el proyecto, por lo que nunca pondremos valores sensibles como tokens, **API keys, passwords**, etc. En cambio podemos poner valores de referencia para que la aplicación pueda partir.

  ```
  AWS_ACCESS_KEY=aws_access_key
  AWS_SECRET_ACCESS_KEY=aws_secret_access_key
  CHECK_THRESHOLD=10
  ```
* `.env.test`: En este archivo, si es necesario, pondremos las variables que deben ser diferentes en el ambiente de testing.
* `.env.local`: En este archivo pondremos todas las variables que tienen valores sensibles y que usaremos en nuestra máquina local para desarrollar. Este archivo esta listado en el `.gitignore`, por lo que no será versionado con el proyecto.

  ```
  AWS_ACCESS_KEY=real_aws_access_key
  AWS_SECRET_ACCESS_KEY=real_aws_secret_access_key
  ```

### ¿ `ENV[]` o `ENV.fetch()`?

Podemos usar `ENV.fetch()` cuando queremos que la app lance una exception si esa variable no está seteada o si queremos asignar un valor por defecto usando el segundo paramentro.

```ruby
ENV.fetch('API_KEY') # Really important variable would trigger a keyError exception
ENV.fetch('CHANGE_THRESHOLD', 10) # Default value variable
```

> Nota: En desarrollo el uso de fetch sin un valor por defecto se va a gatillar sólo cuando no hemos agregado ese key al archivo .env.development, esto es un buen recordatorio de esta práctica de documentación. En ambientes de producción no se usa el archivo .env.development por lo que el uso de fetch nos ayudará a encontrar errores por falta de configuracion.

### ¿ `RACK_ENV` o `RAILS_ENV`?

Al crear la aplicación en heroku, se setea la variable `RACK_ENV=production` para declarar que usaremos el environment `production` de la aplicación. No es necesario usar `RAILS_ENV` ya que cuando se llama a `Rails.env` este hace fallback a RACK\_ENV cuando RAILS\_ENV no esta seteado.

Para que esta convención funcione, se debe usar el metodo `Rails.env` para chequear el ambiente.

### Heroku

En el caso de ambientes deployados en heroku, como `staging` y `production` estas variables son seteadas usando el CLI de heroku.

```bash
heroku config
heroku config:set KEY=value
heroku config:unset KEY
```

Esto hace que los keys seteados esten en el ambiente de heroku, por lo que la app no va a tener problemas al tratar de obtenerlas usando `ENV[] o ENV.fetch()`

> Nota: El environment de deployment es diferente al environment de la aplicación. Para publicaren heroku, siempre usamos el environment production de la aplicacion, ya sea para deployar a staging o production.

> Nota: La aplicación en heroku no usará los valores que estan en .env.development


# Circle CI

Ocupamos [CircleCI](https://circleci.com/) para el linting y testing de nuestros PRs. Cuando creamos un nuevo proyecto con Potassium la configuración de circleci ya está en `.circleci/config.yml` pero tenemos que decirle a CircleCI que tenemos un nuevo proyecto que integrar. Para esto debemos entrar al [CircleCI de Platanus](https://circleci.com/add-projects/gh/platanus): Una vez logueado, buscar repositorio y presionar `Follow Project`.

Por defecto Circle CI intenta adivinar qué es necesario para correr los tests pero hay excepciones para las cuales es necesario agregar un archivo de configuración `circle.yml`:

### Variables de Entorno

Para realizar deploy en ciertos ambientes hay que agregar información a las variables de entorno, como nombres de usuario o API keys.

1. Buscar ⚙️ en el proyecto seleccionado.
2. Seleccionar `Environment Variables`
3. Seleccionar `Add Variable`


# Vue

Gracias a Webpacker y [Potassium](https://github.com/platanus/potassium) es muy fácil empezar a usar Vue junto con un proyecto nuevo de Rails.

```
> potassium create PROYECTO_DE_RAILS
[...]
Which front-end framework are you going to use?:
‣ Vue
  Angular
  None
```

En el caso que tengas que agregar Vue a un proyecto Rails 5 que ya fue inicializado, puedes usar webpacker para configurarlo:

```bash
bundle exec rails webpacker:install:vue
```

En Rails 5.2+ hay que configurar el ambiente de desarrollo con lo siguiente:

`config/initializers/content_security_policy.rb`

```
Rails.application.config.content_security_policy do |policy|
  if Rails.env.development?
    policy.connect_src :self, :https, 'http://localhost:3035', 'ws://localhost:3035'
    policy.script_src :self, :https, :unsafe_eval
  else
    policy.script_src :self, :https
  end
end
```

Una vez instalado, la estructura inicial del proyecto será la siguiente:

```
app/javascript:
  ├── packs:
  │   └── application.js
  └── app.vue
```

```
views/layouts/application.html.erb

<%= javascript_pack_tag 'application' %>
<%= stylesheet_pack_tag 'application' %>
```

Webpacker funciona con *packs*, siendo cada "pack" un bundle de código independiente generado por webpack. En este caso, agregando las tags de arriba la aplicación de Rails va a tener acceso al compilado de todo lo importado en `application.js` y el `scss` generado a partir de los archivos `.vue`.

En un proyecto más avanzado esta sería una estructura ejemplo:

```
app/javascript:
  ├── main
  │   ├── api
  │   │   └── form-api.js
  │   ├── components:
  │   │   ├── sidebar.vue
  │   │   ├── main.vue
  │   │   └── footer.vue
  ├── shop
  │   ├── api
  │   │   └── cart-api.js
  │   ├── components:
  │   │   ├── cart.vue
  │   │   └── cart.spec.js
  │   ├── store:
  │   │   └── index.js
  │   │   └── modules
  │   │       ├── cart.js
  │   │       └── cart.spec.js
  └── packs:
      ├── main.js
      └── shop.js
```

```
views/layouts/application.html.erb

<%= javascript_pack_tag 'main' %>
<%= stylesheet_pack_tag 'main' %>
```

```
views/layouts/shop.html.erb

<%= javascript_pack_tag 'shop' %>
<%= stylesheet_pack_tag 'shop' %>
```


# Apple App Store

Toda la administración de aplicaciones iOS se hace en el portal iTunnes Connect de Apple.

## Crear una cuenta de desarrollador

Este link mantiene una guía actualizada de cómo crear una cuenta para desarrolladores.

<https://help.moreapp.com/es/support/solutions/articles/13000025259-crear-cuenta-de-desarrollador-de-ios>

## Invitar a usuarios a ser parte del equipo de desarrollo

* Ingresar a [Developer Account](https://developer.apple.com/account)
* Iniciar sesión con el usuario dueño de la cuenta o que tenga permisos de admin
* Selecciona el la opción *Personas* en el menú de la izquierda
* Haz click en el boton *Invitar*
* Agrega los emails de las personas que quieres que tengan acceso.
* Invita a <ios-dev@platan.us> como administrador

## Ingresar a iTunnes Connect

* Ingresar a [iTunnes Connect](https://itunesconnect.apple.com/)
* Iniciar sesión el usuario que tiene permisos para la aplicación en cuestión
* Seleccionar el icono *My Apps*
* Seleccionar la aplicación de la lista.


# Google Play

Toda la administración de aplicaciónes Android se hace en el portal de desarrolladores de Google (Play Console).

### Crear una cuenta de desarrollador

Aquí hay un recurso oficial con la información

<https://support.google.com/googleplay/android-developer/answer/6112435?hl=es-419>

### Como agregar usuarios a la cuenta

Para agregar a un usuario del equipo platanus para poder administrar la cuenta de google play y publicar aplicaciones, puedes seguir los pasos en el siguiente recurso oficial:

[https://support.google.com/googleplay/android-developer/answer/2528691?es-419](https://support.google.com/googleplay/android-developer/answer/2528691?es-419=)

### Ingresar al Developer Console

* Ingresar al [Developer Console](https://play.google.com/apps/publish) de Google Play
* Iniciar sesión con el usuario que tiene permisos para la aplicación en cuestión.
* Seleccionar la aplicación de la lista.


# Expo

Todo el setup requerido se puede encontrar [en la documentación](https://docs.expo.io/get-started/installation/), de todas maneras aquí hay un resumen. Para poder desarrollar con Expo y React Native debes tener Node instalado. Se recomienda usar una versión LTS de Node (las versiones con números pares). Se recomieda usar [Nodenv](https://la-guia.platan.us/setup/configuracion_de_tu_entorno_local/tecnologias/node).

Además de esto, necesitas un dispositivo en el que correr tu proyecto. Tienes dos opciones:

* Ocupar un dispositivo físico, para lo que tendrás que descargar la aplicación [Expo Go](https://expo.dev/client).
* Ocupar un emulador. Si tienes un computador Apple, entonces al instalar Xcode ya instalaste el simulador de iOS. Si tienes otro computador o si quieres probar con un emulador Android en tu Mac, puedes ocupar [Android Studio](https://developer.android.com/studio). Si tienes cualquier problema puedes revisar la instalación detallada de [Apple aquí](https://docs.expo.io/workflow/ios-simulator/) o la de [Android Studio aquí](https://docs.expo.io/workflow/android-studio-emulator/).

Para correr el proyecto, ejecuta el comando

```bash
yarn start
```

o el comando

```bash
npx expo start
```

Cualquiera de los comando anteriores levantará un servidor que te permitirá correr tu aplicación mientras desarrollas. Se abrirá una página en tu explorador desde el cual podrás iniciar los emuladores que tengas instalados y mostrará un QR para poder escanearlo si es que quieres ocupar un dispositivo físico. Para iOS debes escanearlo con la cámara y para Android se hace en la misma aplicación Expo Go.

## Crear un proyecto nuevo

Para crear un proyecto nuevo con la plantilla inicial de Typescript usa el comando

```bash
npx create-expo-app <project-name> --template expo-template-blank-typescript
```

## Proyecto existente

Si vas a empezar a trabajar en un proyecto que alguien ya creó.

```bash
git clone platanus/<project-name>
cd <project-name>
yarn install
```

### Conectar la aplicación con el servidor local

Es muy probable que al desarrollar necesites conectar la aplicación con el servidor de rails que estás corriendo en tu computador. Para poder hacer esto, tienes que correr el servidor con el siguiente comando

```bash
bundle exec rails s -b 0.0.0.0
```

Luego, puedes acceder al servidor desde la aplicación ocupando [la dirección IP interna de tu computador](https://lifehacker.com/how-to-find-your-local-and-external-ip-address-5833108). El servidor debería encontrarse en `http://<ip-interna>:3000`.


# S3

S3 es el servicio que usamos en staging y producción para el almacenamiento de archivos. La gema [Shrine](https://github.com/shrinerb/shrine) se encarga de la comunicación entre S3 y nuestra app Rails.

## Inicio de un proyecto

Típicamente al principio de un proyecto no necesitamos subir archivos. Si tratamos de deployar con la configuración tal cuál viene de Potassium probablemente se caiga debido a que no tenemos las variables de ambiente correspondientes. Una solución temporal a esto es comentar la sección de la configuración de shrine en `shrine.rb` para producción y hacer que `production` tenga la misma configuración que `development`. Con esto no funcionarían la subida de archivos en Heroku hasta que se configure un bucket S3 y se devuelva la configuración de shrine a su estado anterior.

## Asociando un bucket de S3

Cuando llega el momento de usar manejo de archivos en nuestra aplicación, vamos a necesitar asociar un bucket a nuestro staging y producción. Heroku nos da una forma fácil de hacerlo con el addon [Bucketeer](https://elements.heroku.com/addons/bucketeer). Al provisionarlo en nuestra app se nos generarán unas variables de ambiente con el prefijo `BUCKETEER_`, se deben usar los valores de estas variables, pero las keys deben ser las mismas que se usan en el `shrine.rb` y que están en el `.env.development`.

> Hay que tener en cuenta que el addon de bucketeer no tiene un tier gratis, siendo 5 USD lo más barato. Hay que conversar esto con el SM y cliente, posiblemente se deba transferir la app a un team de ellos, como se menciona al final de la guía de Heroku.

Teniendo las variables correctamente definidas, ya se puede restaurar la configuración de Shrine para que use S3 en producción.

### CORS

Con lo anterior ya tendríamos casi configurado S3 y Shrine, pero si deployamos y usamos la app veremos mensajes en la consola del navegador diciendo que hay requests que están fallando por CORS. El problema es que no le hemos dicho al bucket que debe aceptar requests desde el dominio de nuestra app. Para esto hay que definir su configuración de CORS. Hay un par de maneras de hacerlo:

* Usando [aws-cli](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html). Se debe instalar y configurar con las credenciales del bucket. Luego se puede usar el comando [s3api put-bucket-cors](https://docs.aws.amazon.com/cli/latest/reference/s3api/put-bucket-cors.html) para setear el CORS a partir de un archivo que tengamos en local. Se vería más o menos así:

  ```bash
  aws s3api put-bucket-cors --bucket my-bucket-name --cors-configuration file://cors.jsons
  ```

  Dónde `cors.json` debe ser algo así:

  ```json
  {
      "CORSRules": [
          {
              "AllowedHeaders": [
                  "*"
              ],
              "AllowedMethods": [
                  "PUT",
                  "POST",
                  "DELETE",
                  "GET"
              ],
              "AllowedOrigins": [
                  "https://pl-my-app.herokuapp.com"
              ]
          }
      ]
  }
  ```
* Llamando directo a la api de AWS. Para esto puedes descargar una colección y un environment de Postman desde [este link](https://www.notion.so/platanus/assets/S3_postman.zip). Debes dar los valores que correspondan a las variables del ambiente, pero notar que la variable `content-md5` se calcula sola al hacer la request, no es necesario darle un valor explícitamente

Si configuramos bien todo ya deberíamos poder subir y ver imágenes.


# Git

Utilizamos [git](https://git-scm.com/) para el control de versiones junto con [github](https://github.com/platanus)

## Commits

Los mensajes de commit:

* siempre los hacemos en inglés
* normalmente tienen una sola línea (aunque sabemos que más podría ser mejor)
* la primera línea se forma de un **tipo**, un **contexto** y una **descripción**

  ```
  tipo(contexto): descripción
  ```

La línea no debiera tener más de 100 caracteres para que se lea bien en Github.

## Tipo

El tipo nos ayuda a clasificar los commits. Los tipos que usamos son:

* **feat**: Un nuevo feature
* **fix**: La corrección de un bug
* **docs**: Cambios en la documentación
* **style**: Cambios que no afectan el significado del código (espacios, indentación, etc.)
* **refactor**: Un cambio en el código que no agrega una funcionalidad ni corrige un bug
* **perf** Cambios en el código que sólo mejoran la performance
* **test**: Agrega, corrige o tests
* **chore**: Cambios al proceso de build y herramientas auxiliares

## Contexto

El contexto es una palabra que hace referencia al lugar del código o funcionalidad que afecta el commit. Debe escribirse usando `kebab-case`, por ejemplo: `user-signup`

De manera opcional, se puede agregar información sobre el componente específico del código afectado. Si se agrega esta información:

* Debe ir después del contexto, separado usando un slash (`/`). Por ejemplo: `api/LoginService`
* El nombre del componente modificado debe estar en el mismo formato en el que aparece en el código (por ejemplo en `CamelCase` si es una clase ruby).

## Descripción

* usamos el verbo imperativo en inglés: "change" no "changed" ni "changes"
* separado por un espacio del contexto
* sin mayúscula al principio
* sin punto (.) al final

Nota: Esto es un extracto/traducción de [este documento](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits), que es más completo, pero que por el momento no es una práctica que sigamos completamente en Platanus

## Branches y Pull-requests

Los features los hacemos en un nuevo branch y hacemos un pull-request hacia master.k

### Ejemplo

Como ejemplo de estas recomendaciones el siguiente commit soluciona un bug en el componente/clase `SignUpForm` del frontend de una aplicación, en el cual no se estaba entregando feedback de la validación sobre si el nombre de usuario estaba disponible o no:

```
fix(ui/SignUpForm): validate username availability

The `SignUpForm` component wasn´t validating the availability of the `username` field and only displayed a `couldn´t create account` error on submit.

This commit adds an asynchronous validation when typing on the field so the user will know if the username is available before completing further fields.
```

## Rebase

Para ordenar un poco los commits, usamos fixups y rebase. [En este post de nuestro blog](https://plata.news/blog/manteniendo-la-historia-limpia-usando-git-rebase/) puedes leer sobre rebase y cómo lo usamos para mantener la historia limpia. También revisa [este post](https://fle.github.io/git-tip-keep-your-branch-clean-with-fixup-and-autosquash.html) sobre fixup, otra herramienta del rebase que usamos para esto.


# Cloudflare

En Platanus usamos Cloudflare para administrar los DNS.

Si tienes que configurar un nuevo dominio, estos son los pasos:

1. Configurar nic.cl
2. Agregar el dominio en Cloudflare
3. Configurar Heroku
4. Configurar los DNS en Cloudflare
5. Configurar los SSL/TLS en Cloudflare
6. Configurar una page rule en Cloudflare

**Nic.cl**

```
Si es un dominio .cl, vas a tener que hacer un truquito antes. En [nic.cl](http://nic.cl/) configura los siguientes nameservers:

1. [alex.ns.cloudflare.com](http://alex.ns.cloudflare.com/)

1. [jade.ns.cloudflare.com](http://jade.ns.cloudflare.com/)
```

**Agregar dominio en Cloudflare**

![](/files/vUXTnTr1QHGHV7AEAPF0)

Si es un `.cl` puede que tengas que esperar unos minutos para que te funcione luego de hacer el primer paso.

**Heroku**

Agrega un dominio en la página de settings del proyecto:

![](/files/GF3ix5eAysAIRESBVn4v)

El **DNS target** es lo importante.

(luego del siguiente paso quizás tengas que hacerle click al "Refresh ACM Status")

**DNS en Cloudflare**

Agrega 2 CNAMEs:

```
1. Para el root con el DNS target que te dio Heroku

1. Para el `www` apuntando al root

<img src='assets/cloudflare-3.png'/>
```

**SSL/TLS en Cloudflare**

Acá debes hacer dos cosas

```
1. Encriptación Full

    <img src='assets/cloudflare-4.png'/>

    **Nota: **A veces hay que esperar un rato para que esto haga efecto. También a veces este setting aparece en Full en un principio y si uno intenta ir al sitio arroja un error de Too Many Redirects. Esperen un rato y actualicen, ahí debería aparecer en Flexible, y se debe modificar para que quede en Full.

1. En Edge Certificates activa "Always Use HTTPS"

<img src='assets/cloudflare-5.png'/>

```

**Page Rules en Cloudflare**

Agrega una regla para redirect de www al root. Este page rule se construye notando que capture todo lo que viene después de la url con `*`, y se usa en el `$1`. Así se ve el form:

![](/files/diwjeV7AakQoWoup4e15)

Y una vez que esté agregado se va a ver así:

![](/files/XGnv4i1rLbqO4tgM6RNZ)


# Sendgrid

## Sendgrid

Para dejar configurado Sendgrid correctamente y mandando mails a nombre del sitio que queremos, primero hay que configurarlo correctamente en Heroku.

## Configurar una cuenta

Cuando nos enfrentamos a querer mandar mails usando Sendgrid tenemos dos opciones. Usar los addons de Heroku o usar una cuenta de Sendgrid directamente.

#### Addon de Heroku

Heroku nos provee una interfaz bien cómoda para agregar servicios a nuestras apps. Para hacerlo:

1. Ir a la app en Heroku, pestaña resources
2. En el buscador escribir sendgrid y seleccionar la primera opción
3. Elegir el plan gratis y *submit order form*

   ![](/files/SVkjEe6UuP7f44rPrs0D)

Si todo anda bien el addon va a aparecer en el listado y se habrán agregado dos variables de entorno al proyecto, `SENDGRID_USERNAME` y `SENDGRID_PASSWORD`.

Muchas veces *todo no anda bien*, y esto se debe a unos problemas que tienen entre Heroku y Sendgrid. Si sale que no se pudo provisionar con un alarmante mensaje de ban como este

![](/files/5t8rcAIbV3ad3fV70JtB)

Solo queda ir a la opción **Cuenta de Sendgrid**, o contactarse con el soporte de Sendgrid, proceso tedioso pero que funciona.

Por otro lado si no sale eso y se provisiona correctamente, podemos hacer click en el listado de resources y esto nos llevará a Sendgrid mismo, Heroku inteligentemente nos *loginea* en su servicio.

![](/files/nbm2wtCrjYfaj2KA84YX)

\*\*Nota/Tip: \*\*muchas veces cuando entramos por primera vez a la interfaz de sendgrid nos dice que validemos el mail `app12983123@heroku.com`. Si intentamos hacerlo poniendo por ejemplo nuestro mail, nos van a *bannear* automáticamente así que ignorar ese mensaje! Todo funciona bien sin hacerlo.

Si somos capaces de entrar a la interfaz de Sendgrid y no tenemos problemas para navegar dentro, lo logramos! 🚀

Más adelante explicaré el paso de la API Key que es igual para addon y para cuenta directa.

#### Cuenta de Sendgrid

Ya sea porque no nos funcionó con el addon, o porque un cliente ya tiene cuenta, podemos registrarnos directamente en Sendgrid. Acá todo debería andar bien, probablemente nos pida configurar un 2FA, a lo que obviamente deberíamos acceder.

Tener en cuenta que acá la validación de mail de usuario puede ser necesaria.

El único drawback de este camino es que la cuenta ya no está encapsulada bajo Heroku y hay que recordar unas credenciales más para el traspaso al cliente o para administrarlas internamente.

## Variable de entorno (API Key)

Ahora que ya tenemos acceso a nuestra cuenta Sendgrid por la vía que hayamos elegido, tenemos que informarle a nuestra app cómo conectarse a Sendgrid para enviar los benditos mails.

Para eso, en Sendgrid, hay que ir a **Settings > API Keys > Create API Key** y crear una nueva con \*full access \*y el nombre que estimemos conveniente:

![](/files/W5hYEG0DvZ0Fxti5346v)

Luego copiamos la key que nos entregue Sendgrid y lo ponemos en nuestra app de Heroku en la variable de entorno `SENDGRID_API_KEY`. Esto porque nuestras apps rails configuradas para Sendgrid esperan una API key como variable de entorno, no las variables de entorno de usuario y contraseña que puso automáticamente Sendgrid en nuestra app si usaron el camino del Addon.

## Sender Authentication

Al fin llegamos a la parte importante. Una vez que nuestra app puede mandar mails vía sendgrid, nos falta hacer que se manden correctamente a nombre nuestro. Esto implica que no lleguen a spam y que aparezcan enviados de nuestro dominio, por ejemplo `algo@mute.cl` (usaré mute de ejemplo de ahora en adelante).

Sendgrid nos provee un paso a paso de cómo hacerlo, es bien directo:

1. Ir a \*\*Settings > Sender Authentication \*\*y hacer click en **Get Started** bajo *Domain Authentication*

   ![](/files/LjDYk8gNOEyAeTw51OVS)
2. Nos va a preguntar qué DNS usamos, seleccionamos Cloudflare y dejamos el branding de links como está
3. Ahora nos pregunta qué dominio vamos a registrar, ponemos el nuestro y listo

   ![](/files/6adPESRFNMo9aUxdhYVF)
4. Al apretar siguiente nos aparecerán los registros DNS que tenemos que agregar en Cloudflare.

   ![](/files/n28GoCvOMg4i1Qj12zJJ)
5. Ahora en Cloudflare, vamos a nuestro dominio y seleccionamos DNS
6. Arriba de la lista de registros apretar el botón **Add Record**
7. Copiamos y agregamos los datos que nos entregó Sendgrid uno a uno.

   ![](/files/bImCyQk0XnCl3vSU9f5d)

   Hay que desmarcar la opción `proxied` haciendo click en la nube naranja, y pasará a decir DNS only:

   ![](/files/2eJpGOpCokVJhnf0GOdT)

   \*\*Nota/Warning: \*\*algo que puede pasar es que no funcione la validación de los pasos siguientes a pesar de haber puesto acá los datos que Sendgrid pedía. Hay que tener cuidado con que el registro CNAME muchas veces espera el subdominio sin el dominio (en el ejemplo sería `em1570` y no `em1570.mute.cl`). Esto hace que si uno pega el string con dominio, en algunos servicios de DNS aparezca duplicado (`em1570.mute.cl.mute.cl` en el ejemplo), lo que claramente no es lo que se espera. Cloudflare detecta automáticamente cuando se estaría duplicando así que no debería haber problema, pero si son porfiados y usan otro DNS, puede pasar y es difícil de cachar.
8. Si todo se hizo correctamente la lista se debería ver así:

   ![](/files/qcwmq9mmQD3rDi3sn4kN)
9. De vuelta en Sendgrid podemos hacer click en \*\*I've added those records \*\*y en **Verify.** Recordemos que la validación puede tomar algunas horas pero en general es bastante instantánea. Debería aparecer algo como:

   ![](/files/tzpLixnxVq2XWpDRdTDb)

Con esto ahora podemos enviar mails desde nuestra app rails pudiendo configurar el from del mail desde cualquier mail `@mute.cl` en el caso del ejemplo. No saldrán en spam!

## Recapitulemos

Primero configuramos una cuenta de Sendgrid para nuestra app, usando Heroku y sus addons o directamente Sendgrid. Después obtuvimos una API Key y pusimos la variable de entorno necesaria para que se pueda ocupar realmente Sendgrid en nuestra app. Finalmente le avisamos al mundo que somos dueños del dominio y que Sendgrid está autorizado a mandar mails por nosotros. :hypers:


# Dominio + Mailing

Cuando lanzamos a producción una aplicación en general hay dos preocupaciones nuevas con las que lidiar: el dominio y el mail.

## Dominio

El proceso de registrar un dominio es bien directo, y nos ayuda a que nuestras apps dejen de llamarse algo como `pl-super-banco-production.herokuapp.com` y se pasen a llamar `superbank.com`.

Veamos los pasos para la parte del dominio en detalle:

1. Se elige un nombre para la app, muchas veces este proceso aparte de estar ligado a mucha imaginación, está amarrado a la disponibilidad de los nombres, por lo que se mezcla mucho con el siguiente paso.

   ![](/files/kqXbRBIdKMgkmuPOxo4w)
2. Se busca y compra el dominio en un [registrar](https://en.wikipedia.org/wiki/Domain_name_registrar) como [name](https://www.name.com/), [namecheap](http://namecheap.com/), [godaddy](http://godaddy.com/), [nic.cl](http://nic.cl/), etc. Hay que tener en cuenta que algunos [TLD](https://en.wikipedia.org/wiki/Domain_name_registrar) (.com, .cl, .io, .so, etc.) solo son vendidos por algunos *registrar* así que a veces hay que abandonar el regalón.

   ![](/files/T56PxmmMo4Ufb7BQlePv)

   **Nota**: debe quedar en las cuentas de Platanus
3. Los dominios y su DNS los manejamos en Cloudflare y hay una muy buena guía para hacerlo con un dominio comprado en nic. En otros servicios es parecido, lo que hay que hacer es configurar los nameservers que nos pide Cloudflare en el servicio. La guía está acá:

   [Cloudflare](/setup/configuracion_de_proyectos/cloudflare)

Una vez completados los pasos anteriores podemos acceder a nuestro dominio fancy y olvidarnos del `pl-super-banco-production.herokuapp.com`. Lo que falta es que ahora se manden mails desde `superbank.com` correctamente.

## Mailing

En general uno quiere mandar mails en nuestros proyectos rails, ya sean de bienvenida, de recuperar contraseña, de notificaciones, etc. Cuando ya se configuró Sendgrid como addon de Heroku o como una cuenta externa, basta con mandar un mail en rails escogiendo el parámetro `from` y listo, se va a enviar un mail que vendrá de parte de quién sea que hayamos especificado en ese parámetro.

¿Estamos listos o no? ¿No basta con poner `misupermail@superbank.com` en el from y conseguí mi objetivo? Por suerte no.

### ¿Por qué me tengo que preocupar del mailing?

En mi opinión el protocolo de mails es inherentemente malo y permite cosas terribles como mandar mails a nombre de otros sin problemas, dejando a criterio del servicio de mail si catalogarlos como spam o no, pero eso da para toda una conversa aparte.

Por suerte Gmail y otros servicios nos van a advertir de que hay algo raro y de que probablemente es spam o suplantación de identidad mostrando algo como:

![](/files/HDiWa9L2figqsluNL0lF)

Lo que pasa es que el mail `noreply.lakatan@gmail.com` no ha autorizado a nuestra app para mandar mails en su nombre. Por eso Gmail nos alega y lo manda directo al spam. Además se ve un `via sendgrid.net`, que tampoco nos gustaría que se viera, pero esto lo hace Sendgrid de buena voluntad dado que sabe que no estamos autorizados, un atacante podría esconder eso.

Lo que menos queremos es que todo esto pase con nuestro nuevo dominio fancy. El problema es que si ponemos el `from` con un mail del dominio que somos dueños, por ejemplo `hola@superbank.com`, igual va a llegar a spam porque Gmail no nos cree, incluso aunque yo nunca le haya mentido a nadie.

Aquí es donde entra la \*\*\*autenticación de dominio, \*\*\*el término que usa Sendgrid (sender authentication) para poder comprobarle a todo el mundo, más allá de cualquier juramento que uno pueda hacer, que se es dueño del dominio y que el envío de mails desde ahí está autorizado.

Este proceso se basa en harta criptografía (firmas digitales y demás) pero a grandes rasgos permite que configurando registros DNS en nuestro dominio, se autorice a Sendgrid y los servicios de mail sepan que está autorizado y no nos marquen como spam. Un mail correctamente enviado se va a ver así:

![](/files/vyuF41X3ycszTCpLBCPZ)

## Uff qué harto preámbulo

Sí, fue un poco largo, pero creo que es importante saber que uno puede mandar mails de dónde quiera, que primero serán *flaggeados* como spam, pero que hay un procedimiento para que todo ande sin alertas, con certeza de que no hay suplantación de identidad. Todo lo que queremos para nuestro nuevo producto recién lanzado.

En la guía a continuación se ven 3 cosas, como configurar el addon o cuenta particular, qué variables de entorno usar y por qué, y finalmente cómo hacer el Sender Authentication.

[Sendgrid](/setup/configuracion_de_proyectos/sendgrid)


# Google Tag Manager, Analytics, Search Console, etc.

En esta guía comento como fue el proceso de usar Google Tag Manager, Analytics, Ads y Search console para poder indexar, crear y anuncio y ver métricas de [Bencinas Chile](https://bencinaschile.cl/). Explico algunas configuraciones y también dejo referencias a otros recursos que yo utilicé.

En general dejé algunas fotos, videos y otras referencias (que en mi opinión son concisas) para explicar.

## Luego de tener el dominio, qué hago?

Una vez configurados el nombre de la app y su [dominio](https://www.notion.so/platanus/Saliendo-con-un-producto-a-producci-n-Dominio-Mailing-83d1bc24343f465483bf9f06b4887946), nos gustaría que nuestra página esté indexada en Google, queremos saber que tal le va a la página, poder saber quienes la ven, cómo los usuarios interactúan en la app, e incluso crear anuncios en Google para poder promocionar nuestra aplicación.

Veamos los pasos que a seguir con detalle:

[Google Tag Manager](/setup/configuracion_de_proyectos/google_tag_manager_analytics_search_console_etc/google_tag_manager)

[Google Analytics](/setup/configuracion_de_proyectos/google_tag_manager_analytics_search_console_etc/google_analytics)

[Indexación en Google](/setup/configuracion_de_proyectos/google_tag_manager_analytics_search_console_etc/indexacion_en_google)

[Google Ads](/setup/configuracion_de_proyectos/google_tag_manager_analytics_search_console_etc/google_ads)

## Extra: Hotjar

Es posible que queramos saber como interactua el usuario con nuestra aplicación, dónde hace click o como se mueve en la página. Para esto, podemos usar Hotjar. Teniendo Google Tag Manager, la integracion con Hotjar es muy facil, puedes seguir [esta guía](https://help.hotjar.com/hc/en-us/articles/115009499708-Google-Tag-Manager-).

En hotjar, hay 2 secciones de analytics: Heatmaps y Recordings. En los heatmaps puedes ver los clicks y scroll que hacen los usuarios y en los recordings vez literalmente una grabación de la sesión del usuario en tu páginas. Ambos analytics puedes configurarlos para que se activen con distintas urls: puede ser que se activen con cualquier url (es decir, entren a cualquier url de tu dominio, por ejemplo: /login /products?name=producto, etc.) o también puedes ajustarlo para que trackee algunas URLs en específico.


# Google Tag Manager

## Google Tag Manager

[Configurar Google Tag Manager](/setup/configuracion_de_proyectos/google_tag_manager_analytics_search_console_etc/google_tag_manager/configurar_google_tag_manager)

### ¿Qué es?

Es una herramienta que permite manejar y publicar tags (pedazos de código) en tu página web o app sin tener que tocar el código.

#### Ejemplo

Un usuario entra a mi página web. Yo quiero guardar esa info en Google Analytics.

La página web le envía un evento a GTM y esté a su vez manda el evento a Google Analytics.

![](/files/dltaKlnjNemiOxztk2yq)

### Ventajas

1. Permite que el equipo de marketing agregue tags sin molestar a los desarrolladores
2. Un mismo evento en la página web puede gatillar varios tags distintos
3. Evita ensuciar el código enviando eventos a proveedores de analytics

## ¿Cómo funciona?

Hay 3 partes fundamentales en GTM.

* \*\*Tags: \*\*el código que se ejecuta - o el pixel
* \*\*Triggers: \*\*el gatillador, le dice a GTM que tiene que ejecutar un tag
* \*\*Variables: \*\*información adicional que puede ser utilizada como información del tag o del trigger

### Tags

Son pedazos de código o pixeles para trackear información de los usuarios. Los tags le dicen a GTM que tiene que hacer.

Tags comunes:

* Google Analytics: Universal Analytics (para configurar Google Analytics)
* Google Analytics: GA4 Configuration/Event (para configurar la nueva versión de GA)
* Hotjar Tracking Code
* LinkedIn Insight
* Twitter Universal Website Tag
* Custom HTML - permite agregar el código que queramos
  * El pixel de Facebook se debe agregar como Custom HTML (me imagino que es un tema de competencia y Google no quiere poner a Facebook en su página)

Hay muchos más, la gracia es que una vez configurado GTM, el equipo de marketing puede configurar todas estas cosas sin tener que molestar a los desarrolladores.

### Triggers

Los triggers le indican a GTM cuándo tiene que ejecutar el código de una tag. Funcionan como un if, cuando ocurre el evento mencionado en el trigger se ejecuta el tag.

Triggers comunes:

* Page View: se gatilla cuando el usuario entra a una nueva página
* Click - All Elements: se gatilla al hacer click
* History Change: se gatilla cuando cambia la url, es muy útil al hacer SPA ya que esos cambios no gatillan Page Views
* Custom Event: se pueden definir eventos custom

Todos los triggers tienen la opción de gatillarse siempre que ocurre un evento o solamente cuando se cumplen ciertas condiciones.

### Variables

Las variables permiten pasarle información a los tags o para agregarle condiciones a un trigger.

Se separan en dos tipos Built-In Variables (variables que vienen por defecto) y User-Defined Variables (variables que crea el usuario)

Built-In Variables comunes:

* Page URL - url de la página
* Page Path - path actual
* Click Classes - classes de css en el cual se hizo clic
* Click Id - id del elemento que se clickeo
* Clit Text - texto del elemento que se clickeo

User-Defined Variables comunes

* 1st Party Cookie - cookies del sitio
* Constant - una constante
* Google Analytics Settings - el id para trackear Google Analytics (parte con UA-)


# Configurar Google Tag Manager

Primero tenemos que configurar un contenedor de [Google Tag Manager](https://tagmanager.google.com/) para nuestra aplicación. Para hacer esto, si no se tiene una cuenta primero se debe crear una, y bajo esta crear un contenedor. [Este corto video](https://www.youtube.com/watch?v=P4suvDuj0kI\&list=PLI5YfMzCfRtYLtw_djEwG0nR-F9r6B5JT) explica bastante bien como crear un contenedor.

## Instalación con Potassium

Actualmente se puede agregar GTM utilizando Potassium. Revisar si `env.development` tiene la variable de entorno `GTM_CONTAINER_ID`, si no la tiene ejecutar `potassium install google_tag_manager`.

Luego hay que agregar la variable de entorno en el proyecto. El valor se obtiene en tagmanager.google.com, y siempre comienza en GTM-

![](/files/iUJ1NwnXV7G9cpCIdAVI)

## Instalación Manual

Una vez creado el contenedor, se obtendran unos *code snippets* que deben agregarse a la aplicación para *linkearla* con el contenedor

Como yo lo hice fue asi: Creé un archivo `/app/views/common/_tag_manager_head.html.erb` con el siguiente código:

```javascript
<% if Rails.env.production? %>
	<!-- Google Tag Manager -->
	<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
	new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
	j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
	'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
	})(window,document,'script','dataLayer','GTM-####');</script>
	<!-- End Google Tag Manager -->
<% end %>
```

Y otro archivo `/app/views/common/_tag_manager_body.html.erb` con el siguiente código:

```javascript
<% if Rails.env.production? %>
	<!-- Google Tag Manager (noscript) -->
	<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-####"
	height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
	<!-- End Google Tag Manager (noscript) -->
<% end %>
```

OJO! EN `GTM-####` deben reemplazarlo por el id de su contenedor, que aparecé en el menú.

![](/files/PPt4TMF94CFHz05ZbW5N)

y luego, deben agregarse estas vistas parciales en `/app/views/layouts/application.html.erb`, el *head* debería ir lo más arriba posible en el *head*, y el *body* lo más arriba posible dentro del tag *body*. Debería verse algo así:

```html
<!DOCTYPE html>
<html>
  <head>
    <%= render partial: '/common/tag_manager_head' %>
    <title>My Applications</title>
    <%= csrf_meta_tags %>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>

    <%= stylesheet_pack_tag 'application' %>
  </head>

  <body>
    <%= render partial: '/common/tag_manager' %>
    <div id="app">
      <app></app>
      <%= yield %>
    </div>
  </body>
</html>
```

Luego, se suben estos cambios a producción y estamos listos con Google Tag Manager (por ahora)


# Google Analytics

Teniendo nuestro contenedor en tag manager y ya agregados los *code snippets*, hacemos [setup de Analytics para el sitio](https://support.google.com/analytics/answer/10269537?ref_topic=1009620). Con esa corta guía, logramos crear una propiedad de *Universal Analytics* (importante tener al menos la universal, para agregarla Google Tag Manager). Teniendo configurada la propiedad, copiamos el Id de esta, que podemos verlo acá (tiene forma `UA-####`)

![](/files/d4nnYfZ4cMUkzTv3JI3C)

Teniendo este id, vamos de vuelta a nuestro contenedor de Google Tag manager, le damos a *Add a new tag*

![](/files/DDJF54HCUStS9rMdtP3C)

En **Tag Configuration** le elegimos la opción `Google Analytics: Universal Analytics`

![](/files/hFvgNDCCh36g5XPZXqvI)

En *Google Analytics Settings* elegimos *New Variable*, lo que nos mostrará otra ventana donde tenemos que ingresar nuestro *Tracking id* (el que tiene la forma `UA-#####`), le damos un nombre a la variable y la guardamos.

![](/files/CVfcpcAyq35FnPLIp0Vx)

Luego de esto en el selector debería aparecer la variable con el nombre que le dimos, elegimos esta para configurarlo con Google Analytics.

En **Triggering**, selecionamos *All Pages*. No olvidar darle un nombre descriptivo al tag, y ya con esto, guardamos el Tag.

Al guardar, debería cerrarse esa vista, y arriba a la derecha debería salir un botón que diga **Submit**, le hacemos click, agregamos una descripcion mencionando que agregamos el tag de google analytics y luego apretamos **Publish**. Y listo, queda configurado google analytics para nuestra aplicación y ya podemos ver las métricas.


# Indexación en Google

Si bien Google tarde o temprano indexará nuestra aplicación, a veces queremos que esté disponible lo más rápido posible para que aparezca en búsquedas de google (es muy posible que esto no sea del todo suficiente para aparecer en las primeras páginas al principio y sea necesario publicar un anuncio, hablaremos de esto mas adelante). Para verificar si Google ya indexó nuestra página, vamos a [Google Search Console](https://search.google.com/) (asegurarse de esta conectado con la cuenta correcta, ya que acá se verificará el dominio y pedir los permisos correspondientes) e ingresamos el dominio de nuestra app.

![](/files/4AmSlsRqKwcDxzBB66fq)

Al hacer click en continuar, hará una verificación de si eres propietario del sitio, y lo más seguro es que no esté verificada aún. Ahí saldra una ventana para hacer una configuración con el DNS (agregar un registro TXT) y así verificar la propiedad. Como se menciona ahí mismo, es posible que esto tarde, e igual hay [otras maneras de verificar la propiedad de un sitio](https://support.google.com/webmasters/answer/9008080), como por ejemplo con Google Tag Manager.

Una vez verificada la propiedad, [se le puede solicitar a Google](https://support.google.com/webmasters/answer/6065812) que rastree las URLs, que puede tardar algunos días también. Es recomendable también agregar un `sitemap` que así el *crawler* de Google indexe todas nuestras URLs y lo haga más rápido. Para proyectos en RoR, es puede usar [sitemap\_generator](https://github.com/kjvarga/sitemap_generator).

Para agregar un sitemap, primero se debe configurar uno (que puede ser con la gema `sitemap_generator` u otro método), y una vez configado, en Google Search Console, uno selecciona el dominio que quiere configurar, va a la sección *Sitemaps*

![](/files/DDB61wFRXOlWauFb93Mb)

Y ahí se rellena el field con la dierección al sitemap (Generalmente es algo como `www.example.org/sitemap.xml`) y luego *click* en *submit*. Con esto hacemos que Google pueda encontrar e indexar mas rápido las URLs de nuestra aplicación.


# Google Ads

Es muy posible que nuestra aplicación no aparezca en la primera página de Google, y quizás consideremos crear un anuncio en Google para así aumentar la visibilidad.

Para esto, vamos a [Google Ads](https://ads.google.com/) (Puede ser que tengas que solicitar acceso a la organización). En la sección *All campaigns*, saldrá un *overview* de todas las campañas creadas, y arriba a la izquierda hacemos *click* en **NEW CAMPAIGN**.

![](/files/gbTTiWIshuCrHZt6Ixc2)

Ahí luego se elige el objetivo y el tipo de campaña deseada (lo más basico es 'Web Traffic' y 'Search' respectivamente), además de introducir el sitio web. (Si se quiere elegir otro objetivo y tipo de campaña, al hacer hover sobre cada uno explica brevemente que significa cada uno, para mí, los dos que mencioné calzaban con lo que buscaban y resulta ser lo más "clásico").

En la parte 1 se va configurando el anuncio a lo que uno desee lograr (pais/es donde se anuncia, en que parte de internet se anuncia, etc.). Algo importante que uno debe ingresar un *budget*, que es cuánto uno planea gastar en el anuncio **POR DIA** en promedio (a veces se gastará un poco más, a veces un pcoo menos, pero en promedio se buscar llegar a eso) además de *Bidding*. *Biddings* es lo que uno busca maximizar, o busca aumentar, lo más clásico en *clicks* pero tambien pueden ser conversiones, valor de conversiones, etc.. En general Google deja varias cosas automáticas para maximizar clicks, pero es bueno que leas todas las opciones disponibles si buscar maximizar otro tipo de acciones o customizar más tu anuncio.

En la parte 2 se configuran los ad groups, lo que es muy importante ya que ahí se configuran las *keywords* de búsqueda que haran que aparezca nuestro anuncio. Recomiendo usar el [Keyword planner](https://ads.google.com/aw/keywordplanner/home) que provee google ads para ver cuales son las mejores opciones para lo que uno busca. En esta herramienta, apretamos *Discover new keywords*

![](/files/hWAVtNpxZnGsN2AJBZZu)

Ahi desplegará una ventana con unos campos a rellenar, ahí escribimos una o más palabras claves que representen en general las palabras claves que queremos generar (tambien podemos usar un sitio web para empezar a generar palabras claves). Luego hacemos click en **Get Results**.

Ahora debería desplegarse una vista que varias palabras claves, las cuales tienen un checkbox y podemos elegir varias. Notar que hay una columna que dice "Competition", en general queremos buscar algunas que tengan baja competencia al principio, ya que queremos harta visibilidad de nuestro anuncio. Entonces, elegimos las palabras claves que nos resultan interesantes, y arriba nos aparecerá esta barra

![](/files/XxtUbwvdP9bRDDc35cP2)

Aqui podemos guardar las keywords en un ad group, e incluso podemos copiar las keywords seleccionadas (a la derecha).

Una vez copiadas (y si quieren pueden guardarlas), volvemos a la configuracion del anuncio y las pegamos donde dice keywords

![](/files/ZKVGHxH2DXJC1h5p0Ucc)

Al agregar keywords, a la derecha se actualiza una "predicción" de los clicks estimados para el anuncio y el CPC (cost per click). Una vez contento con las keywords seleccionadas, damos *save and continue* OJO: en mi caso, siempre salió no traffic expected, movi hartas cosas y logré aumentarlo a 1. Pero si uno elige bien las keywords, buenos headers y descripciones para su anuncio, estos números aumentarian mas adelante, así que uno puede ignorarlo un poco.

Por último, se configura el anuncio en sí (se puede crear mas de uno y Google va cambiando y ya despues de un tiempo muestra el que tenga mejor performance), se tiene que crear headers y descripciones (Google usa distintas combinaciones de estos y se va quedando con la mejor combinación) para el anuncio. No olvidar agregar la url a la que se quiere redirigir con el anuncio.

![](/files/ACW3xw5f8yRCFmYr57om)

Finalmente, al continuar, aparece un *Review* del anuncio creado e indica si hay campos que arreglar o revisar. Y luego de dar el visto bueno, se da publicar y ya se anuncia. Se tarda un poco en publicarse ya que primero Google verifica que este todo en orden, y una vez publicado, se pueden ver métricas de las interacciones de los usuarios con el anuncio.


# Crear un bucket de S3

## Crear un bucket de S3

Muchas veces es necesario hacer un bucket en S3 para no usar bucketeer, sobretodo en productos internos.

Para eso, necesitamos hacer 3 cosas: crear un bucket, crear un usuario para acceder al bucket (y tener las credenciales) y una política de acceso que asocie al usuario con el bucket.

## Crear el bucket

1. [Ir acá](https://s3.console.aws.amazon.com/s3/home?region=us-east-1)
2. Apretar el botón `Create bucket`.
3. Poner el nombre usando la convención `proyecto.platan.us` y `proyecto-staging.platan.us`, por ejemplo `lacatan.platan.us` y `lacatan-staging.platan.us`. Seguiré usando lacatan para el ejemplo.
4. Dejar la región como está. (generalmente es us-east-1)
5. Desmarcar las opciones que dicen "Bloquear todo" (aparecerá un warning, aceptarlo).

   ![](/files/3BZjPwxtrl7GdCJHBARR)
6. Dejar todos los otros campos como están y crear.

Ahora debemos modificar un permiso del bucket. Para eso, seleccionar el bucket recién creado del listado y:

1. Ir a la pestaña de permisos
2. Ir al fondo y buscar la sección `Uso compartido de recursos entre orígenes (CORS)`.
3. Seleccionar el botón editar.
4. Escribir lo siguiente dentro, cambiando en allowed origins, las urls que correspondan:

   ```json
   [
   	{
   		"AllowedHeaders": [
   			"Authorization",
   			"x-amz-date",
   			"x-amz-content-sha256",
   			"content-type"
   		],
   		"AllowedMethods": [
   			"GET",
   			"POST",
   			"PUT"
   		],
   		"AllowedOrigins": [
   			"https://pl-lacatan-staging.herokuapp.com"
   		],
   		"ExposeHeaders": [
   			"ETag"
   		],
   		"MaxAgeSeconds": 3000
   	}
   ]
   ```

   **Nota:** En mute por ejemplo, que hay url para staging, se pueden poner dos orígenes: `pl-mute-meetings-staging.herokuapp.com` y `staging.mute.so`
5. Guardar los cambios

## Crear una política de acceso

La política es lo que permite que el usuario que vamos a crear después pueda meter cosas al bucket (y nadie más que el)

1. Ir a <https://console.aws.amazon.com/iam/home> o a IAM en el menú del sito de AWS
2. En la barra lateral ir a "Políticas"
3. Apretar crear política
4. Seleccionar la pestaña JSON
5. Escribir lo siguiente, reemplazando lacatan por el nombre que corresponda:

   ```json
   {
       "Version": "2012-10-17",
       "Statement": [
           {
               "Sid": "",
               "Effect": "Allow",
               "Action": "s3:*",
               "Resource": [
                   "arn:aws:s3:::lacatan-staging.platan.us/*",
                   "arn:aws:s3:::lacatan-staging.platan.us"
               ]
           }
       ]
     }
   ```
6. Siguiente y revisar (sin agregar etiquetas)
7. Poner el nombre, no hay convención dura, pero yo les pongo `s3-lacatan` o `s3-lacatan-staging`
8. Guardar (los otros campos dejar como están).

## Crear un usuario para acceder al bucket

Ahora creamos un usuario (y las correspondientes credenciales) para usar la política y tener lo que poner en las vars de heroku

1. Ir [acá](https://us-east-1.console.aws.amazon.com/iamv2/home?region=us-east-1#/users).
2. Darle a `Create User`
3. En el nombre poner `lacatan` o `lacatan-staging` y le damos a siguiente.
4. Le asignamos una Policy directamente y seleccionamos la que creamos anteriormente y vamos a siguiente.

   ![](/files/cSofmLD16xtjXUnZnSNf)
5. En el siguiente paso le damos a crear
6. Una vez creado, entramos al user recién creado a la sección de `security credentials` y ahí buscamos `Access Keys` y creamos una nueva.

![](/files/q77levBKwniW6pBe4Der)

1. Elegimos la opción de `Application running outside AWS` y vamos al siguiente paso.
2. No le agregamos descripción y creamos la llave de acceso.
3. IMPORTANTE acá ahora aparecerá la Access Key y Secret Access Key. Acá tenemos que guardar esos valores que nos saldrán para luego poder configurarlos en la aplicación. Abajo sale un botón de `Download CSV File`. Descarga y guarda el archivo.
4. \*\*Bonus: \*\*ir a heroku a las vars de entorno y poner las credenciales correspondientes, junto al nombre del bucket. La región en general siempre es us-east-1.4


# SlackBot

## Contexto

Muchas veces queremos recibir información desde nuestras aplicaciones a algún canal de Slack y para esto existe una [API](https://api.slack.com/) que nos provee diversas funcionalidades.

## Configuración

Para configurar un nuevo bot que interactue con nuestro workspace de Slack es importante seguir [esta guía.](https://api.slack.com/authentication/basics)

Y luego hacer pruebas desde rails con la gema [slack-ruby-client](https://github.com/slack-ruby/slack-ruby-client/blob/v0.17.0/README.md) .

## Posibles errores

Puede pasar que tu app esté bien configurada, pero que cuando intentes que el código mande mensajes o haga alguna acción, la request te entregue el siguiente error:

```javascript
Slack::Web::Api::Errors::MissingScope: missing_scope
```

Esto pasa porque no se han actualizado bien los scopes en el workspace, entonces Slack cree que tu app no tiene ningún permiso.

Para eso tienes que hacer Reinstall to Workspace como en la imagen:

![](/files/Ccdj3e13kNpa1YmUtp6e)

Y luego tiene que pedirte permiso para acceder al workspace como en la siguiente imagen:

![](/files/dpM8Tnb88aulsRhHAsfa)

Si no te aparece la segunda imagen te recomendamos agregar a alguien del workspace como colaborador, y que esa persona reinstale la app.


# Google BigQuery

Google BigQuery es un [*data warehouse*](https://en.wikipedia.org/wiki/Data_warehouse) \*serverless. \*En esencia, es una base de datos SQL, externa a la infraestructura que se suele llevar en nuestros proyectos, y está diseñado para funcionar con grandes volúmenes de datos. Sus integraciones son súper sencillas, por lo que permite tener soluciones para *analytics* rápidamente.

## ¿Cuando/por qué usar BigQuery?

Podríamos considerar usar BigQuery en un proyecto en los siguientes casos:

* Se tiene mucha información transaccional, a la que se quiere tener acceso para hacer analytics.
* Se quieren aprovechar herramientas que tengan una fácil integración con big query (como data studio)
* Se quiere evitar sobrecargar la base de datos operacional con consultas para analytics que pueden ser pesadas.
* Si el producto es B2B podría ser buena idea tener una base de datos individual para cada cliente, que incluya la información relacionada con su empresa y se le pueda facilitar al cliente para que pueda integrar con las herramientas de BI que estime convenientes.

Esto ya que las ventajas de usar Google BigQuery son:

* Manejar grandes volúmenes de datos con alta velocidad.
* Acceder a una interfaz en la consola de Google Cloud que permite hacer consultas usando SQL
* Integrarse con herramientas de *analytics* y BI de manera sencilla (como por ejemplo Google DataStudio).
* Al utilizar una base de datos externa para *analytics* podemos evitar la carga que implicaría sobre nuestra base de datos en heroku las consultas que se requieran para obtener estadísticas más complejas.

## ¿Cómo la usamos?

### Instalación

1. **Creación de un proyecto en la consola de Google Cloud**

El primer paso para integrarse con BigQuery es tener un proyecto creado en la consola de Google Cloud.

<https://cloud.google.com/resource-manager/docs/creating-managing-projects>

1. **Creación de credenciales para acceder a BigQuery via api.**

Para usar la gema de BigQuery, necesitaremos una serie de credenciales que se generan creando una \*Service Account \*en google cloud. Debemos asegurarnos de darle permisos de BigQuery a la cuenta de servicio creada.

<https://cloud.google.com/iam/docs/creating-managing-service-accounts>

Posteriormente, debemos generar una key nueva para esa cuenta de servicio. Debe ser una key de tipo JSON.

<https://cloud.google.com/iam/docs/creating-managing-service-account-keys#creating>

Con estos pasos, estamos listos para comenzar a configurar BigQuery en nuestro proyecto de rails.

### Uso básico

**Setup** **de la gema**

Debemos agregar la gema `google-cloud-bigquery` al gemfile.

Esta gema, automáticamente obtiene las credenciales de una variable de entorno llamada `BIGQUERY_CREDENTIALS`, por lo que para usarla debemos agregar esa variable, cuyo valor tiene que ser la key que descargamos en el archivo JSON.

Debe verse algo así:

```javascript
BIGQUERY_CREDENTIALS={"type": "service_account", "project_id": "super-proyecto-genial", "private_key_id": "some_id", "private_key": "some_private_key","client_email": "some_email","client_id": "some_id","auth_uri": "https://accounts.google.com/o/oauth2/auth","token_uri": "https://oauth2.googleapis.com/token","auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs","client_x509_cert_url": "some_cert_url"}
```

En el lugar donde se va a usar la gema (probablemente un client), se debe hacer un `require 'google/cloud/bigquery'` y crear un objeto `Google::Cloud::Bigquery.new`. Esto obtiene las credenciales definidas en la variable de entorno automáticamente.

**Tipos de recursos**

En BigQuery manejaremos principalmente 2 tipos de recursos, datasets y tablas.

Un dataset esta contenido dentro de un proyecto (el que usamos se define por las credenciales), y se usa para organizar y controlar acceso a tablas y vistas.

Una tabla es lo que contiene las filas de datos que le enviaremos a BigQuery, siempre está contenido dentro de un dataset.

Dependiendo del uso, podemos tener múltiples datasets, un solo dataset, múltiples tablas o una sola tabla.

<https://cloud.google.com/ruby/docs/reference/google-cloud-bigquery/latest/index.html#creating-datasets-and-tables>

\*\*Sobre la \*\*[**desnormalización**](https://en.wikipedia.org/wiki/Denormalization)

En BigQuery no se busca mantener un esquema de base de datos que replique exactamente la que tenemos en nuestro servidor. Esto es porque las bondades de BigQuery son mejor aprovechadas si los datos con los que trabajamos están denormalizados.

No obstante, [BigQuery ofrece algunas soluciones para esto](https://cloud.google.com/bigquery/docs/best-practices-performance-nested) de manera que no sea tan complicado realizar el proceso de desnormalización.

### Ejemplo

En nuestros proyectos probablemente vamos a tener un cliente, un módulo de util que nos permita conocer el schema de la tabla y cómo desnormalizar un record, y unos cuantos jobs para la creación de datasets, creación de tablas e inserción de datos. Además, necesitaremos agregar un callback al observer del modelo que queremos registrar en bigquery.

> 💡 Para los siguientes ejemplos, supongamos que tenemos una app para realizar encuestas de cualquier tipo.

```
Queremos tener una tabla para registrar la información de las respuestas, incluyendo la respuesta misma, la pregunta respondida, la encuesta asociada e información del usuario que responde.

Queremos que esto ocurra cada vez que se crea una nueva respuesta.

Tenemos las siguientes asociaciones entre modelos de nuestra app.

<img src='assets/google-bigquery-1.png'/>
```

**Cliente**

```ruby
# /app/clients/google_bigquery_client.rb
require 'google/cloud/bigquery'

class GoogleBigqueryClient
  def create_dataset(dataset_name)
    client.create_dataset(dataset_name)
  end

  def delete_dataset(dataset_name)
    dataset = client.dataset(dataset_name)
    dataset.delete
  end

  def create_table(dataset_name, table_name, schema)
    dataset = client.dataset(dataset_name)
    dataset.create_table table_name do |table|
      table.name = table_name
      table.description = "Table for #{table_name}"
      table.schema do |s|
        schema.each do |column|
          s.send(column[:type], column[:name], mode: column[:mode])
        end
      end
    end
  end

  def insert_row(dataset_name, table_name, row)
    dataset = client.dataset(dataset_name)
    table = dataset.table(table_name)
    table.insert(row)
  end

  private

  def client
    @client ||= Google::Cloud::Bigquery.new
  end
end
```

\*\*Módulo para mantener el ***schema*** y obtener los atributos de cada \*\****Answer***

```ruby
# app/utils/answers_data.rb
module AnswersData
  def self.attributes
    [
      { type: :integer, name: :id, mode: :required },
      { type: :string, name: :value, mode: :required },
      { type: :integer, name: :question_id, mode: :required },
      { type: :integer, name: :user_id, mode: :required },
      { type: :string, name: :user_email, mode: :required },
      { type: :string, name: :question_name, mode: :required },
      { type: :string, name: :question_option1, mode: :required },
      { type: :string, name: :question_option2, mode: :required },
      { type: :integer, name: :survey_id, mode: :required },
      { type: :string, name: :survey_name, mode: :required }
    ]
  end

  def self.row(answer)
    {
      id: answer.id,
      value: answer.value,
      user_id: answer.user_id,
      user_email: answer.user.email,
      question_id: answer.question_id,
      question_name: answer.question.name,
      question_option1: answer.question.option_1,
      question_option2: answer.question.option_2,
      survey_id: answer.question.survey_id,
      survey_name: answer.question.survey.name
    }
  end
end
```

**Job para crear un dataset**

```ruby
# app/jobs/create_bigquery_dataset_job.rb
class CreateBigqueryDatasetJob < ApplicationJob
  def perform
    client.create_dataset('bigquery_demo')
  end

  private

  def client
    @client ||= GoogleBigqueryClient.new
  end
end
```

**Job para crear una tabla**

```ruby
# app/jobs/create_bigquery_answers_table_job.rb
class CreateBigqueryAnswersTableJob < ApplicationJob
  include AnswersData

  def perform
    client.create_table(
      'bigquery_demo', 'answers',
      AnswersData.attributes
    )
  end

  private

  def client
    @client ||= GoogleBigqueryClient.new
  end
end
```

**Job para subir una answer a BigQuery**

```ruby
class UploadAnswerToBigqueryJob < ApplicationJob
  include AnswersData

  def perform(answer_id)
    answer = Answer.find(answer_id)

    client.insert_row('bigquery_demo', 'answers', AnswersData.row(answer))
  end

  private

  def client
    @client ||= GoogleBigQueryClient.new
  end
end
```

**Observer**

```ruby
class AnswerObserver < PowerTypes::Observer
  after_create_commit :upload_to_google_bigquery
	...

  def upload_to_google_bigquery
    UploadAnswerToBigqueryJob.perform_later(object.id)
  end

	...
end
```

### Recursos útiles

<https://cloud.google.com/bigquery/docs/introduction>

<https://github.com/googleapis/google-cloud-ruby/tree/main/google-cloud-bigquery>


# Rails

## Rails Deployment

Los proyectos Rails se publican en [heroku](https://dashboard.heroku.com/) en la cuenta de Platanus.

Utilizaremos los [pipelines](https://devcenter.heroku.com/articles/pipelines) de heroku para manejar diferente stages de la aplicación. Como convención partiremos siempre con *staging* y *production*.

### Creación de la app en heroku

Al crear un proyecto rails con potassium, si tienes acceso a la cuenta de heroku, las aplicaciones serán creadas por el mismo comando `create` de potassium.

> Para crear la aplicación se deben usar la cuenta owner que es tiene permisos para crear nuevas aplicaciones. Para esto debes installar el heroku-toolbelt y la gema potassium.

Debes hacer login con la cuenta de heroku

```bash
heroku login
```

y crear la aplicación

```bash
potassium create <app-name>
```

Esto creará una aplicacion para cada **stage**, creará el **pipeline** y hará la asociación entre las apps y el stage. Todo esto esta definido en la [receta heroku](https://github.com/platanus/potassium/blob/master/lib/potassium/recipes/heroku.rb) de potassium.

Si tienes un proyecto que todavia no tiene sus aplicaciones creadas en heroku, puedes ejecutar nuevamente la receta con el comando `install`

```bash
potassium install heroku
```

### Conectar github

Entrar al [dashboard de heroku](https://dashboard.heroku.com/) y conectar el pipeline con un repositorio en github y configurar los **automatic deploys** usando github.

![](/files/T1i7xhy95XR4UezYkYOm)

Al configurar los **automatic deploys** hay que elegir un branch para cada stage.

> NOTA: Si vas a habilitar CI, al conectar un branch a un stage debes habilitar la opcion que dice, esperar CI antes de publicar. Eso para que solo se publiquen braches en los que los tests estan pasando.

### Continuous integration

Los test de la aplicación ejecutados por el servicio CircleCi. Para esto debes habilitar el repositorio en <https://circleci.com/add-projects>.

### Continuous delivery

El deploy se hace de manera automática mediante usando los branches definidos para cada stage.

Cada vez que se hace un push al repositorio en github a uno de estos branches, la aplicación del stage correspondiente comienza su proceso de build y luego es publicada.

### Usando heroku desde la linea de comando

Para comenzar a usar heroku desde la linea de comando debes instalar [heroku-toolbelt](https://toolbelt.heroku.com/)

```bash
brew install heroku-toolbelt
```

Luego debes hacer login con tu cuenta de heroku.

```bash
heroku login
```

Para acceder mas fácil a las aplicaciones en heroku desde tu proyecto, el heroku toolbelt usa los remotes de github para saber en que stage o aplicación ejecutar un comando.

Potassium crea los remotes automáticamente a generar la aplicacion. Si acabas de clonar una aplicación existente puedes ejecutar el script `bin/setup`.

Luego de esto puedes ejecutar los comandos de la siguiente manera

```bash
heroku logs --remote staging
heroku config:set KEY=value --remote production
```

> El remote staging queda configurado como por defecto, por lo que puedes omitirlo.

**branch → stage**

master → staging

production → production


# Ruby Gems

Las gemas deben ser publicadas en [rubygems.org](https://rubygems.org/). Las publicaremos con nuestro usuario personal pero debemos agregar como owner al usuario de [platanus](https://rubygems.org/profiles/platanus).

```bash
# Agregar un owner a una gem
gem owner GEM_NAME --add rubygems@platan.us
```

## Configuración en Circle CI

Para hacer el deploy usando CircleCI, necesitamos agregar el archivo de configuración en la gema. Puedes ver un ejemplo de configuración [aquí](https://github.com/platanus/power_api/pull/29), pero en resumen lo que se hizo fue:

### Agregar el script en `.circleci/setup-rubygems.sh`

```
mkdir ~/.gem
echo -e "---\\r\\n:rubygems_api_key: $RUBYGEMS_API_KEY" > ~/.gem/credentials
chmod 0600 /home/circleci/.gem/credentials
```

Para poder utilizar la api key de rubygems al hacer el deploy.

### Agregar el job "deploy" en `.circleci/config.yml`

```yaml
deploy:
  executor: main-executor
    steps:
      - setup
      - run:
          name: Setup rubygems
          command: bash .circleci/setup-rubygems.sh
      - run:
          name: Publish to rubygems
          command: |
            gem build power_api.gemspec
            version_tag=$(git describe --tags)
            gem push power_api-${version_tag#v}.gem
```

Esto:

* Ejecuta el setup básico del proyecto.
* Ejecuta el script para copiar la api key en un archivo que se utilizará para el deploy de la gema.
* Crea el archivo .gem
* Crea la nueva versión en rubygems.

### Agregar el job deploy al workflow

```yaml
workflows:
  version: 2
  main:
    jobs:
      - lint:
          context: org-global
      - test:
          matrix:
            parameters:
              ruby-version: ["2.6", "2.7"]
      - deploy:
          context: org-global
          filters:
            tags:
              only: /.*/
            branches:
              ignore: /.*/
```

Lo que se hace aquí es ejecutar el job deploy siempre que se cree un nuevo tag en github.

> org-global es un contexto que tiene definido la variable de entorno `RUBYGEMS_API_KEY` que necesitamos para hacer el deploy.

### Publicación

Una vez configurado CircleCI en la gema tenemos que:

1. Cambiar `VERSION` en `lib/my_new_gem/version.rb` para que apunte a la nueva versión.
2. Cambiar el título `Unreleased` a la versión nueva en el `CHANGELOG.md`.
3. Correr `bundle install`.
4. Hacer commit (directo en master) de un nuevo release. Por ejemplo: `Releasing v0.1.0`.
5. Crear el tag. Por ejemplo: `git tag v0.1.0`.
6. Hacer push del tag. Por ejemplo: `git push origin v0.1.0`.

¡Listo!


# Browser and Node (Open Source)

Siempre que crees una nueva librería invierte tiempo en buscar un buen nombre (compártelo con el equipo)

## Crear el paquete

Primero, [crea un repositorio en Platanus](https://github.com/organizations/platanus/repositories/new). Asegúrate de que sea público.

Luego puedes crear el paquete con `npm init`, el cual te preguntará algunos detalles del paquete, o crear manualmente un `package.json`.

### `package.json`

En el archivo `package.json` debemos llenar al menos los siguientes campos.

```json
//restmod package.json example
{
  "name": "angular-restmod",
  "description": "API Bound Models for AngularJS",
  "version": "1.11.1",
  "dependencies": {
    ...
  },
  "devDependencies": {
    ...
  }
}
```

Puedes agregar como autor a Platanus y a ti como colaborador:

```json
"author": {
    "name": "Platanus",
    "url": "https://platan.us"
  },
"contributors": [
  {
    "name": "Tu nombre"
  }
],
```

## Dependencias

Al menos deberías instalar las siguientes dependencias:

* `jest`: Testing
* `eslint`: Linter. Puedes copiar las [reglas](https://github.com/platanus/potassium/blob/master/lib/potassium/assets/.eslintrc.json) que vienen en Potassium. Puede ser necesario eliminar las reglas relacionadas a Vue y Tailwind si no serán utilizados.
* `eslint-plugin-import`: Permite utilizar el linter para revisar sintaxis de import/export.

Opcionalmente, puedes instalar:

* `eslint-plugin-jest`: en caso de no instalarlo reemplazar `jest/globals` en `env` de `.eslintrc.json` por solo `jest`.

## ES6 vs CommonJS export sintaxis

Si estamos desarrollando un paquete de Node (y no de web) debemos usar la sintaxis de CommonJS para imports/exports:

```javascript
// lib.js
module.exports = {
  funcionLib
}
```

```javascript
// otro.js
const lib = require('./lib')
```

Se puede utilizar esta sintaxis manualmente o utilizar la sintaxis de ES6 y luego compilar con un bundle como webpack para que la librería quede en formato CommonJS.

## Comandos de consola personalizados para funciones del paquete

Para correr algún archivo con un comando personalizado se deben seguir los siguientes pasos:

* Crear un archivo `cli.js` o una carpeta `cli` con archivos que queremos que se ejecuten.
* Al principio de estos archivos colocar:

  ```
  #!/usr/bin/env node
  ```
* En `package.json` agregar una sección de:

  ```json
  "bin": {
    "nombre-comando-cli": "archivo.js",
    "nombre-comando-cli-2": "archivo2.js",
  }
  ```

Correr `npm link` (permite simular la instalación del paquete), reiniciar la terminal y probar corriendo alguno de los comandos creados. Si se modifica alguno de los archivos javascript no es necesario volver a correr `npm link`.

## Publicación

Los paquetes deben ser publicados en [NPM](http://npmjs.com/). Si no la tienes, deberás crearte una cuenta personal.

### NPM

Si no has iniciado sesión en tu cuenta desde tu terminal, corre:

```bash
npm login
```

En NPM debes registrar el paquete y publicar directamente cada versión.

```bash
npm publish
```

> IMPORTANTE: Las publicaremos con nuestro usuario personal pero debemos agregar como owner al usuario de platanus.

```bash
# Agregar un owner a un package
npm owner add platanus-owner <package-name>
```


# Mobile

[Mobile Resources](/deployment/mobile/mobile_resources)

[Apple App Storage](/deployment/mobile/apple_app_storage)

[Google Play](/deployment/mobile/google_play)


# Mobile Resources

## Guía de recursos para aplicaciones móviles

Todos los recursos mencionados son obligatorios para la publicación de la aplicación en las tiendas de cada plataforma a menos que se indique lo contrario.

### Recursos mínimos para la aplicación

### Ícono

* Un mínimo de `192x192px`
* Si el ícono tiene fondo no debe tener bordes redondeados.
* [Template PSD](http://code.ionicframework.com/resources/icon.psd)

### Android

* (Opcional) Ícono (`48x48px`) para ser usado en notificaciones. Blanco, con transparencia para lograr una silueta.

### Splashscreen

* Un mínimo de `1200x1200px`, centrado en un canvas de `2208x2208px`. [Ejemplo](http://i.imgur.com/cHU7zue.png).
* [Template PSD](http://code.ionicframework.com/resources/splash.psd)

## Recursos para publicar la aplicación

### General

### \[Google Play] Ícono de Vista Previa

Se refiere al ícono que se ve en la vista previa de la Play Store en el perfil de la app.\*\* Este icono es diferente al ícono de la app misma.\*\* Debe cumplir los siguientes requisitos:

* Archivo PNG de 32 bits
* Tamaño de 512x512 píxeles
* No puede superar un tamaño de 1024 KB
* No puede ser engañoso para los usuarios
* La forma debe ser de un cuadrado. Google Play se encarga de redondear los bordes
* No agregar sombras en el ícono, Google Play se encarga de eso
* Elegir un background-color para el ícono. No dejarlo transparente. Íconos transparentes aparecerán con el background-color de la UI de Google Play

![](/files/UL4IgikF245ZaGTxKwXO)

Lo que se ve en la Google Play Store es lo que se encuentra dentro de ese cuadrado verde.

### Ícono en alta resolución

* Un mínimo de `1024x1024px`. Si es solo para Android puede ser de `512x512px`
* PNG, 32-bit
* Si el ícono tiene fondo no debe tener bordes redondeados.

### Descripción corta (Google Play)

* Rápida Sinopsis de los servicios que provee la app. Máximo de 80 carácteres.

### Descripción:

* Descripción de la aplicación en español.
* Máximo de 4000 carácteres.

### Palabras clave (Apple)

* Palabras que describan la aplicación. Esto es útil para el motor de búsqueda para que la aplicación pueda ser fácilmente descubierta.
* Es importante considerar el equilibrio entre clasificar bien para términos menos comunes versus clasificar más bajo para términos populares. Los términos menos comunes generan menos tráfico, pero son menos competitivos.

### Sitio Web:

* Sitio web de la aplicación

### (Opcional) Videos

Se muestra antes que los screenshots. No es obligatorio, pero ambos Google y Apple lo recomiendan.

### iPhone (iOS) App Store

* `1080x1920px` o `1920x1080px`
* MOV o MP4
* El video no puede contener precios o propaganda.

### Android - Google Play

* Una URL de YouTube
* La URL debe apuntar a un video y no a un canal o categoría.

### Imágenes

### iPhone (iOS) App Store

### Screenshots

Los iPhones tienen diferentes dimensiones, por lo que las capturas de pantalla deben abarcar un rango de estas dimensiones. Las especificaciones de dimensiones se pueden encontrar [aquí](https://developer.apple.com/help/app-store-connect/reference/screenshot-specifications).

* Pueden llevar texto explicativo. [Ejemplo 1](https://i.imgur.com/qiI5wYV.jpg) - [Ejemplo 2](https://i.imgur.com/PvhLGqE.jpg) - [Ejemplo 3](https://i.imgur.com/8kLwz4w.jpg) (Ejemplos solo para contenido, no considerar para tamaño)
* JPG o PNG sin transparencia.
* Una screenshot de la aplicación en cada uno de los siguientes tamaños.
  * Pantalla 3.5 pulgadas: `640x940px`
  * Pantalla 4 pulgadas (Retina): `640x1096px`
* (Opcional) Screenshots para los siguientes tamaños:
  * Pantalla 4.7 pulgadas (iPhone 6): `750x1334px`
  * Pantalla 5.5 pulgadas (iPhone 6 Plus): `1242x2208`
  * iPad: `768x1004px` o `1536x2008px`
* Cada sección puede tener hasta 5 screenshots en total.
* Las screenshots no deben tener la `status bar`.

### Android - Google Play

### Screenshots

* Pueden llevar texto explicativo. [Ejemplo 1](https://i.imgur.com/SYGKwnl.jpg) - [Ejemplo 2](https://i.imgur.com/EsdqOta.jpg) - [Ejemplo 3](http://i.imgur.com/KQICzi7.jpg) (Ejemplos solo para contenido, no considerar para tamaño)
* Un mínimo de 2 screenshots, máximo 8.
* JPG o PNG sin transparencia.
* Mínimo de `320x320px`, máximo de `3840x3840px`.

### Feature Graphic

* Aparece en la parte superior de la página de Google Play. [Ejemplo](http://i.imgur.com/cdcFMWb.jpg).
* `1024x500px`
* JPG o PNG sin transparencia.
* La imagen debe ser apta para todo tipo de pantallas por lo que no debería contener mucho texto.
* Si también se proporciona un video de vista previa, el feature graphic será algo así como el screenshot del video antes de que esté comience a reproducirse. Por lo tanto, es importante que el feature graphic sea llamativo y los usuarios al verlo quieran hacer click para que comience el video.

## Disclaimer

Los requerimientos descritos en esta página están sujetos a cambios sin previo aviso por parte de Apple y Google, por lo que igualmente deberían revisarse los requerimientos en sus páginas, para cersiorarse de que no hayan habido cambios en el sistema.


# Apple App Storage

Como práctica general, las aplicaciones serán publicadas por un miembro del equipo en testflight (beta testing). Luego el product-owner debe probar, aprobar y promover la aplicación a producción.

Para esto, el product-owner debe seguir los siguientes pasos:

* Ingresar a la sección *App Store*
* Seleccionar la versión a publicar, normalmente con la leyenda "x.x.x Prepare for Submission"
* En la sección *Version Information* completar el campo *What's New in This Version*
* En la sección *Build* hacer click en el signo (+) o en el link *Select a build before you submit your app*
* Seleccionar la versión a publicar.
* Grabar los cambios con el botón *Save* en la parte superior de la página.
* Enviar la aplicación a review con el botón *Submit for Review*.

El proceso de revisión por parte de Apple toma algunos días, en general menos de una semana.

![](/files/Kqz7VXxYnDqOQaKfGLvq)


# Google Play

Como práctica general las aplicaciones serán publicadas por un miembro del equipo en la sección **Beta** o **Alpha**. Lo ideal es que este proceso este configurado de forma automática, dependiendo del tipo de aplicación. Sin embargo aquí también se explica también la forma manual de subir el `.apk`.

### Subir de forma manual a la play store

* Se debe acceder a la sección de *Manage Releases* en la [Consola de Google Play](https://play.google.com/apps/publish/).
* Ir a la sección *App releases*. Dependiendo del estado de la aplicación:
  * *Pre-registration*
  * *Internal test track*
  * *Closed tracks* → **Alpha**
  * *Open track* → **Beta**
  * *Production track*
* Crear un nuevo release con el botón *Create release*.
* Rellenar la información en cada campo, incluir también el APK. Si no se rellena toda la infromación no se podrá hacer el *Rollout*.
* Guardar el release con el botón *Save*.
* Para preparar el release con el botón *Review*.
* Presionar en *Edit Release*.
* Ir a la sección *Review and roll out release* (Si hay errores aparecerán aquí y tendrás que arreglarlos).
* Presionar *Confirm rollout.*

Es importante enviar esta versión al grupo de testers y al Product Owner. Luego el Product Owner debe probar, aprobar y **promover la aplicación a producción** (presionando en el botón *Release to Production*). El procedimiento completo para el Product Owner es el siguiente.

### Publicar la aplicación

* Se debe acceder a la sección de *Manage Releases* en la [Consola de Google Play](https://play.google.com/apps/publish/).
* Seleccionar la opcion *Manage production* (o Alpha testing).
* Crear un nuevo release con el botón *Create release*.
* En la sección *APKS TO ADD* hacer click en el boton \**Add APK from library*.
* Seleccionar la versión a publicar.
* En la sección *WHAT'S NEW IN THIS RELEASE?* completar el campo con la información.
* Guardar el release con el botón *Save*.
* Revisar el release con el botón *Release*.
* Publicar la nueva versión con el botón *Start Rollout*.

Links útiles:

* [Upload an app](https://support.google.com/googleplay/android-developer/answer/113469?hl=en)
* [Prepare & roll out releases](https://support.google.com/googleplay/android-developer/answer/7159011)

![](/files/ZbMMDDQPpTD7jfHiYoeq)


# Upgrade de Vue 2 a Vue 3

1. Hacer tests de sistema para los flujos más importantes que tengan componentes de Vue.
2. Instalar la versión de compatibilidad de vue 3, siguiendo las instrucciones en <https://v3-migration.vuejs.org/migration-build.html> hasta el punto 3 (usar las instrucciones de webpack). Ignorar warnings de peer dependencies.

```
// config/webpack/environment.js
// ...
environment.config.merge({
  resolve: {
    alias: {
      'vue': '@vue/compat/dist/vue.esm-browser.js',
    },
  },
});
```

```
// config/webpack/loaders/vue.js

module.exports = {
  test: /\\.vue$/,
  loader: 'vue-loader',
  options: {
    compilerOptions: {
      compatConfig: {
        MODE: 2,
      },
    },
  },
};
```

1. Ejecutar `./bin/webpack-dev-server` en la carpeta del proyecto e ir resolviendo uno a uno los warnings y errores hasta que compile y los tests pasen.
2. Buscar alternativas a librerías usadas o actualizarlas.
   * vee-validate 3 -> vee-validate 4 (no tiene guía de migración, el autor recomienda tratar vee-validate 4 como una librería nueva)
   * vue-js-modal -> modal de [HeadlessUI](https://headlessui.com/)
   * v-tooltip -> [floating-vue](https://github.com/Akryum/floating-vue)
   * [vuex 3 -> vuex 4](https://vuex.vuejs.org/guide/migrating-to-4-0-from-3-x.html) (en un proyecto de cero recomendaría Pinia pero en un upgrade no vale la pena cambiar)
   * [vue-router 3 -> vue-router 4](https://router.vuejs.org/guide/migration/)
3. `npm ls vue` en la raiz del proyecto debería retornar *solo* vue 3.

```bash
❌
❯ npm ls vue
app@0.1.0 /project
├── vue@3.2.37
└─┬ vue-linkify@1.0.1
  └── vue@2.7.8

✅
❯ npm ls vue
app@0.1.0 /project
└── vue@3.2.37
```

1. Una vez que la app funcione, no necesariamente al 100% pero por lo menos que el trabajo de migración esté semicompleto, romper todo de nuevo instalando la receta de frontend de potassium. Acá hay tres alternativas:
   * Crear un proyecto nuevo con potassium en otra carpeta y comparar los cambios con lo que tienen, usando algo como [Beyond Compare](https://www.scootersoftware.com/) o [Meld](https://meldmerge.org/), para ir actualizando a mano.
   * Instalar la receta de frontend encima del proyecto, forzando la sobreescritura y revisar qué archivos sobran. Tiene la desventaja que hay que estar revisando el diff de git para ver qué cosas del proyecto cambiaron.
   * Instalar shakapacker a mano, siguiendo la [guía de migración](https://github.com/shakacode/shakapacker/blob/master/docs/v6_upgrade.md) y después revisar los cambios que hace la receta de frontend para que quede igual que el resto de los proyectos.
2. **No es necesario reescribir los componentes ya existentes a typescript**, se pueden ir migrando de a poco.
3. **No es necesario reescribir los componentes ya existentes a la Composition API + \*\*\*\*`script setup`**, se pueden ir migrando de a poco.
4. Al final de esto el proyecto debería quedar con Vue 3 (sin el build de compatibilidad a menos que no hayan encontrado alternativa a las librerías usadas), Tailwind 3 y Shakapacker.

### Comentarios Kalio

1. Los filtros ya no existen en vue3 (con la compatibility build tampoco funcionaban, al menos que haya hecho algo mal XD). lo que hice fue:
   * definir los filtros como funciones globales:

     ```javascript
     app.config.globalProperties.$filters = {
         camelizeKeys,
         camelize,
         deaccentisize,
         capitalize,
       };
     ```
   * luego se pueden usar asi: `:startup="$filters.camelizeKeys(<%= @startup.to_json %>)"`
   * en caso de que el proyecto use filtros nesteados, por ej. `filtro1 | filtro2 | filtro3`, se puede definir una función que los aplique en orden:

     ```javascript
     export function filterChain(value, filters) {
       if (!value) return null;

       let result = value;
       filters.forEach((filter) => {
         result = filter(result);
       });

       return result;
     }
     ```

     y luego se puede usar asi:

     ```javascript
     $filters.filterChain(state, [$filters.capitalize, $filters.split])
     ```
2. En vue3 `v-bind="$attrs"` reemplaza a `v-on="$listeners`. Si se quiere agregar algun input custom o modificar alguno, se puede hacer asi:

   ```javascript
   attrs() {
       return {
         ...this.$attrs,
         onInput: (event) => this.$emit('update:modelValue', event.target.value),
       };
     },
   ```

   notar que los eventos vienen prefixeados con `on`
3. `beforeDestroy` esta deprecated, no se si con la compatibility build sigue funcionando, en vue3 hay que remplazarlo por `beforeUnmount`
4. Si se usaba un EventBus con `Vue`, se puede reemplazar por algo como <https://github.com/developit/mitt>, o <https://vueuse.org/core/useEventBus/>


# Migración Hound → reviewdog

### Contexto

Desde hace un tiempo estamos pasando los proyectos que usaban Hound para el linting a reviewdog. Hound corría en un servidor hosteado por nosotros y usaba una versión de Hound de cuando el proyecto era completamente open-source. Lamentablemente ciertas partes del proyecto pasaron a ser closed-source por lo que nos quedamos con una versión antigua.

Además, Hound usaba una versión particular de los linters, por lo que al hacer una actualización de rubocop por ejemplo, había que actualizar todos los proyectos para que fueran compatibles, haciendo que la actualización de reglas ocurriera muy esporádicamente.

Reviewdog es una especie de formateador de los outputs de los linters, y se puede correr donde uno quiera. Esto nos permite correrlo en CircleCI y no depende de versiones particulares de los linters, simplemente recibe el output de un linter y reporta a GitHub el comentario correspondiente.

### Pasos para migrar un proyecto antiguo

1. Los proyectos actualmente no cuentan con los linters como dependencias por lo que se deben agregar. Para eso se deben *lockear* a `devDependencies` del `package.json` las siguientes librerías: `eslint`, sus respectivos plugins como `eslint-plugin-import` o `eslint-plugin-vue` y `stylelint`.

* **Ejemplo**: <https://github.com/platanus/mok/blob/master/package.json>

1. Por el lado de ruby, se deben agregar al `Gemfile` bajo el grupo `:development, :test` todas las gemas relativas al linting, esto es `rubocop` y los plugins como `rubocop-rspec`, `rubocop-rails`, etc.

* **Ejemplo**: <https://github.com/platanus/mok/blob/master/Gemfile>
* **Nota:** rubocop suele introducir breaking changes en cada actualización de minor antes de que lleguen a la versión `1.0`, por lo que se debería dejar la versión especificada al menos como `~> 0.82.0`, es decir, fijando el minor pero permitiendo updates al patch.

1. Copiar las reglas de cada linter a la raíz del proyecto, esto implica los archivos: `.eslintrc.json` , `.rubocop.yml` y `.stylelintrc.json`. En [Potassium](https://github.com/platanus/potassium) están incluidos los archivos, considerando el de rubocop actualizado para ser compatible con la versión `0.82`.

* **Nota**: Al terminar este paso ya tenemos los linters con sus reglas en el proyecto, lo siguiente es correr los linters en CircleCI.

1. Actualizar el archivo `config.yml` incluido en cada proyecto. Para esta actualización hay que basarse en el archivo incluido [aquí](https://github.com/platanus/potassium/blob/master/lib/potassium/assets/.circleci/config.yml.erb). En primer lugar el job llamado `build` se renombra a `test` y en segundo lugar se agrega un nuevo job llamado `lint`. Este último agrega los pasos necesarios para realizar linting en el proyecto usando caché; se puede copiar entero sin cambiar. Finalmente se agrega una sección `workflows` que especifica cómo se deben correr ambos jobs, en este caso en paralelo y utilizando un contexto global para el de linting.

* \*\*Nota 1: \*\*si no se requiere linting de `eslint` o de `stylelint` se pueden eliminar los steps correspondientes a cada uno, y si no se va a usar ninguno se pueden eliminar los steps correspondientes a dependencias de `yarn` y su caché.
* \*\*Nota 2: \*\*el contexto llamado `org-global` lo que hace es incluir la variable de entorno necesaria (`REVIEWDOG_GITHUB_API_TOKEN`) para que reviewdog reporte a GitHub.
* \*\*Nota 3: \*\*el job de linting no va a reportar una falla (o una cruz roja) a GitHub en caso de encontrar violaciones, solo va a comentar el PR. Esto se condice con el comportamiento que tenía Hound.

1. Asumiendo que ya está CircleCI activado en el proyecto, se debe ir a la configuración del proyecto para marcar que solo corra los builds en PRs. Esto es necesario porque reviewdog no reporta nada si no hay un PR en el que hacerlo. Para activar esta configuración ir en los settings del proyecto a la sección `Advanced`. Ahí seleccionar `Only build pull requests` como se ve en la foto.

* **Nota**: Para la rama master igual se van a correr los tests al mergear un PR pero no se reportarán errores de linting.

![](/files/sJ7CNrqRzWTuYKx2QwRf)


# Upgrade de Postgresql

## Upgrade de Postgresql

original:

### Índice

1. [Intro](#intro)
2. [Pasos a seguir](#pasos-a-seguir)
3. [Resumen](#resumen)

## Intro

Caso de ejemplo: *del 10 al 11*

**Base de datos a upgradear**: [postgresql-elliptical-29911](https://data.heroku.com/datastores/d7e170f0-29ee-4f26-ae26-fb7eaeca7ed7)

El upgrade de postgres, en este caso debe hacerse con el comando `pg:upgrade`, el cual se usa solamente para hacer un upgrade de una “follower database”. Debido a que la que vamos a actualizar es una base de datos tipo `leader` y `primary`, se debe considerar hacer varios pasos antes de hacer el upgrade.

> 💡 OJO esto es solo para bds que no son del tipo hobbie-tier y que pesan más de 10gb!

## Pasos a seguir

Según <https://devcenter.heroku.com/articles/upgrading-heroku-postgres-databases#upgrading-with-pg-upgrade>

#### 1. Proveer una follower database

Para empezar, hay que crear un nuevo follower para la base de datos, y esperar a que se “ponga al día” con al leader.

```bash
**$ heroku addons:create ****heroku-postgresql:standard-0**** --follow ****HEROKU_POSTGRESQL_WHITE_URL**** --app pl-rutt-production**
# te va a salir lo siguiente:
Adding heroku-postgresql:standard-0 to pl-rutt-production... done, v71 ($200/mo)
Attached as HEROKU_POSTGRESQL_XX
Follower will become available for read-only queries when up-to-date
Use `heroku pg:wait` to track status
# ------

**$ heroku pg:wait --app pl-rutt-production**
Waiting for database HEROKU_POSTGRESQL_XX_URL... available
```

La follower es considerada “caught up” cuando está dentro de los 200 commits o condirmaciones de la bbdd principal. Se puede veridicar cuántos commits tiene la follower usando el comando `pg:info`

```bash
**$ heroku pg:info --app pl-rutt-production**
=== HEROKU_POSTGRESQL_WHITE_URL, DATABASE_URL
Plan:                  Standard 0
Status:                Available
...
=== HEROKU_POSTGRESQL_PUCE_URL
Plan:                  Standard 0
Status:                Available
..
=== HEROKU_POSTGRESQL_XX
Plan:                  Standard 0
Status:                Available
...
Following: HEROKU_POSTGRESQL_WHITE (DATABASE_URL)
Behind By: 123 commits
```

#### 2. Entrar en un modo mantención para prevenir las escrituras en la base de datos.

> 💡 El modo de mantenimiento no *"scale down"* automáticamente los dynos. Los dynos web y no web deben *"scale down"* (por ejemplo, heroku ps:scale worker=0) para garantizar que ninguna conexión esté escribiendo datos en la base de datos.

Es importante que ninguna data nueva sea escrita en la primary database ([postgresql-elliptical-29911](https://data.heroku.com/datastores/d7e170f0-29ee-4f26-ae26-fb7eaeca7ed7)) durante el proceso de upgrade, porque sino, esta no va a ser transferida a la nueva base de datos. Para lograr esto, hay que poner la bbdd en modo mantención. Si hay scheduler jobs ejecudandose, también se deben deshabilitar.

**Si tienes dynos asociados corriendo (web y no web)**

````
Ejecuta el siguiente comando para saber la escala de los dynos:

```javascript
**$ heroku ps --app pl-rutt-production
**=== web (Standard-2X): bundle exec puma -C ./config/puma.rb (2)
web.1: up 2022/10/19 23:04:41 -0300 (~ 3h ago)
web.2: up 2022/10/19 04:29:34 -0300 (~ 22h ago)

=== worker (Standard-2X): bundle exec sidekiq (1)
worker.1: up 2022/10/20 02:24:47 -0300 (~ 37m ago)
```

Luego, debes ejecutar el siguiente comando para reducirlos:

`heroku ps:scale web=0 worker=0 --app pl-rutt-production` 
````

```bash
**$ heroku maintenance:on --app pl-rutt-production
**Enabling maintenance mode for pl-rut-production... done
```

#### 3. Actualizar las base de datos tipo follower

Ahora se puede actualizar la base de datos secundaria. Hay que esperar a que la follower database se ponga al día por completo con la principal, en la que debería decir que esta “behind by: 0 commits”.

```
$ **heroku pg:info --app pl-rutt-production**
=== HEROKU_POSTGRESQL_WHITE_URL
Plan:        Standard 0
Status:      available
...
=== HEROKU_POSTGRESQL_XX_URL
Plan:        Standard 2
Status:      available
...
Following:   HEROKU_POSTGRESQL_WHITE_URL (DATABASE_URL)
Behind By:   **0 commits**
```

Si no se espera a que quede “caughted up” el upgrade te va a tirar el siguiente error (probablemente)

`▸ database must not be too far behind leader, please wait until your follower catches up with its leader.`

Una vez que esté en el estado “caught up” se puede hacer el upgrade. Lo ideal es que, como estamos en la versión 10, la actualicemos a la 11.

Entonces, el comando sería más o menos así:

`heroku pg:upgrade ``HEROKU_POSTGRESQL_XX`` ``--version 11`` --app pl-rutt-production`

Este paso aproximadamente durará como 20 min en completarse. De hecho, sale que es aprox 3 min por gb, así que podrían ser en verdad 19,4 GB \* 3 min/GB ≈ 60 minutos 🙀

De todas maneras, se puede ver el progreso del upgrade con el comando

`heroku pg:wait --app pl-rutt-production`

#### 4. Promote to Primary 🎉

Ahora se debe “promote” la bbdd recién actualizada para configurarla como la bbdd principal (`DATABASE_URL`). Para ello, se debe hacer un `pg:promote`, que también crea un archivo adjunto alternativo para la antigua base de datos princial, asignado con una nueva variable de configuración `HEROKU_POSTGRESQL_<color>_URL`. El proceso de *promotion* activa un lanzamiento y reinicia la app.

`heroku pg:promote HEROKU_POSTGRESQL_XX --app pl-rutt-production`

ahora la base de datos follower es la principal, aunque aún no recibe nuevas solicitudes.

Si la bbdd principal original se adjuntó a varias apps, se debe adjuntar la nueva bbdd de esas aplicaciones con la extensión `heroku addons:attach ...`.

> 💡 \*\*IMPORTANTE!!! \*\*Después del *promotion* los followers de la bbdd principal original NO comienzan a seguir de manera automática la nueva bbdd principal. Por lo que se debe crear nuevos followers para la nueva bbdd principal, según sea necesario:

```
`heroku addons:create heroku-postgresql:standard-0/ --follow DATABASE_URL --app example-app`
```

#### 5. Salir del modo mantención

Para reanudar el funcionamiento normal de la app, se debe volver a escalar los non-web dynos a sus niveles originales. (e.g., `heroku ps:scale worker=1` ).

**Si tienes dynos asociados que deben correr (web y no web)**

```
Debes ejecutar el siguiente comando para volverlos a escalar:

`heroku ps:scale ``web=2 worker=1`` --app pl-rutt-production` 
```

**Si la primaria que estás actualizando tenía followers**

```
Tienes que volver a crear las followers que tenía. Ejemplo:

`heroku addons:create heroku-postgresql:standard-0 --follow DATABASE_URL -a pl-rutt-production`
```

Finalmente, se apaga el maintenance mode, con el siguiente comando:

```javascript
**$ heroku maintenance:off --app pl-rutt-production**
```

Ahora la app puede recibir requests a la bbdd actualizada. Se puede confirmar esto ejecutando el comando `heroku pg:info --app pl-rutt-production`. La bbdd denotada por `DATABASE_URL` es la considerada como primaria.

Finalmente, debes sacar las bases de datos deprecadas:

```bash
**$ heroku addons:destroy ****HEROKU_POSTGRESQL_WHITE**** --app pl-rutt-production**
```

***

## Resumen

Pasos a seguir:

* [ ] `heroku addons:create heroku-postgresql:standard-0 --follow HEROKU_POSTGRESQL_WHITE_URL --app pl-rutt-production`

  Te crea la bd follower en la que vas a copiar la BD primaria para hacer el upgrade.

  OJO es útil recordar el nombre de la nueva base de datos. En mi caso, se llamaba *HEROKU\_POSTGRESQL\_COPPER\_URL*.
* [ ] `heroku pg:wait --app pl-rutt-production`

  este te ayuda a ver el estado de la bd que creaste

  **si tienes dynos corriendo**

  * [ ] `heroku ps --app pl-rutt-production`

    este comando ayuda a ver la scale que se debe volver a poner una vez actualizada la bd.

    En mi caso me salió esto:

    ```bash
    === web (Standard-2X): bundle exec puma -C ./config/puma.rb (2)
    web.1: up 2022/10/19 23:04:41 -0300 (~ 3h ago)
    web.2: up 2022/10/19 04:29:34 -0300 (~ 22h ago)

    === worker (Standard-2X): bundle exec sidekiq (1)
    worker.1: up 2022/10/20 02:24:47 -0300 (~ 37m ago)
    ```

    Así yo sabía que eran\*\* 2 web\*\* y **1 worker**.
  * [ ] `heroku ps:scale web=0 worker=0 --app pl-rutt-production`

    este permite que los workers dejen de funcionar, para que así no traten de escribir en la BD
* [ ] `heroku maintenance:on --app pl-rutt-production`

  con este comando vas a hacer que la app pase a estado de mantención, por lo que no se podrá escribir en la BD primaria
* [ ] `heroku pg:info --app pl-rutt-production`

  este comando te ayuda a ver si la base de datos que creaste está a la par con la primaria. ¿Cómo saberlo? debería decir esto debajo del nombre de la bd creada:

  ```bash
  Following:   HEROKU_POSTGRESQL_WHITE_URL (DATABASE_URL)
  Behind By:   **0 commits**
  ```
* [ ] `heroku pg:upgrade HEROKU_POSTGRESQL_COPPER_URL --version 11 --app pl-rutt-production`

  Este comando se debe ejecutar una vez que la bd creada está a la par con la primaria. Con este haces el upgrade. En mi caso se demoró unos 20 mins
* [ ] `heroku pg:wait --app pl-rutt-production`

  este comando te ayudará a ver el progreso del update
* [ ] `heroku pg:promote HEROKU_POSTGRESQL_COPPER_URL --app pl-rutt-production`

  este comando hace que ahora la bd primaria es la creada y actualizada.

  **\[ANTES DE APAGAR EL MODO MANTENCIÓN] si tienes dynos:**

  * [ ] `heroku ps:scale web=2 worker=1 --app pl-rutt-production`

    Con este comando volví a la escala que tenía antes.

  \*\*Si tienes en un principio la primaria tenía una bd follower \*\*

  * [ ] `heroku addons:create heroku-postgresql:standard-0 --follow DATABASE_URL -a pl-rutt-production`

    tienes que crear una follower nueva. CREO que no es necesario esperar a que se copie entera para apagar la mantención, pero quizá mejor esperar (por si las 🪰🪰)
* [ ] `heroku maintenance:off --app pl-rutt-production`

  Y ahí vuelve todo a la normalidad
* [ ] `heroku addons:destroy HEROKU_POSTGRESQL_WHITE --app pl-rutt-production`

  OJO: hay que sacar dps las bases de datos que sobren, es decir, las antiguas. En mi caso, la primaria ERA *HEROKU\_POSTGRESQL\_WHITE* así que tengo que eliminar esta.

LISTOOOO!! 🎉🎉


