Skip to content

Commit

Permalink
Merge pull request #35 from sad16/15-background-jobs
Browse files Browse the repository at this point in the history
background jobs
  • Loading branch information
sad16 committed Aug 26, 2021
2 parents 033f8ce + 4d9bf03 commit 4c58541
Show file tree
Hide file tree
Showing 46 changed files with 581 additions and 5 deletions.
3 changes: 3 additions & 0 deletions Gemfile
Expand Up @@ -51,6 +51,9 @@ gem 'pundit'
gem 'doorkeeper'
gem 'active_model_serializers', '~> 0.10'
gem 'oj'
gem 'sidekiq'
gem 'whenever', require: false
gem 'redis'

group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
Expand Down
12 changes: 12 additions & 0 deletions Gemfile.lock
Expand Up @@ -87,6 +87,7 @@ GEM
activesupport
childprocess (0.9.0)
ffi (~> 1.0, >= 1.0.11)
chronic (0.10.2)
cocoon (1.2.15)
coderay (1.1.3)
coffee-rails (4.2.2)
Expand All @@ -98,6 +99,7 @@ GEM
coffee-script-source (1.12.2)
commonjs (0.2.7)
concurrent-ruby (1.1.5)
connection_pool (2.2.5)
crass (1.0.4)
devise (4.7.3)
bcrypt (~> 3.0)
Expand Down Expand Up @@ -251,6 +253,7 @@ GEM
rb-fsevent (0.10.3)
rb-inotify (0.10.0)
ffi (~> 1.0)
redis (4.4.0)
regexp_parser (1.3.0)
request_store (1.5.0)
rack (>= 1.4)
Expand Down Expand Up @@ -293,6 +296,10 @@ GEM
rubyzip (~> 1.2, >= 1.2.2)
shoulda-matchers (4.0.1)
activesupport (>= 4.2.0)
sidekiq (6.2.2)
connection_pool (>= 2.2.2)
rack (~> 2.0)
redis (>= 4.2.0)
slim (4.0.1)
temple (>= 0.7.6, < 0.9)
tilt (>= 2.0.6, < 2.1)
Expand Down Expand Up @@ -345,6 +352,8 @@ GEM
websocket-driver (0.7.0)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3)
whenever (1.0.0)
chronic (>= 0.6.3)
with_model (2.1.5)
activerecord (>= 5.2)
xpath (3.2.0)
Expand Down Expand Up @@ -381,10 +390,12 @@ DEPENDENCIES
pundit
rails (~> 5.2.2)
rails-controller-testing
redis
rspec-rails (~> 3.8)
sass-rails (~> 5.0)
selenium-webdriver
shoulda-matchers
sidekiq
slim-rails
spring
spring-watcher-listen (~> 2.0.0)
Expand All @@ -395,6 +406,7 @@ DEPENDENCIES
validate_url
web-console (>= 3.3.0)
webdrivers (~> 4.0)
whenever
with_model

RUBY VERSION
Expand Down
50 changes: 50 additions & 0 deletions app/assets/javascripts/notifications.js
@@ -0,0 +1,50 @@
$(document).on('turbolinks:load', function() {
onAjaxSuccessSubscribeNotification($('.subscribe-notification-link'));
onAjaxSuccessUnsubscribeNotification($(`.unsubscribe-notification-link`));
});

function onAjaxSuccessSubscribeNotification(elem) {
$(elem)
.on('ajax:success', function(response) {
successSubscribeNotification(response, this);
})
.on('ajax:error', function(response) {
errorSubscribeNotification(response, this);
})
}

function successSubscribeNotification(response, elem) {
var notification = response.detail[0]['notification'];
var notificationBlock = $(elem).closest('.notification');
insertUnsubscribeNotificationLink(notification, notificationBlock);
onAjaxSuccessUnsubscribeNotification(notificationBlock.find('.unsubscribe-notification-link'));
}

function insertUnsubscribeNotificationLink(notification, notificationBlock) {
var unsubscribeNotificationLink = `<p><a class="unsubscribe-notification-link" rel="nofollow" data-method="delete" data-remote="true" href="/notifications/${notification.id}">Unsubscribe</a></p>`
notificationBlock.html(unsubscribeNotificationLink);
}

function onAjaxSuccessUnsubscribeNotification(elem) {
$(elem).on('ajax:success', function(response) {
successUnsubscribeNotification(response, this);
});
}

