added 260907 version of worker toolkit

This commit is contained in:
2026-09-08 20:30:51 -04:00
parent cbd3f0f8ca
commit 97aca37663
1283 changed files with 142951 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
class ApplicationJob < ActiveJob::Base
end

View File

@@ -0,0 +1,18 @@
class CheckinReminderJob
require "sidekiq/api"
include Sidekiq::Worker
def perform(profile_id, checkin_reminder_at)
profile = Profile.find_by(id: profile_id)
return unless profile
return unless profile.checkin_reminder
return if profile.rejected_type.present?
return unless jid == profile.reminder_job_id
CheckinReminderMailer.remind(email: profile.email).deliver_later
profile.update_column(:reminder_job_id, self.class.perform_in(24.hours, profile_id, checkin_reminder_at))
end
end

View File

@@ -0,0 +1,138 @@
class DataExportJob < ActiveJob::Base
SUBFIELD_SEPARATOR = "; ".freeze
queue_as :default
attr_accessor :user_locale, :symptom_ids, :condition_ids, :treatment_ids, :tag_ids, :food_ids
def perform(user_id)
user = User.find(user_id)
data = csv_data(user)
UserDataMailer.trackings_csv(user.email, data).deliver_later
end
def csv_data(user)
checkins = user.checkins.includes(:conditions, :symptoms, :treatments).order_by(date: :asc)
set_attributes(user.locale, checkins)
CSV.generate do |csv|
csv << headers
checkins.each { |checkin| csv << checkin_row(checkin) }
end
end
private
def checkin_row(checkin)
row = [checkin.date]
symptoms_map = trackables_map(checkin.symptoms, "symptom")
conditions_map = trackables_map(checkin.conditions, "condition")
treatments_map = treatments_map(checkin.treatments)
condition_names.keys.each { |id| row << conditions_map[id] }
symptom_names.keys.each { |id| row << symptoms_map[id] }
treatment_names.keys.each { |id| row << treatments_map[id] }
row << tag_names.slice(*checkin.tag_ids).values.join(SUBFIELD_SEPARATOR)
row << food_names.slice(*checkin.food_ids).values.join(SUBFIELD_SEPARATOR)
row << checkin.weather&.summary
row << checkin.weather&.temperature_max
row << checkin.weather&.temperature_min
row << checkin.weather&.pressure
row << checkin.weather&.precip_intensity
row << checkin.weather&.humidity
end
def set_attributes(locale, checkins)
checkin_ids = checkins.pluck(:id)
self.user_locale = I18n.locale_available?(locale) ? locale : I18n.default_locale
self.symptom_ids = Checkin::Symptom.in(checkin_id: checkin_ids).distinct(:symptom_id)
self.condition_ids = Checkin::Condition.in(checkin_id: checkin_ids).distinct(:condition_id)
self.treatment_ids = Checkin::Treatment.in(checkin_id: checkin_ids).distinct(:treatment_id)
self.tag_ids = checkins.distinct(:tag_ids)
self.food_ids = checkins.distinct(:food_ids)
end
def symptom_names
@symptom_names ||= object_names(Symptom, :name, symptom_ids)
end
def condition_names
@condition_names ||= object_names(Condition, :name, condition_ids)
end
def treatment_names
@treatment_names ||= object_names(Treatment, :name, treatment_ids)
end
def tag_names
@tag_names ||= object_names(Tag, :name, tag_ids)
end
def food_names
@food_names ||= object_names(Food, :long_desc, food_ids)
end
def object_names(klass, field, ids)
name_slug = klass.name.underscore
relation = :"#{name_slug}_translations"
# .pluck(:"#{name_slug}_id", field)
klass
.joins(relation)
.where(:id => ids, relation => {locale: user_locale})
.pluck(:id, field)
.to_h
end
def headers
weather_headers = [
"Weather summary",
"Weather max temperature",
"Weather min temperature",
"Weather pressure",
"Weather precipitation intensity",
"Weather humidity"
]
["Date"]
.concat(condition_names.values)
.concat(symptom_names.values)
.concat(treatment_names.values)
.concat(%w[Tags Foods])
.concat(weather_headers)
end
def trackables_map(trackables, type)
id_field = "#{type}_id"
trackables.map { |c| [c[id_field], c.value] }.to_h
end
# special handling to allow distinguishing between treatments with no dosage, and treatments not taken
def treatments_map(treatments)
treatments.map do |treatment|
[
treatment["treatment_id"],
treatment_value(treatment.is_taken, treatment.value)
]
end.to_h
end
def treatment_value(is_taken, value)
if is_taken
value || "Taken"
else
"Not Taken"
end
end
end

