Skip to content
Open
2 changes: 1 addition & 1 deletion app/controllers/v1/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ def update
private

def prediction_params
params.require(:user).permit(:name, :timezone, :photo_key)
params.require(:user).permit(:name, :timezone, :photo_key, notifications: {})
end
end
16 changes: 16 additions & 0 deletions app/jobs/notifications/prediction_missing_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class Notifications::PredictionMissingJob < ApplicationJob
queue_as :default

def perform(match_ids)
matches = Match.where(id: match_ids)
matches.each do |match|
# loads all users in this competition that have emailing turned on
users_to_email = User.need_prediction_notifications(match)
users_to_email.each do |user|
# Checks to see if an email has already been sent
email = Email.find_by(user: user, topic: match, notification: 'prediction_missing')
UserMailer.with(user: user, match: match, notification: 'prediction_missing').prediction_missing.deliver_later unless email
end
end
end
end
4 changes: 4 additions & 0 deletions app/jobs/schedule_daily_tasks_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ class ScheduleDailyTasksJob < ApplicationJob
def perform
competitions = Competition.on_going
competitions.each do |competition|
# Schedules the matches as "started" based on their kickoff_time
matches = competition.matches.where(kickoff_time: Date.today.all_day)
matches.pluck(:kickoff_time).uniq.each do |kickoff_time|
MatchStartedJob.set(wait_until: kickoff_time).perform_later(kickoff_time)
end
# Schedules notifications for missing predicitions
matches_tomorrow = competition.matches.where(kickoff_time: Date.tomorrow.all_day)
Notifications::PredictionMissingJob.perform_later(matches_tomorrow.pluck(:id))
end
end
end
2 changes: 1 addition & 1 deletion app/mailers/application_mailer.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class ApplicationMailer < ActionMailer::Base
default from: 'from@example.com'
default from: 'hello@octacle.app'
layout 'mailer'
end
15 changes: 15 additions & 0 deletions app/mailers/user_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class UserMailer < ApplicationMailer

# Subject can be set in your I18n file at config/locales/en.yml
# with the following lookup:
#
# en.user_mailer.prediction_missing.subject
#
def prediction_missing
@user = params[:user] # Instance variable => available in view
@match = params[:match]
@notification = params[:notification]
mail(to: @user.email, subject: "Octacle - You're missing a prediction for #{@match.team_home.abbrev} vs. #{@match.team_away.abbrev}")
Email.create(user: @user, topic: @match, notification: @notification)
end
end
13 changes: 12 additions & 1 deletion app/models/competition.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
class Competition < ApplicationRecord
belongs_to :current_round, class_name: 'Round', optional: true
has_many :matches, dependent: :destroy
has_many :matches, -> { distinct }, dependent: :destroy
has_many :rounds, -> { distinct }, through: :matches
has_many :groups, through: :rounds
has_many :affiliations, through: :groups
has_many :teams, through: :affiliations
has_many :leaderboards, dependent: :destroy
has_many :predictions, through: :matches, dependent: :destroy
has_many :users, through: :leaderboards, source: :users
# For some reason, this doesn't give me back a collection
# has_many :all_users_predicted, -> { distinct }, through: :predictions, source: :user
has_one_attached :photo

validates :name, presence: true, uniqueness: { scope: :start_date}
Expand All @@ -26,4 +28,13 @@ def destroy_rounds
def max_possible_score
matches.finished.joins(:round).sum('rounds.points')
end

def users_predicted
# For some reason, this doesn't give me back a collection
# User.joins(:predictions)
# .where(predictions: { user_id: predictions.select(:user_id).distinct }).distinct
# TODO: refactor using an Active Record query that works
user_ids = User.find_by_sql(['SELECT DISTINCT users.id FROM users JOIN predictions ON predictions.user_id = users.id JOIN matches ON matches.id = predictions.match_id WHERE matches.competition_id = ?', id])
User.where(id: user_ids.pluck(:id))
end
end
4 changes: 4 additions & 0 deletions app/models/email.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class Email < ApplicationRecord
belongs_to :user
belongs_to :topic, polymorphic: true, optional: true
end
34 changes: 34 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@ class User < ApplicationRecord
# has_many :competitions, through: :leaderboards
has_many :predictions, dependent: :destroy
has_many :matches, through: :predictions
has_many :emails, dependent: :destroy

# Scenic views
has_many :scores, class_name: 'UserScore'

scope :with_email_prediction_missing, -> {
where("notifications->'email'->>'prediction_missing' = ?", 'true')
}

validates :name, presence: true, on: :update, if: :name_changed?

after_create :auto_join_leaderboards
Expand All @@ -22,6 +27,35 @@ def name
super || email.split('@').first
end

# user.notification_enabled?(:email, :prediction_missing)
# (query) User.where("notifications->'email'->>'prediction_missing' = ?", 'true')
def notification_enabled?(method, event)
notifications.dig(method.to_s, event.to_s) || false
end

# user.enable_notification!(:email, :prediction_missing)
def enable_notification!(method, event)
self.notifications[method.to_s] ||= {}
self.notifications[method.to_s][event.to_s] = true
save
end

# user.disable_notification!(:email, :prediction_missing)
def disable_notification!(method, event)
self.notifications[method.to_s] ||= {}
self.notifications[method.to_s][event.to_s] = false
save
end

