Skip to content

Commit

Permalink
Restructure how content is linked to users
Browse files Browse the repository at this point in the history
De-normalize the database to store the last content that users received
on the user. This makes it easier to know what to send the user next,
and reduces the reliance on their age in months. This was causing issues
where the child would turn a month older at the beginning of each month,
and depending on when they had signed up it meant they were missing
content.
  • Loading branch information
cdccollins committed Dec 6, 2024
1 parent 648dbad commit f07a248
Show file tree
Hide file tree
Showing 26 changed files with 178 additions and 136 deletions.
2 changes: 1 addition & 1 deletion app/controllers/contents_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ def destroy
private

def content_params
params.require(:content).permit(:body, :link, :position, :welcome_message)
params.require(:content).permit(:body, :link, :position, :welcome_message, :age_in_months)
end
end
4 changes: 2 additions & 2 deletions app/controllers/groups_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class GroupsController < ApplicationController
def index
@groups = Group.order(:age_in_months)
@groups = Group.all
end

def show
Expand Down Expand Up @@ -44,6 +44,6 @@ def destroy
private

def group_params
params.require(:group).permit(:name, :age_in_months)
params.require(:group).permit(:name)
end
end
6 changes: 2 additions & 4 deletions app/jobs/send_bulk_message_job.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
class SendBulkMessageJob < ApplicationJob
queue_as :default

def perform(users, group)
return unless group.present?

message_jobs = users.map { |user| SendMessageJob.new(user, group) }
def perform(users)
message_jobs = users.map { |user| SendMessageJob.new(user) }

ActiveJob.perform_all_later(message_jobs)
end
Expand Down
17 changes: 14 additions & 3 deletions app/jobs/send_message_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ class SendMessageJob < ApplicationJob

queue_as :default

def perform(user, group)
content = user.next_content(group)
def perform(user)
content = user.next_content

return unless content.present?
# If BulkMessage fails and reruns this job, don't send them the next message
Expand All @@ -18,6 +18,17 @@ def perform(user, group)
m.content = content
end

Twilio::Client.new.send_message(message) if message.save
Twilio::Client.new.send_message(message) if save_user_and_message(user, message, content)
end

private

def save_user_and_message(user, message, content)
ActiveRecord::Base.transaction do
user.update(last_content_id: content.id)
message.save
rescue ActiveRecord::RecordInvalid
false
end
end
end
8 changes: 4 additions & 4 deletions app/jobs/send_welcome_message_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ class SendWelcomeMessageJob < ApplicationJob
queue_as :default

def perform(user)
group = Group.find_by(age_in_months: user.child_age_in_months_today)
content = Content.find_by(age_in_months: user.child_age_in_months_today, welcome_message: true)

message = if group.present? && group.welcome_message.present?
message = if content.present?
Message.create do |m|
m.token = m.send(:generate_token)
m.link = group.welcome_message.link
m.link = content.link
m.user = user
m.body = group.welcome_message.body.gsub("{{link}}", track_link_url(m.token))
m.body = content.body.gsub("{{link}}", track_link_url(m.token))
end
else
Message.create do |m|
Expand Down
2 changes: 1 addition & 1 deletion app/models/content.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ class Content < ApplicationRecord

has_many :messages, dependent: :nullify

validates_presence_of :body, :link
validates_presence_of :body, :link, :age_in_months
end
6 changes: 1 addition & 5 deletions app/models/group.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
class Group < ApplicationRecord
has_many :contents, dependent: :destroy

validates :name, :age_in_months, presence: true

def welcome_message
contents.find_by(welcome_message: true)
end
validates :name, presence: true

def weekly_content
contents.where(welcome_message: false)
Expand Down
33 changes: 29 additions & 4 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,38 @@ def full_name
"#{first_name} #{last_name}"
end

def next_content(group)
return unless group.present?
# find lowest ranked content minus any they have already seen
(group.weekly_content - contents).min_by(&:position)
def next_content
if had_any_content_before?
find_next_unseen_content
else
Content.where(age_in_months: child_age_in_months_today, welcome_message: false).min_by(&:position)
end
end

def had_content_this_week?
messages.where("created_at > ?", 6.days.ago).where.not(content: nil).exists?
end

private

def had_any_content_before?
last_content_id.present?
end

def not_seen_content?(content)
messages.where(content_id: content.id).none?
end

def find_next_unseen_content
i = Content.find(last_content_id).position + 1

loop do
content = Content.find_by(position: i)
# Last message in series
return nil if content.nil?
# Next message
return content if not_seen_content?(content)
i += 1
end
end
end
4 changes: 4 additions & 0 deletions app/views/contents/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
<%= form.input :body, as: :text, input_html: { class: "w-full", rows: 5 }, hint: "To put the link in the message body, use {{link}}" %>
</div>

<div class="mt-3">
<%= form.input :age_in_months, wrapper_html: { class: "w-1/2" } %>
</div>

<div class="mt-3">
<%= form.input :link %>
</div>
Expand Down
4 changes: 0 additions & 4 deletions app/views/groups/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,5 @@
<%= form.input :name %>
</div>

<div class="mt-3">
<%= form.input :age_in_months %>
</div>