View File

@@ -0,0 +1,40 @@
class DiscussionMention
include Sidekiq::Worker
NAME_REGEXP = /@\w+/
def perform(encrypted_user_id, comment_id)
comment = Comment.find(comment_id)
profiles = found_profiles(comment.body)
return if profiles.empty?
create_notifications(encrypted_user_id, profiles.map(&:user_id), comment)
end
private
def found_profiles(mentioned_string)
selected_names = mentioned_string.scan(NAME_REGEXP).uniq
selected_names.each_with_object([]) do |matched_string, result_array|
matched_string.slice!(0) # remove '@ tag'
profile = Profile.find_by(slug_name: matched_string)
result_array << profile if profile
end
end
def create_notifications(encrypted_user_id, user_ids, comment_id)
encrypted_notify_user_ids = user_ids.map { |user_id| SymmetricEncryption.encrypt(user_id) }
comment = Comment.find(comment_id)
encrypted_notify_user_ids.map do |encrypted_notify_user_id|
Notification.create(
kind: :mention,
notificateable: comment,
encrypted_user_id: encrypted_user_id,
encrypted_notify_user_id: encrypted_notify_user_id
)
end
end
end

View File

@@ -0,0 +1,31 @@
class EmailRejectDispatcher
include Sidekiq::Worker
def perform(raw_post)
body = JSON.parse(raw_post)
test_case_type = body["notificationType"]
message_raw = body["Message"]
recipients =
if test_case_type == "Bounce"
emails = body.dig("mail", "destination") || []
{bounce: emails}
elsif message_raw
generate_recipients(message_raw)
end
return unless recipients.present?
EmailRejectJob.perform_async(recipients)
end
def generate_recipients(message_raw)
message = JSON.parse message_raw
emails = message.dig("mail", "destination") || []
rejected_type = message["notificationType"].downcase
{rejected_type.to_sym => emails}
end
end

View File

@@ -0,0 +1,9 @@
class EmailRejectJob
include Sidekiq::Worker
def perform(recipients_hash)
User.where(email: recipients_hash.values.first).includes(:profile).map do |user|
user.profile.update_column(:rejected_type, recipients_hash.keys.first.to_s)
end
end
end

View File

@@ -0,0 +1,30 @@
class GroupNotifiersPerUser
include Sidekiq::Worker
def perform(encrypted_user_id)
begin
user_id = SymmetricEncryption.decrypt(encrypted_user_id)
rescue OpenSSL::Cipher::CipherError
return
end
user = User.find_by(id: user_id)
return unless user&.notify
return if user&.rejected_type.present? # Rejected via AWS SES
active_notifications = Notification.where(encrypted_notify_user_id: encrypted_user_id, delivered: false)
return if active_notifications.none?
aggregate_data = active_notifications.groupped_by_post_and_kind(encrypted_user_id)
NotificationsMailer.notify(email: user.email, data: aggregate_data).deliver_later
clean_notifications(active_notifications)
end
private
def clean_notifications(active_notifications)
active_notifications.batch_size(500).each { |notification| notification.update(delivered: true) }
end
end

View File

@@ -0,0 +1,14 @@
class GroupTopPostsJob
include Sidekiq::Worker
def perform(notify_token)
profile = Profile.find_by(notify_token: notify_token)
user = profile&.user
return unless profile && profile.notify_top_posts && user
return if user&.rejected_type.present? # Rejected via AWS SES
top_posts_ids = DiscussionPosts.new(nil, user).refined_top_list.map(&:id).map(&:to_s)
TopPostsMailer.notify(email: user.email, notify_token: notify_token, top_posts_ids: top_posts_ids).deliver_later
end
end

View File

@@ -0,0 +1,7 @@
class HelloWorldJob < ActiveJob::Base
queue_as :default
def perform(*_args)
Rails.logger.debug("Hello World".green)
end
end

View File