def self.need_prediction_notifications(next_match)
return [] if next_match.blank?

# total number of people who have made predicitons (and have email on)
users = next_match.competition.users_predicted.with_email_prediction_missing
# minus the ones who have made predictions for this match
users - users.joins(:predictions).where(predictions: { match_id: next_match.id })
end

private

def auto_join_leaderboards
Expand Down
20 changes: 18 additions & 2 deletions app/views/layouts/mailer.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,27 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
<style>
/* Email styles need to be inline */
@import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap');
h1 {
font-family: "Montserrat", sans-serif;
}
.btn-purple {
background-color: #404080;
color: white;
text-decoration: none;
}
.btn-purple:hover {
background-color: #587AA1;
color: white;
text-decoration: none;
}
.bg-purple {
background: linear-gradient(167.4deg, #3e3b7d 0%, #6690b7 88.73%);
}
</style>
</head>

<body>
<%= yield %>
</body>
Expand Down
4 changes: 4 additions & 0 deletions app/views/shared/_banner.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div class='d-flex align-items-center py-5 px-1 bg-purple'>
<%= image_tag 'https://raw.githubusercontent.com/trouni/predictor-vue/main/src/assets/logo.png', width: 100, alt: "text", class: 'me-2' %>
<h1 class='m-0 text-white'>Octacle</h1>
</div>
1 change: 1 addition & 0 deletions app/views/shared/_unsubscribe.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<em><small><%= link_to 'Unsubscribe', 'https://www.octacle.app/profile', class: 'text-muted text-decoration-none' %></small></em>
18 changes: 18 additions & 0 deletions app/views/user_mailer/prediction_missing.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<div class="container">
<%= render 'shared/banner' %>
<div class="container">
<div class="d-flex flex-column align-items-center">
<div class='d-flex align-items-center my-3'>
<%= image_tag cl_image_path(@match.team_home.flag.key), alt: "text", width: 100, class: 'me-2 rounded' %><strong><%= @match.team_home.name %></strong> <span class='px-2 fw-light'>vs.</span> <strong><%= @match.team_away.name %></strong> <%= image_tag cl_image_path(@match.team_away.flag.key), alt: "text", width: 100, class: 'ms-2 rounded' %>
</div>
<p class='m-0'>The match kicks off at <%= @match.kickoff_time.strftime('%a, %e %b %H:%M') %> UTC.</p>
<p class='pb-2'>Don't forget to get your prediction locked in!</p>
<p>
<%= link_to 'Visit Octacle', 'https://www.octacle.app/competitions/predictions', class: 'btn btn-purple', target: '_blank' %>
</p>
<p>
<%= render 'shared/unsubscribe' %>
</p>
</div>
</div>
</div>
3 changes: 3 additions & 0 deletions app/views/user_mailer/prediction_missing.text.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
User#prediction_missing

<%= @greeting %>, find me in app/views/user_mailer/prediction_missing.text.erb
2 changes: 1 addition & 1 deletion app/views/v1/users/_user.json.jbuilder
Original file line number Diff line number Diff line change
@@ -1 +1 @@
json.extract! user, :id, :email, :timezone, :admin, :photo_key, :name
json.extract! user, :id, :email, :timezone, :admin, :photo_key, :name, :notifications
15 changes: 15 additions & 0 deletions db/migrate/20240623064544_add_notifications_to_users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class AddNotificationsToUsers < ActiveRecord::Migration[6.1]
def change
add_column :users, :notifications, :jsonb, default: {}
add_index :users, :notifications, using: :gin
User.find_each do |user|
user.notifications = {
email: {
prediction_missing: true,
competition_new: true
}
}
user.save
end
end
end
10 changes: 10 additions & 0 deletions db/migrate/20240627134655_create_emails.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class CreateEmails < ActiveRecord::Migration[6.1]
def change
create_table :emails do |t|
t.references :user, null: false, foreign_key: true
t.string :notification
t.references :topic, polymorphic: true
t.timestamps
end
end
end
17 changes: 16 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions test/fixtures/emails.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html

one:
user: one
notification: 1
topic: one

two:
user: two
notification: 1
topic: two
7 changes: 7 additions & 0 deletions test/jobs/email/prediction_missing_job_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require "test_helper"

class Emails::PredictionMissingJobTest < ActiveJob::TestCase
# test "the truth" do
# assert true
# end
end
7 changes: 7 additions & 0 deletions test/jobs/notifications/prediction_missing_job_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require "test_helper"

class Notifications::PredictionMissingJobTest < ActiveJob::TestCase
# test "the truth" do
# assert true
# end
end
9 changes: 9 additions & 0 deletions test/mailers/previews/user_mailer_preview.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview

# Preview this email at http://localhost:3000/rails/mailers/user_mailer/prediction_missing
def prediction_missing
UserMailer.prediction_missing
end

end
12 changes: 12 additions & 0 deletions test/mailers/user_mailer_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
require "test_helper"

class UserMailerTest < ActionMailer::TestCase
test "prediction_missing" do
mail = UserMailer.prediction_missing
assert_equal "Prediction missing", mail.subject
assert_equal ["to@example.org"], mail.to
assert_equal ["from@example.com"], mail.from
assert_match "Hi", mail.body.encoded
end

end
7 changes: 7 additions & 0 deletions test/models/email_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require "test_helper"

class EmailTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end