function successUnsubscribeNotification(response, elem) {
var notification = response.detail[0]['notification'];
var notificationBlock = $(elem).closest('.notification');
insertSubscribeNotificationLink(notification, notificationBlock);
onAjaxSuccessSubscribeNotification(notificationBlock.find('.subscribe-notification-link'));
}

function insertSubscribeNotificationLink(notification, notificationBlock) {
var subscribeNotificationLink = `<p><a class="subscribe-notification-link" rel="nofollow" data-method="post" data-remote="true" href="/questions/${notification.question_id}/notifications">Subscribe</a></p>`
notificationBlock.html(subscribeNotificationLink);
}

function errorSubscribeNotification(response, elem) {
var errors = response.detail[0]['errors'];
var notificationBlock = $(elem).closest('.notification');
notificationBlock.append(errors);
}
24 changes: 24 additions & 0 deletions app/controllers/notifications_controller.rb
@@ -0,0 +1,24 @@
class NotificationsController < ApplicationController
before_action :authenticate_user!, only: [:create, :destroy]

def create
question = Question.find(params[:question_id])
notification = Notification.new(user: current_user, question: question)

if notification.save
render json: notification
else
render json: { errors: notification.errors.full_messages }, status: :unprocessable_entity
end
end

def destroy
notification = Notification.find(params[:id])

authorize notification

notification.destroy

render json: notification
end
end
1 change: 1 addition & 0 deletions app/controllers/questions_controller.rb
Expand Up @@ -13,6 +13,7 @@ def show
@answer = @question.answers.new
@answer.links.new
@vote = current_user&.vote_by(@question) || @question.votes.new
@notification = Notification.find_by(user: current_user, question: @question)
set_gon
end

Expand Down
7 changes: 7 additions & 0 deletions app/jobs/daily_digest_job.rb
@@ -0,0 +1,7 @@
class DailyDigestJob < ApplicationJob
queue_as :default

def perform
Services::SendDailyDigest.new.call
end
end
7 changes: 7 additions & 0 deletions app/jobs/notifications_job.rb
@@ -0,0 +1,7 @@
class NotificationsJob < ApplicationJob
queue_as :default

def perform(answer)
Services::SendNotifications.new.call(answer)
end
end
6 changes: 6 additions & 0 deletions app/mailers/daily_digest_mailer.rb
@@ -0,0 +1,6 @@
class DailyDigestMailer < ApplicationMailer
def digest(user)
@questions_data = Services::DailyDigestData.new.call
mail to: user.email
end
end
6 changes: 6 additions & 0 deletions app/mailers/notification_mailer.rb
@@ -0,0 +1,6 @@
class NotificationMailer < ApplicationMailer
def new_answer(user, answer)
@answer = answer
mail to: user.email
end
end
8 changes: 8 additions & 0 deletions app/models/answer.rb
Expand Up @@ -17,6 +17,8 @@ class Answer < ApplicationRecord

scope :bests, -> { where(question: Question.with_best_answer) }

after_create_commit :send_notification

def mark_as_best
question.update(best_answer_id: id)
end
Expand All @@ -32,4 +34,10 @@ def assign_reward
def unassign_reward
reward&.unassign
end

private

def send_notification
NotificationsJob.perform_later(self)
end
end
6 changes: 6 additions & 0 deletions app/models/notification.rb
@@ -0,0 +1,6 @@
class Notification < ApplicationRecord
belongs_to :user
belongs_to :question

validates :user_id, uniqueness: { scope: [:question_id] }
end
10 changes: 10 additions & 0 deletions app/models/question.rb
Expand Up @@ -9,6 +9,8 @@ class Question < ApplicationRecord
has_one :reward, dependent: :destroy

has_many :answers, dependent: :destroy
has_many :notifications, dependent: :destroy
has_many :subscribed_users, through: :notifications, source: :user

has_many_attached :files

Expand All @@ -18,7 +20,15 @@ class Question < ApplicationRecord

scope :with_best_answer, -> { where.not(best_answer_id: nil) }

after_create_commit :create_notification

def answers_without_best
answers.where.not(id: best_answer_id)
end

private

def create_notification
notifications.create(user_id: user_id)
end
end
1 change: 1 addition & 0 deletions app/models/user.rb
Expand Up @@ -10,6 +10,7 @@ class User < ApplicationRecord
has_many :votes, dependent: :destroy
has_many :comments, dependent: :destroy
has_many :authorizations, dependent: :destroy
has_many :notifications, dependent: :destroy

def author_of?(resource)
resource.user_id == id
Expand Down
2 changes: 2 additions & 0 deletions app/policies/application_policy.rb
Expand Up @@ -38,6 +38,8 @@ def author_of?(entity = record)
user&.author_of?(entity)
end