@@ -0,0 +1,32 @@
class MergeTrackables::CheckinTrackables
include Sidekiq::Worker
def perform(trackable_type, parent_id, rest_ids)
if Checkin::FIELD_TYPE.include? trackable_type
update_health_factors(trackable_type, parent_id, rest_ids)
else
update_regular_trackables(trackable_type, parent_id, rest_ids)
end
p "PERFORM MergeTrackables::PostTrackables"
MergeTrackables::PatternIncludes.perform_async(trackable_type, parent_id, rest_ids)
end
def update_health_factors(trackable_type, parent_id, rest_ids)
Checkin.where("#{trackable_type}_ids" => {"$in" => rest_ids}).map do |checkin|
updated_ids = checkin.send("#{trackable_type}_ids") - rest_ids + [parent_id]
checkin.update("#{trackable_type}_ids".to_sym => updated_ids.uniq)
end
end
def update_regular_trackables(trackable_type, parent_id, rest_ids)
klass = "Checkin::#{trackable_type.capitalize}".constantize
return unless klass
klass.where("#{trackable_type}_id".to_sym.in => rest_ids).map do |checkin|
checkin.update("#{trackable_type}_id".to_sym => parent_id)
end
end
end

View File

@@ -0,0 +1,50 @@
class MergeTrackables::Dispatcher
include Sidekiq::Worker
def perform(trackable_type, translation = nil)
trackable_class = trackable_type.capitalize.constantize
searchable_attr = (trackable_class.name == "Food") ? "long_desc" : "name"
return find_duplicates(trackable_type, trackable_class, translation, searchable_attr) if translation.present?
trackable_class::Translation.find_each do |trackable_translation|
translation = trackable_translation.send(searchable_attr.to_s)
next unless translation
find_duplicates(trackable_type, trackable_class, translation, searchable_attr)
end
end
def find_duplicates(trackable_type, trackable_class, translation, searchable_attr = "name")
begin
escaped_translation = Regexp.escape(translation.squish).split(" ").join("s+")
regex = "^\\s*#{escaped_translation}\\s*$"
same_translations = trackable_class::Translation.where("#{searchable_attr} ~* ?", regex)
return if same_translations.length <= 1 # Next step if origin found only
same_trackables = trackable_class
.where(id: same_translations.where(locale: "en").select("#{trackable_type}_id".to_sym))
.order(trackable_usages_count: :desc, id: :asc)
parent, *rest = same_trackables
return if parent.nil? || rest.length.zero?
p "DUPLICATES FOUND..."
p "#{trackable_type.capitalize} ids: #{same_trackables.pluck(:id)}"
p "#{trackable_type.capitalize} translations: }"
p same_trackables.map(&:"#{searchable_attr}")
rest_ids = rest.map(&:id)
# Remove duplicates from translations
same_translations.where("#{trackable_type}_id": rest_ids).map(&:destroy)
rescue ActiveRecord::StatementInvalid
return
end
p "PERFORM MergeTrackables::UserTrackableAssociation"
MergeTrackables::UserTrackableAssociation.perform_async(trackable_type, parent.id, rest_ids)
end
end

View File

@@ -0,0 +1,22 @@
class MergeTrackables::PatternIncludes
include Sidekiq::Worker
def perform(trackable_type, parent_id, rest_ids)
parent = trackable_type.capitalize.constantize.find_by(id: parent_id)
return unless parent
Pattern.where("includes.id" => {"$in" => rest_ids}).map do |item|
item.includes
.select { |hash| hash[:category] == trackable_type.tableize && rest_ids.include?(hash[:id]) }
.map do |selected_hash|
selected_hash[:id] = parent_id
selected_hash[:label] = parent.name
end
item.includes = item.includes.uniq
item.save
end
MergeTrackables::PostTrackables.perform_async(trackable_type, parent_id, rest_ids)
end
end

View File

@@ -0,0 +1,17 @@
class MergeTrackables::PostTrackables
include Sidekiq::Worker
def perform(trackable_type, parent_id, rest_ids)
trackable_key = "#{trackable_type}_ids"
Post.where(trackable_key.to_sym.in => rest_ids).map do |post|
updated_ids = post.send(trackable_key) - rest_ids + [parent_id]
post.update(trackable_key.to_sym => updated_ids.uniq)
end
p "PERFORM MergeTrackables::RemoveDuplicates"
MergeTrackables::RemoveDuplicates.perform_async(trackable_type, rest_ids)
end
end

