diff --git a/.rubocop.yml b/.rubocop.yml index f77b4a3..37f238b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,22 +1,6 @@ -inherit_from: .rubocop_todo.yml +#inherit_from: .rubocop_todo.yml -# No siempre queremos metadatear los modelos -Rails/CreateTableWithTimestamps: - Enabled: false # Queremos poder hacer comentarios en castellano, esto quiere que # saquemos las tildes Style/AsciiComments: Enabled: false -# El estilo de módulo y clases anidados agrega mucha indentación para -# nuestros gusto -Style/ClassAndModuleChildren: - Enabled: false -Metrics/LineLength: - Exclude: - - 'db/schema.rb' -Metrics/BlockLength: - Exclude: - - 'db/schema.rb' - - 'config/routes.rb' - - 'db/seeds.rb' - - 'config/environments/production.rb' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml deleted file mode 100644 index 08012f6..0000000 --- a/.rubocop_todo.yml +++ /dev/null @@ -1,80 +0,0 @@ -# This configuration was generated by -# `rubocop --auto-gen-config` -# on 2019-04-05 17:51:53 -0300 using RuboCop version 0.66.0. -# The point is for the user to remove these configuration records -# one by one as the offenses are removed from the code base. -# Note that changes in the inspected code, or installation of new -# versions of RuboCop, may require this file to be generated again. - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: empty_lines, no_empty_lines -Layout/EmptyLinesAroundBlockBody: - Exclude: - - 'db/schema.rb' - -# Offense count: 1 -Metrics/AbcSize: - Max: 18 - -# Offense count: 1 -Metrics/CyclomaticComplexity: - Max: 9 - -# Offense count: 2 -# Configuration parameters: CountComments, ExcludedMethods. -Metrics/MethodLength: - Max: 13 - -# Offense count: 1 -Metrics/PerceivedComplexity: - Max: 9 - -# Offense count: 1 -Style/Documentation: - Exclude: - - 'spec/**/*' - - 'test/**/*' - - 'config/application.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: when_needed, always, never -Style/FrozenStringLiteralComment: - Exclude: - - 'db/schema.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -Style/IfUnlessModifier: - Exclude: - - 'bin/bundle' - -# Offense count: 2 -Style/MixinUsage: - Exclude: - - 'bin/setup' - - 'bin/update' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: MinDigits, Strict. -Style/NumericLiterals: - Exclude: - - 'db/schema.rb' - -# Offense count: 4 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. -# SupportedStyles: single_quotes, double_quotes -Style/StringLiterals: - Exclude: - - 'db/schema.rb' - -# Offense count: 12 -# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. -# URISchemes: http, https -Metrics/LineLength: - Max: 190 diff --git a/Gemfile b/Gemfile index 454e298..9c93267 100644 --- a/Gemfile +++ b/Gemfile @@ -40,7 +40,7 @@ group :development do gem 'listen', '>= 3.0.5', '< 3.2' # Spring speeds up development by keeping your application running in # the background. Read more: https://github.com/rails/spring - gem 'rubocop' + gem 'rubocop-rails' gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' gem 'yard' diff --git a/Gemfile.lock b/Gemfile.lock index 5eb27d5..b28099e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -64,7 +64,7 @@ GEM hkdf (0.3.0) i18n (1.6.0) concurrent-ruby (~> 1.0) - jaro_winkler (1.5.2) + jaro_winkler (1.5.3) jbuilder (2.8.0) activesupport (>= 4.2.0) multi_json (>= 1.2) @@ -90,13 +90,12 @@ GEM nio4r (2.3.1) nokogiri (1.10.3) mini_portile2 (~> 2.4.0) - parallel (1.16.2) - parser (2.6.2.0) + parallel (1.17.0) + parser (2.6.3.0) ast (~> 2.4.0) pry (0.12.2) coderay (~> 1.1.0) method_source (~> 0.9.0) - psych (3.1.0) public_suffix (4.0.0) puma (3.12.1) rack (2.0.7) @@ -132,15 +131,17 @@ GEM rb-fsevent (0.10.3) rb-inotify (0.10.0) ffi (~> 1.0) - rubocop (0.66.0) + rubocop (0.72.0) jaro_winkler (~> 1.5.1) parallel (~> 1.10) - parser (>= 2.5, != 2.5.1.1) - psych (>= 3.1.0) + parser (>= 2.6) rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 1.6) - ruby-progressbar (1.10.0) + unicode-display_width (>= 1.4.0, < 1.7) + rubocop-rails (2.2.0) + rack (>= 1.1) + rubocop (>= 0.72.0) + ruby-progressbar (1.10.1) ruby_dep (1.5.0) spring (2.0.2) activesupport (>= 4.2) @@ -159,7 +160,7 @@ GEM thread_safe (0.3.6) tzinfo (1.2.5) thread_safe (~> 0.1) - unicode-display_width (1.5.0) + unicode-display_width (1.6.0) validate_url (1.0.8) activemodel (>= 3.0.0) public_suffix @@ -185,7 +186,7 @@ DEPENDENCIES puma (~> 3.11) rack-cors rails (~> 5.2.3) - rubocop + rubocop-rails spring spring-watcher-listen (~> 2.0.0) sqlite3 diff --git a/app/controllers/barcas_controller.rb b/app/controllers/barcas_controller.rb index 7b76404..92335e3 100644 --- a/app/controllers/barcas_controller.rb +++ b/app/controllers/barcas_controller.rb @@ -47,6 +47,9 @@ class BarcasController < ApplicationController @barca.piratas << current_pirata if @barca.save + piratas = Pirata.todas_menos(current_pirata).pluck(:id) + notify(subject: 'barcas.create.subject', piratas: piratas) + render status: :created else render json: { errors: @barca.errors.messages }, @@ -129,4 +132,17 @@ class BarcasController < ApplicationController def find_barca @barca = Barca.find(params[:barca_id]) end + + def notify(piratas:, subject:) + # Notificar a todas las piratas que hay una nueva barca para que + # se puedan sumar + payload = WebpushPayload.new(subject: I18n.t(subject, + barca: @barca.nombre), + message: @barca.descripcion, + endpoint: barca_path(@barca)) + + WebpushJob.perform_later(piratas: piratas, + ttl: 7.days.to_i, + payload: payload.to_json) + end end diff --git a/app/controllers/consensos_controller.rb b/app/controllers/consensos_controller.rb index 50be7af..6ac7e77 100644 --- a/app/controllers/consensos_controller.rb +++ b/app/controllers/consensos_controller.rb @@ -66,8 +66,7 @@ class ConsensosController < ApplicationController begin @consenso = @barca.consensos.find(params[:id]) rescue ActiveRecord::RecordNotFound - render json: {}, status: :not_found - return + render(json: {}, status: :not_found) && return end if @consenso.posiciones.empty? && @consenso.destroy diff --git a/app/controllers/posiciones_controller.rb b/app/controllers/posiciones_controller.rb index aff68d7..05fb0eb 100644 --- a/app/controllers/posiciones_controller.rb +++ b/app/controllers/posiciones_controller.rb @@ -15,7 +15,8 @@ class PosicionesController < ApplicationController # Crea una posición dentro de un consenso # # @param :consenso_id [Integer] El ID del consenso - # @param :posicion [Hash] { posicion: { estado: @string, comentario: @string } } + # @param :posicion [Hash] { posicion: { estado: @string, + # comentario: @string } } # @return [Hash] { id: @int, estado: @string, comentario: @string, # pirata: @pirata } def create diff --git a/app/jobs/webpush_job.rb b/app/jobs/webpush_job.rb new file mode 100644 index 0000000..f9804e7 --- /dev/null +++ b/app/jobs/webpush_job.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# Envía las notificaciones +class WebpushJob < ApplicationJob + queue_as :default + + # @param :piratas [String,Array] 'all' para traer todas, array de IDs + # para filtrar + # @param :payload [WebpushPayload] La notificación a enviar + # @param :urgency [Symbol] La urgencia + # @param :ttl [Any] Duración convertible a segundos + def perform(piratas:, payload:, urgency: :normal, ttl: 1.day.to_i) + # Encontrar todas las piratas + piratas = find_piratas(piratas) + payload = WebpushPayload.from_json payload + + # No cargar todas las piratas en memoria! + piratas.find_each do |pirata| + # Enviarles a todas sus suscripciones + pirata.webpush_subscriptions.find_each do |subscription| + subscription.payload_send(payload: payload, + urgency: urgency, + ttl: ttl) + end + end + end + + private + + def find_piratas(ids) + if ids == 'all' + Pirata.all + else + Pirata.where(id: ids) + end + end +end diff --git a/app/models/pirata.rb b/app/models/pirata.rb index 914a410..0816708 100644 --- a/app/models/pirata.rb +++ b/app/models/pirata.rb @@ -18,4 +18,6 @@ class Pirata < ApplicationRecord # Una por correo validates :email, presence: true, uniqueness: true validates :nick, presence: true, uniqueness: true + + scope :todas_menos, ->(pirata) { where.not(id: pirata) } end diff --git a/app/models/webpush_payload.rb b/app/models/webpush_payload.rb new file mode 100644 index 0000000..dd623db --- /dev/null +++ b/app/models/webpush_payload.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# El payload que se envía en la notificación +# +# @param :subject [String] El título de la notificación +# @param :message [String] El cuerpo de la notificación +# @param :endpoint [String] La URL a visitar al abrir la notificación +WebpushPayload = Struct.new(:subject, :message, :endpoint, + keyword_init: true) do + # Convertir el payload a JSON para poder enviarlo + # + # @return String + def to_json(opts = nil) + to_h.to_json(opts) + end + + def self.from_json(string) + new(JSON.parse(string)) + end +end diff --git a/app/models/webpush_subscription.rb b/app/models/webpush_subscription.rb index 7d0051d..33efcd2 100644 --- a/app/models/webpush_subscription.rb +++ b/app/models/webpush_subscription.rb @@ -6,4 +6,30 @@ class WebpushSubscription < ApplicationRecord validates :endpoint, url: true validates_uniqueness_of :auth, scope: %i[p256dh endpoint], case_sensitive: false + + URGENCY = %w[very-low low normal high].freeze + + # Envía la notificación + def payload_send(payload:, urgency: :normal, ttl: 1.day) + Webpush.payload_send( + vapid: { + subject: 'mailto:lumi@partidopirata.com.ar', + public_key: Rails.application.credentials.vapid.public_key, + private_key: Rails.application.credentials.vapid.private_key + }, + endpoint: endpoint, auth: auth, p256dh: p256dh, + ttl: ttl.to_i, urgency: normalize_urgency(urgency), + message: payload.to_json + ) + end + + private + + def normalize_urgency(urgency) + if URGENCY.includes? urgency.to_s + urgency.to_s + else + 'normal' + end + end end diff --git a/config/locales/en.yml b/config/locales/en.yml index decc5a8..7d88ef3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,33 +1,4 @@ -# Files in the config/locales directory are used for internationalization -# and are automatically loaded by Rails. If you want to use locales other -# than English, add the necessary files in this directory. -# -# To use the locales, use `I18n.t`: -# -# I18n.t 'hello' -# -# In views, this is aliased to just `t`: -# -# <%= t('hello') %> -# -# To use a different locale, set it with `I18n.locale`: -# -# I18n.locale = :es -# -# This would use the information in config/locales/es.yml. -# -# The following keys must be escaped otherwise they will not be retrieved by -# the default I18n backend: -# -# true, false, on, off, yes, no -# -# Instead, surround them with single quotes. -# -# en: -# 'true': 'foo' -# -# To learn more, please read the Rails Internationalization guide -# available at http://guides.rubyonrails.org/i18n.html. - en: - hello: "Hello world" + barcas: + create: + subject: '%{barca} has been created!' diff --git a/config/locales/es.yml b/config/locales/es.yml new file mode 100644 index 0000000..2f4423d --- /dev/null +++ b/config/locales/es.yml @@ -0,0 +1,4 @@ +es: + barcas: + create: + subject: '%{barca} creada, ¡al abordaje!' diff --git a/test/jobs/webpush_job_test.rb b/test/jobs/webpush_job_test.rb new file mode 100644 index 0000000..3c00301 --- /dev/null +++ b/test/jobs/webpush_job_test.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'test_helper' + +class WebpushJobTest < ActiveJob::TestCase + test 'se pueden enviar a todas las piratas' do + payload = WebpushPayload.new(subject: 'test', message: 'test', endpoint: 'https://partidopirata.com.ar') + + assert_nothing_raised do + WebpushJob.perform_now(piratas: 'all', payload: payload.to_json, + urgency: :low, ttl: 1.day.to_i) + end + end + + test 'se pueden enviar a algunas piratas' do + pirata = create :pirata + payload = WebpushPayload.new(subject: 'test', message: 'test', endpoint: 'https://partidopirata.com.ar') + + assert_nothing_raised do + WebpushJob.perform_now(piratas: [pirata.id], payload: payload.to_json, + urgency: :low, ttl: 1.day.to_i) + end + end +end