alias_method :owner_of?, :author_of?

class Scope
attr_reader :user, :scope

Expand Down
5 changes: 5 additions & 0 deletions app/policies/notification_policy.rb
@@ -0,0 +1,5 @@
class NotificationPolicy < ApplicationPolicy
def destroy?
owner_of?
end
end
3 changes: 3 additions & 0 deletions app/serializers/notification_serializer.rb
@@ -0,0 +1,3 @@
class NotificationSerializer < ApplicationSerializer
attributes :id, :question_id, :user_id
end
25 changes: 25 additions & 0 deletions app/services/daily_digest_data.rb
@@ -0,0 +1,25 @@
module Services
class DailyDigestData < ApplicationService
CACHE_KEY = 'daily_digest_data'.freeze

def call
Rails.cache.fetch(CACHE_KEY, expires_in: cache_expires_in) do
questions.map { |q| { id: q.id, title: q.title } }
end
end

private

def questions
Question.where('created_at >= ? AND created_at <= ?', yesterday.beginning_of_day, yesterday.end_of_day)
end

def yesterday
@yesterday ||= Date.yesterday
end

def cache_expires_in
Date.today.end_of_day
end
end
end
19 changes: 19 additions & 0 deletions app/services/send_daily_digest.rb
@@ -0,0 +1,19 @@
module Services
class SendDailyDigest < ApplicationService
BATCH_SIZE = 500

def call
User.find_each(batch_size: BATCH_SIZE) { |u| deliver_mail(u) } if questions_data.present?
end

private

def deliver_mail(user)
DailyDigestMailer.digest(user).deliver_later
end

def questions_data
Services::DailyDigestData.new.call
end
end
end
15 changes: 15 additions & 0 deletions app/services/send_notifications.rb
@@ -0,0 +1,15 @@
module Services
class SendNotifications < ApplicationService
BATCH_SIZE = 500

def call(answer)
answer.question.subscribed_users.find_each(batch_size: BATCH_SIZE) { |u| deliver_mail(u, answer) }
end

private

def deliver_mail(user, answer)
NotificationMailer.new_answer(user, answer).deliver_later
end
end
end
4 changes: 4 additions & 0 deletions app/views/daily_digest_mailer/digest.html.slim
@@ -0,0 +1,4 @@
p Hi, it is daily digest!
p Yesterday questions:
- @questions_data.each do |q|
= link_to q[:title], question_url(id: q[:id])
5 changes: 5 additions & 0 deletions app/views/notification_mailer/new_answer.html.slim
@@ -0,0 +1,5 @@
p Hi
p New answer by question:
p = @answer.question.title
p Answer
p = @answer.body
6 changes: 6 additions & 0 deletions app/views/questions/_question.html.slim
Expand Up @@ -10,6 +10,12 @@
.links
= render "/shared/links/links_block", links: @question.links

.notification
- if @notification
p = link_to "Unsubscribe", notification_path(@notification), method: :delete, remote: true, class: "unsubscribe-notification-link"
- else
p = link_to "Subscribe", question_notifications_path(@question), method: :post, remote: true, class: "subscribe-notification-link"

- if policy(@question).edit?
p = link_to "Edit question", "#", class: "edit-question-link", data: {question_id: @question.id}

Expand Down
2 changes: 1 addition & 1 deletion app/views/questions/index.html.slim
@@ -1,6 +1,6 @@
h1 Questions

- if policy(Question).new?
- if user_signed_in?
p = link_to "Ask question", new_question_path

.questions-list
Expand Down
2 changes: 2 additions & 0 deletions config/application.rb
Expand Up @@ -26,5 +26,7 @@ class Application < Rails::Application
request_specs: false

end

config.active_job.queue_adapter = :sidekiq
end
end
5 changes: 2 additions & 3 deletions config/environments/development.rb
Expand Up @@ -17,16 +17,15 @@
if Rails.root.join('tmp', 'caching-dev.txt').exist?
config.action_controller.perform_caching = true

config.cache_store = :memory_store
config.public_file_server.headers = {
'Cache-Control' => "public, max-age=#{2.days.to_i}"
}
else
config.action_controller.perform_caching = false

config.cache_store = :null_store
end

config.cache_store = :redis_cache_store, { url: 'redis://localhost:6379/1' }

# Store uploaded files on the local file system (see config/storage.yml for options)
config.active_storage.service = :amazon

Expand Down

0 comments on commit 4c58541

Please sign in to comment.