View File

@@ -0,0 +1,11 @@
class MergeTrackables::RemoveDuplicates
include Sidekiq::Worker
def perform(trackable_type, rest_ids)
trackable_class = trackable_type.capitalize.constantize
return unless trackable_class
trackable_class.where(id: rest_ids).map(&:destroy)
p "FINISH!!!!"
end
end

View File

@@ -0,0 +1,17 @@
class MergeTrackables::TopicFollowing
include Sidekiq::Worker
def perform(trackable_type, parent_id, rest_ids)
trackable_key = "#{trackable_type}_ids"
TopicFollowing.all(trackable_key.to_sym.in => rest_ids).map do |tf|
updated_ids = tf.send(trackable_key) - rest_ids + [parent_id]
tf.update(trackable_key.to_sym => updated_ids.uniq)
end
p "PERFORM MergeTrackables::CheckinTrackables"
MergeTrackables::CheckinTrackables.perform_async(trackable_type, parent_id, rest_ids)
end
end

View File

@@ -0,0 +1,31 @@
class MergeTrackables::TrackableUsages
include Sidekiq::Worker
def perform(trackable_type, parent_id, rest_ids)
trackable_class = trackable_type.capitalize.constantize
return unless trackable_class
parent = trackable_class.find_by(id: parent_id)
rest = trackable_class.where(id: rest_ids)
return if parent.nil? && rest.length.zero?
TrackableUsage.where(trackable: rest).map do |tr_usage|
parent_usage = TrackableUsage.find_by(trackable: parent, user_id: tr_usage.user_id)
if parent_usage
parent_usage.count += tr_usage.count
parent_usage.save
tr_usage.destroy
else
tr_usage.update(trackable_id: parent.id)
parent.increment!(:trackable_usages_count)
end
end
p "PERFORM MergeTrackables::Trackings"
MergeTrackables::Trackings.perform_async(trackable_type, parent_id, rest_ids)
end
end

View File

@@ -0,0 +1,13 @@
class MergeTrackables::Trackings
include Sidekiq::Worker
def perform(trackable_type, parent_id, rest_ids)
Tracking.where(trackable_id: rest_ids, trackable_type: trackable_type.camelcase).map do |tracking|
tracking.update(trackable_id: parent_id)
end
p "PERFORM MergeTrackables::TopicFollowing"
MergeTrackables::TopicFollowing.perform_async(trackable_type, parent_id, rest_ids)
end
end

View File

@@ -0,0 +1,27 @@
class MergeTrackables::UserTrackableAssociation
include Sidekiq::Worker
def perform(trackable_type, parent_id, rest_ids)
trackable_class = trackable_type.capitalize.constantize
klass = "User#{trackable_type.capitalize}".constantize
return unless trackable_class && klass
parent = trackable_class.find_by(id: parent_id)
rest = trackable_class.where(id: rest_ids)
return if parent.nil? && rest.length.zero?
klass.where(trackable_type.to_sym => rest).map do |user_trackable_type|
parent_user_trackable = klass.find_by(trackable_type.to_sym => parent,
:user_id => user_trackable_type.user_id)
if parent_user_trackable.present?
user_trackable_type.destroy
else
user_trackable_type.update_columns("#{trackable_type}_id".to_sym => parent.id)
end
end
p "PERFORM MergeTrackables::TrackableUsages"
MergeTrackables::TrackableUsages.perform_async(trackable_type, parent_id, rest_ids)
end
end

View File

@@ -0,0 +1,20 @@
class NotesExportJob < ActiveJob::Base
SUBFIELD_SEPARATOR = "; ".freeze
queue_as :default
attr_accessor :user_locale, :symptom_ids, :condition_ids, :treatment_ids, :tag_ids, :food_ids
def perform(user_id)
user = User.find(user_id)
note_details = user.checkins.where.not(note: ["", nil]).order(date: :desc).pluck(:date, :note)
body = note_details.map { |detail| detail.join(",") }
ActionMailer::Base.mail(
from: Rails.application.secrets.smtp_email_from,
to: user.email,
subject: "Flaredown data export",
body: body
).deliver
end
end

View File

