diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb index c759abf..7277582 100644 --- a/app/controllers/v1/users_controller.rb +++ b/app/controllers/v1/users_controller.rb @@ -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 diff --git a/app/jobs/notifications/prediction_missing_job.rb b/app/jobs/notifications/prediction_missing_job.rb new file mode 100644 index 0000000..eef7c0f --- /dev/null +++ b/app/jobs/notifications/prediction_missing_job.rb @@ -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 diff --git a/app/jobs/schedule_daily_tasks_job.rb b/app/jobs/schedule_daily_tasks_job.rb index 3df356f..1785960 100644 --- a/app/jobs/schedule_daily_tasks_job.rb +++ b/app/jobs/schedule_daily_tasks_job.rb @@ -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 diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 286b223..d90f92a 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,4 +1,4 @@ class ApplicationMailer < ActionMailer::Base - default from: 'from@example.com' + default from: 'hello@octacle.app' layout 'mailer' end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb new file mode 100644 index 0000000..294f814 --- /dev/null +++ b/app/mailers/user_mailer.rb @@ -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 diff --git a/app/models/competition.rb b/app/models/competition.rb index 6032651..3261357 100644 --- a/app/models/competition.rb +++ b/app/models/competition.rb @@ -1,6 +1,6 @@ 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 @@ -8,6 +8,8 @@ class Competition < ApplicationRecord 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} @@ -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 diff --git a/app/models/email.rb b/app/models/email.rb new file mode 100644 index 0000000..f1eb3e4 --- /dev/null +++ b/app/models/email.rb @@ -0,0 +1,4 @@ +class Email < ApplicationRecord + belongs_to :user + belongs_to :topic, polymorphic: true, optional: true +end diff --git a/app/models/user.rb b/app/models/user.rb index 79c214a..7333c3c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 @@ -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 diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb index cbd34d2..afa23db 100644 --- a/app/views/layouts/mailer.html.erb +++ b/app/views/layouts/mailer.html.erb @@ -2,11 +2,27 @@
+ - <%= yield %> diff --git a/app/views/shared/_banner.html.erb b/app/views/shared/_banner.html.erb new file mode 100644 index 0000000..077f518 --- /dev/null +++ b/app/views/shared/_banner.html.erb @@ -0,0 +1,4 @@ +The match kicks off at <%= @match.kickoff_time.strftime('%a, %e %b %H:%M') %> UTC.
+Don't forget to get your prediction locked in!
++ <%= link_to 'Visit Octacle', 'https://www.octacle.app/competitions/predictions', class: 'btn btn-purple', target: '_blank' %> +
++ <%= render 'shared/unsubscribe' %> +
+