<%= form.submit (action_name == "edit" ? "Update" : "Create"), class: "btn btn-primary mt-5 bg-purple-600 text-white w-full" %>
<% end %>
2 changes: 0 additions & 2 deletions app/views/groups/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
<thead>
<tr class="px-4 py-4 bg-purple-300 text-left">
<th class="p-4 w-3/4">Content group name</th>
<th class="p-4 w-3/4">Child age</th>
<th class="p-4 w-1/5">No. of messages</th>
<th>&nbsp</th>
</tr>
Expand All @@ -18,7 +17,6 @@
<% @groups.each do |group| %>
<tr class="border-b-2">
<td class="p-4"><%= link_to group.name, group, class: "underline hover:no-underline" %></td>
<td class="p-4"><%= group.age_in_months %> months</td>
<td class="p-4"><%= group.contents.size %></td>
<td class="p-4">
<%= link_to "Edit", edit_group_path(group), class: "underline hover:no-underline" %>
Expand Down
8 changes: 6 additions & 2 deletions app/views/groups/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
<thead>
<tr class="px-4 py-4 bg-purple-300 text-left">
<th class="p-4"></th>
<th class="p-4 w-3/5">Body</th>
<th class="p-4 w-2/5">Link</th>
<th class="p-4 w-6/12">Body</th>
<th class="p-4 w-1/12">Age</th>
<th class="p-4 w-5/12">Link</th>
<th>&nbsp</th>
</tr>
</thead>
Expand All @@ -31,6 +32,9 @@
<% end %>
<%= content.body %>
</td>
<td class="p-4">
<%= content.age_in_months %>
</td>
<td class="p-4"><%= content.link %></td>
<td class="p-4">
<%= link_to "Edit", edit_group_content_path(@group, content), class: "underline hover:no-underline" %>
Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20241205095730_add_last_content_to_user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddLastContentToUser < ActiveRecord::Migration[7.2]
def change
add_reference :users, :last_content, foreign_key: {to_table: :contents}
end
end
12 changes: 12 additions & 0 deletions db/migrate/20241205100911_remove_age_from_group.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class RemoveAgeFromGroup < ActiveRecord::Migration[7.2]
def change
add_column :contents, :age_in_months, :integer

Content.find_each do |content|
content.update(age_in_months: content.group.age_in_months)
end

remove_column :groups, :age_in_months, :integer
change_column_null :contents, :age_in_months, false
end
end
9 changes: 5 additions & 4 deletions db/schema.rb

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

32 changes: 8 additions & 24 deletions lib/tasks/scheduler.rake
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,35 @@ namespace :scheduler do
task send_morning_message: :environment do
(next unless Date.today.wday == ENV.fetch("WEEKLY_MESSAGE_DAY").to_i) if ENV.fetch("SET_WEEKLY") == "true"

User.contactable.wants_morning_message.group_by(&:adjusted_child_age_in_months_today).each do |age, users|
group = Group.find_by(age_in_months: age)

next unless group

SendBulkMessageJob.perform_later(users, group)
User.contactable.wants_morning_message.find_in_batches do |users|
SendBulkMessageJob.perform_later(users)
end
end

desc "Send afternoon text message"
task send_afternoon_message: :environment do
(next unless Date.today.wday == ENV.fetch("WEEKLY_MESSAGE_DAY").to_i) if ENV.fetch("SET_WEEKLY") == "true"

User.contactable.wants_afternoon_message.group_by(&:adjusted_child_age_in_months_today).each do |age, users|
group = Group.find_by(age_in_months: age)

next unless group

SendBulkMessageJob.perform_later(users, group)
User.contactable.wants_afternoon_message.find_in_batches do |users|
SendBulkMessageJob.perform_later(users)
end
end

desc "Send evening text message"
task send_evening_message: :environment do
(next unless Date.today.wday == ENV.fetch("WEEKLY_MESSAGE_DAY").to_i) if ENV.fetch("SET_WEEKLY") == "true"

User.contactable.wants_evening_message.group_by(&:adjusted_child_age_in_months_today).each do |age, users|
group = Group.find_by(age_in_months: age)

next unless group

SendBulkMessageJob.perform_later(users, group)
User.contactable.wants_evening_message.find_in_batches do |users|
SendBulkMessageJob.perform_later(users)
end
end

desc "Send no timing preference text message"
task send_no_timing_preference_message: :environment do
(next unless Date.today.wday == ENV.fetch("WEEKLY_MESSAGE_DAY").to_i) if ENV.fetch("SET_WEEKLY") == "true"

User.contactable.no_preference_message.group_by(&:adjusted_child_age_in_months_today).each do |age, users|
group = Group.find_by(age_in_months: age)

next unless group

SendBulkMessageJob.perform_later(users, group)
User.contactable.no_preference_message.find_in_batches do |users|
SendBulkMessageJob.perform_later(users)
end
end

Expand Down
1 change: 1 addition & 0 deletions test/factories/content.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
factory :content do
body { "Sample Body" }
link { "www.example.com" }
age_in_months { 18 }
sequence(:position) { |n| n }

group
Expand Down
3 changes: 1 addition & 2 deletions test/factories/group.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
FactoryBot.define do
factory :group do
name { "Content for 18 month olds" }
age_in_months { 18 }
name { "Group" }
end
end
13 changes: 2 additions & 11 deletions test/jobs/send_bulk_message_job_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,11 @@ class SendBulkMessageJobTest < ActiveSupport::TestCase

test "#perform creates jobs to send messages to users" do
users = create_list(:user, 3, child_birthday: 3.months.ago)
group = create(:group, age_in_months: 3)
group = create(:group)
create(:content, group:)

assert_enqueued_jobs 3 do
SendBulkMessageJob.perform_now(users, group)
end
end

test "#perform does not create jobs if group is not present" do
users = create_list(:user, 3, child_birthday: 3.months.ago)
group = nil

assert_no_enqueued_jobs do
SendBulkMessageJob.perform_now(users, group)
SendBulkMessageJob.perform_now(users)
end
end
end
Loading

0 comments on commit f07a248

Please sign in to comment.