@@ -0,0 +1,9 @@
class NotificationDispatcher
include Sidekiq::Worker
def perform
Notification.distinct(:encrypted_notify_user_id).each do |encrypted_id|
GroupNotifiersPerUser.perform_async(encrypted_id)
end
end
end

View File

@@ -0,0 +1,10 @@
class PositionReferenceJob
include Sidekiq::Worker
def perform(object_class, objec_id, postal_code)
object = object_class.constantize.find_by(id: objec_id)
position = Position.find_or_create_by(postal_code: postal_code)
object.update(position_id: position.id) if position.persisted?
end
end

View File

@@ -0,0 +1,37 @@
class SameTrackablesJob
include Sidekiq::Worker
def perform(options)
trackable_type = options["trackable_type"]
translation = options["translation"]
trackable_class = trackable_type.capitalize.constantize
searchable_attr = (trackable_class.name == "Food") ? "long_desc" : "name"
[].tap do |array|
if translation.present?
array << find_duplicates(trackable_class, translation, searchable_attr)
else
trackable_class::Translation.find_each do |trackable_translation|
translation_value = trackable_translation.send(searchable_attr.to_s)
duplicates = find_duplicates(trackable_class, translation_value, searchable_attr)
array << duplicates if duplicates.present?
end
end
p "Same #{trackable_type.pluralize.camelize}: "
p array.uniq
end
end
def find_duplicates(trackable_class, translation, searchable_attr = "name")
escaped_translation = Regexp.escape(translation.squish).split(" ").join("s+")
regex = "^\\s*#{escaped_translation}\\s*$"
same_translations = trackable_class::Translation.where("#{searchable_attr} ~* ?", regex)
same_translations.map(&:"#{searchable_attr}") if same_translations.length > 1
rescue ActiveRecord::StatementInvalid
nil
end
end

View File

@@ -0,0 +1,16 @@
class SwitchTrackableVisibility < ApplicationJob
queue_as :default
def perform(trackable_usage_id)
trackable_usage = TrackableUsage.find_by(id: trackable_usage_id)
return unless trackable_usage
trackable = trackable_usage.trackable.reload
if trackable.trackable_usages_count >= Flaredown.config.trackables_min_popularity
trackable.update(global: true)
else
trackable.update(global: false) if trackable.global? # rubocop:disable Style/IfInsideElse
end
end
end

View File

@@ -0,0 +1,20 @@
class TopPostsMailerDispatcher
include Sidekiq::Worker
TOP_POST_WEEKLY_TIME = ENV["TOP_POST_WEEKLY_TIME"]
MAX_DIFF = 5.minutes
def perform
return unless Time.current.tuesday? # Send on tuesdays
Profile.where(notify_top_posts: true, time_zone_name: find_time_zone_names).find_each(batch_size: 500) do |profile|
GroupTopPostsJob.perform_async(profile.notify_token)
end
end
def find_time_zone_names
Profile.pluck(:time_zone_name).compact.uniq.select do |time_zone_name|
(Time.current.in_time_zone(time_zone_name) - TOP_POST_WEEKLY_TIME.in_time_zone(time_zone_name)).abs < MAX_DIFF
end
end
end

View File

@@ -0,0 +1,25 @@
class UpdateCheckinReminders
require "sidekiq/api"
include Sidekiq::Worker
def perform(profile_id)
profile = Profile.find_by(id: profile_id)
return unless profile
return unless profile.checkin_reminder && profile.checkin_reminder_at
Sidekiq::ScheduledSet.new.find_job(profile.reminder_job_id)&.delete
job_id = CheckinReminderJob.perform_in(get_reminder_time(profile).minutes, profile_id, profile.checkin_reminder_at)
profile.update_column(:reminder_job_id, job_id)
end
def get_reminder_time(profile)
time_zone_name = profile.time_zone_name
checkin_at_timezone = profile.checkin_reminder_at.strftime("%H:%M").in_time_zone(time_zone_name)
# Select minutes
(checkin_at_timezone - Time.current.in_time_zone(time_zone_name)).divmod(1.day)[1].divmod(1.minute)[0]
end
end

View File

@@ -0,0 +1,13 @@
class UpdatePostCountersJob
include Sidekiq::Worker
def perform(params)
parent_type = params["parent_type"]
return unless parent_type == "Post"
post = Post.find_by(_id: params["parent_id"], _type: parent_type)
return unless post
post&.update_counters
end
end