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,15 @@
class AdditionalPosts
protected
def current_ability
@current_ability ||= Ability.new(user)
end
def add_last_new_posts(number, followed_ids)
Post.where(_type: "Post")
.not_in(_id: followed_ids)
.order(created_at: :desc)
.to_a
.first(number)
end
end

View File

@@ -0,0 +1,96 @@
class ChartListService
TRACKABLES = %w[symptom condition treatment].freeze
HBI_PAYLOAD = [[1, "Harvey Bradshaw Index".freeze, true].freeze].freeze
WEATHERS_PAYLOAD = [
[1, "Avg daily humidity".freeze, true].freeze,
[2, "Avg daily atmospheric pressure".freeze, true].freeze
].freeze
attr_reader :user
def initialize(current_user:)
@user = current_user
end
def as_json
{
chart_list: {
id: 1,
payload: {
tags: tags_payload,
foods: foods_payload,
symptoms: symptoms_payload,
conditions: conditions_payload,
treatments: treatments_payload,
weathersMeasures: weathers_payload,
harveyBradshawIndices: hbi_payload
}
}
}
end
private
def tag_ids
user.checkins.distinct(:tag_ids)
end
def food_ids
user.checkins.distinct(:food_ids)
end
def checkin_ids
@_user_checkin_ids ||= user.checkins.distinct(:id)
end
def last_checkin
@_last_checkin ||= user.last_checkin
end
def tags_payload
Tag::Translation
.where(tag_id: tag_ids, locale: I18n.locale)
.pluck(:tag_id, :name)
.map { |payload| payload << last_checkin.tag_ids.include?(payload.first) }
end
def foods_payload
Food::Translation
.where(food_id: food_ids, locale: I18n.locale)
.pluck(:food_id, :long_desc)
.map { |payload| payload << last_checkin.food_ids.include?(payload.first) }
end
def weathers_payload
last_checkin&.weather_id ? WEATHERS_PAYLOAD : WEATHERS_PAYLOAD.map { |p| [p[0], p[1], false] }
end
def hbi_payload
if Condition::Translation.where(name: "Crohn's disease", id: last_tracked_condition_ids).any?
HBI_PAYLOAD
else
[[HBI_PAYLOAD[0][0], HBI_PAYLOAD[0][1], false]]
end
end
TRACKABLES.each do |type|
titleized = type.titleize
model = "Checkin::#{titleized}".constantize
id_field = :"#{type}_id"
last_tracked_ids_method = :"last_tracked_#{type}_ids"
define_method(last_tracked_ids_method) do
model.where(checkin_id: last_checkin&.id).distinct(id_field)
end
define_method(:"#{type.pluralize}_payload") do
ids = model.where(:checkin_id.in => checkin_ids).distinct(id_field)
"#{titleized}::Translation"
.constantize
.where(id_field => ids, :locale => I18n.locale)
.pluck(id_field, :name)
.map { |payload| payload << send(last_tracked_ids_method).include?(payload.first) }
end
end
end

View File

@@ -0,0 +1,137 @@
class ChartsPattern
attr_accessor :start_at, :end_at, :pattern, :user, :checkins_with_weather
attr_reader :used_color_ids
COLOR_IDS = Flaredown::Colorable::IDS
ADITIONAL_DAYS = 8
TYPE_CHART = {
line: %w[conditions symptoms],
marker: %w[treatments tags foods],
hbi: %w[harveyBradshawIndices],
weather: %w[weathersMeasures]
}.freeze
SUBTYPE = {
static: %i[line marker],
dynamic: %i[hbi weather]
}.freeze
def initialize(options)
@start_at = options[:start_at]
@end_at = options[:end_at]
@pattern = options[:pattern]
@used_color_ids = []
@checkins_with_weather = checkins.where(:weather_id.ne => nil)
end
def user
@user ||= pattern.author
end
def checkins
@checkins ||= user.checkins.order(date: :asc)
end
def scoped_checkins
@scoped_checkins ||= checkins.by_date(start_at.to_date - ADITIONAL_DAYS.days, end_at.to_date + ADITIONAL_DAYS.days)
end
def chart_data
@data ||=
{
pattern_id: pattern.id,
pattern_name: pattern.name,
author_email: user.email,
series: pattern_includes.map do |chart|
category = chart[:category]
type = TYPE_CHART.select { |_, value| value.include?(category) }.keys&.first
{
type: type,
subtype: SUBTYPE.select { |_, value| value.include? type }.keys&.first,
label: chart[:label],
category: category,
color_id: get_color_id(chart),
data: data(chart)
}
end
}
end
def pattern_includes
@pattern_includes ||= pattern.includes || []
end
def data(chart)
category = chart[:category]
id = chart[:id]
collected_data =
if %w[conditions symptoms treatments].include? category
static_trackables_coordinates(category, scoped_checkins.map(&:id), id)
else
health_factors_trackables_coordinate(category, scoped_checkins, id)
end
average = collected_data.inject(0.0) { |result, datum| result + datum[:y].to_f } / collected_data.count
[{x: collected_data[0]&.fetch(:x), y: average}]
end
def static_trackables_coordinates(category, checkin_ids, id)
find_coordinates_by_checkin(category, checkin_ids, id)
.sort! { |x, y| x[:x].to_time.to_i <=> y[:x].to_time.to_i }
end
def find_coordinates_by_checkin(category, checkin_ids, id)
category_name = category.singularize
"Checkin::#{category_name.camelize}".constantize.where(
checkin_id: {"$in": checkin_ids},
"#{category_name}_id": id
).map do |tr|
coord = {x: tr.checkin.date, y: tr.value}
coord[:is_taken] = tr.is_taken if category == "treatments"
coord
end.uniq
end
def health_factors_trackables_coordinate(category, selected_checkins, id)
is_weather = %w[weathersMeasures].include? category
trackables =
if %w[foods tags].include? category
selected_checkins.select { |checkin| checkin.send("#{category.singularize}_ids").include? id }
.map { |checkin| {x: checkin.date} }.uniq
elsif is_weather
selected_checkins.select(&:weather).map { |checkin| {x: checkin.date, y: checkin.weather.send(id)} }
else
selected_checkins.select(&:harvey_bradshaw_index)
.map { |checkin| {x: checkin.date, y: checkin.harvey_bradshaw_index.score} }
end
trackables.sort! { |x, y| x[:x].to_time.to_i <=> y[:x].to_time.to_i }
end
def get_color_id(chart)
model_name = chart[:category].singularize.camelize
color_id =
if Tracking::TYPES.include? model_name
user.trackings.where(trackable_type: model_name, trackable_id: chart[:id]).pluck(:color_id).compact.last
end
if color_id.present?
used_color_ids << color_id
return color_id
end
color_id = (COLOR_IDS - used_color_ids).shift
used_color_ids << color_id
color_id
end
end

View File

@@ -0,0 +1,72 @@
class Checkin::Creator
attr_reader :user, :date
def initialize(user_id, date)
@user = User.find(user_id)
@date = date
end
# FIXME
# rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
def create!
prev_checkin = user.last_checkin
checkin = Checkin.new(user_id: user.id, date: date, tag_ids: [], food_ids: [])
if prev_checkin && prev_checkin.position.present?
checkin.position_id = prev_checkin.position_id
checkin.weather_id = WeatherRetriever.get(date, checkin.position.postal_code)&.id
end
active_trackings = user.trackings.includes(:trackable).active_at(date)
condition_attrs = []
symptom_attrs = []
treatment_attrs = []
active_trackings.each do |tracking|
trackable = tracking.trackable
most_recent_position = user.profile.most_recent_trackable_position_for(trackable)
if trackable.is_a? Condition
condition_attrs << {
condition_id: trackable.id,
position: most_recent_position || condition_attrs.length,
color_id: tracking.color_id
}
elsif trackable.is_a? Symptom
symptom_attrs << {
symptom_id: trackable.id,
position: most_recent_position || symptom_attrs.length,
color_id: tracking.color_id
}
elsif trackable.is_a? Treatment
treatment_attrs << {
treatment_id: trackable.id,
position: most_recent_position || treatment_attrs.length,
color_id: tracking.color_id,
is_taken: false,
value: user.profile.most_recent_dose_for(trackable.id)
}
end
end
checkin.update!(
conditions_attributes: condition_attrs,
symptoms_attributes: symptom_attrs,
treatments_attributes: treatment_attrs
)
create_or_update_trackable_usages(checkin)
checkin
end
# rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
private
def create_or_update_trackable_usages(checkin)
%w[Condition Symptom Treatment].each do |trackable_class_name|
trackable_class = trackable_class_name.constantize
trackable_id_method = "#{trackable_class_name.downcase}_id"
checkin_trackables_method = trackable_class_name.downcase.pluralize
checkin.send(checkin_trackables_method).each do |checkin_trackable|
trackable_id = checkin_trackable.send(trackable_id_method)
TrackableUsage.create_or_update_by(user: user, trackable: trackable_class.find(trackable_id))
end
end
end
end

View File

@@ -0,0 +1,193 @@
class Checkin::Updater
attr_reader :current_user, :checkin, :permitted_params
def initialize(current_user, params)
@current_user = current_user
id = params.require(:id)
@checkin = Checkin.find(id)
@permitted_params =
params
.require(:checkin)
.permit(
:note, :postal_code, :weather_id, :promotion_skipped_at,
tag_ids: [],
food_ids: [],
conditions_attributes: [:id, :value, :condition_id, :color_id, :position, :_destroy],
symptoms_attributes: [:id, :value, :symptom_id, :color_id, :position, :_destroy],
treatments_attributes: [:id, :value, :treatment_id, :is_taken, :color_id, :position, :_destroy]
)
.tap do |p|
set_most_recent_doses(p[:treatments_attributes])
update_trackables_positions(p)
p[:tag_ids] = [] if p[:tag_ids].nil?
p[:food_ids] = [] if p[:food_ids].nil?
end
end
def update!
checkin.update!(permitted_params.except(:postal_code))
if checkin.date.today?
save_most_recent_doses
save_most_recent_trackables_positions
end
update_trackable_usages
position = Position.find_or_create_by(postal_code: permitted_params[:postal_code])
if position.persisted?
checkin.position_id = position.id
checkin.save!
end
checkin
end
private
def update_trackables_positions(params)
%w[Condition Symptom Treatment].each do |trackable_class_name|
update_trackables_positions_on_destroy(trackable_class_name, params)
set_added_trackables_positions(trackable_class_name, params)
end
end
def update_trackables_positions_on_destroy(trackable_class_name, params)
trackables_attrs = trackables_attrs(trackable_class_name, params)
removed_trackables = removed_trackables_attrs(trackables_attrs)
removed_trackables.each do |removed_trackable|
trackables_attrs.each do |trackable|
next if trackable[:position].blank? ||
removed_trackable[:position].blank? ||
trackable[:position] <= removed_trackable[:position]
trackable[:position] -= 1
end
end
end
def set_added_trackables_positions(trackable_class_name, params)
trackables_attrs = trackables_attrs(trackable_class_name, params)
trackable_max = trackables_attrs.reject { |t| t[:position].blank? }.max_by { |t| t[:position] }
max_position = trackable_max.present? ? trackable_max[:position] : 0
added_trackables = added_trackables_attrs(trackables_attrs)
added_trackables.each do |added_trackable|
if added_trackable[:position].blank?
max_position += 1
added_trackable[:position] = max_position
end
end
end
def save_most_recent_trackables_positions
%w[condition symptom treatment].each do |trackable_type|
trackable_class = trackable_type.capitalize.constantize
trackables_attributes = permitted_params["#{trackable_type.pluralize}_attributes".to_sym]
next if trackables_attributes.blank?
trackables_attributes.each do |trackable_attrs|
trackable = trackable_class.find(trackable_attrs["#{trackable_type}_id".to_sym])
current_user.profile.set_most_recent_trackable_position(
trackable, trackable_attrs[:position]
)
end
end
end
def set_most_recent_doses(treatments_attrs)
return if treatments_attrs.blank?
added_trackables_attrs(treatments_attrs).each do |t|
t[:value] = current_user.profile.most_recent_dose_for(t[:treatment_id])
end
end
def save_most_recent_doses
treatments_attributes = permitted_params[:treatments_attributes]
return if treatments_attributes.blank?
treatments_with_doses = treatments_attributes.reject { |t| (t[:value].blank? || t[:is_taken].eql?("false")) }
return if treatments_with_doses.empty?
treatments_with_doses.each do |t|
current_user.profile.set_most_recent_dose(
t[:treatment_id], t[:value]
)
end
current_user.profile.save!
end
def update_trackable_usages
%w[Condition Symptom Treatment].each do |trackable_class_name|
update_removed_trackables_usages(trackable_class_name)
update_added_trackables_usages(trackable_class_name)
end
%w[Tag Food].each do |health_class_name|
update_health_factors_trackable_usages(health_class_name)
end
end
# For trackables that have been removed from checkin, look for TrackableUsage records
# for the current user. When a record exists with count 1 it's destroyed,
# else count is decremented
def update_removed_trackables_usages(trackable_class_name)
removed_trackables = removed_trackables_attrs(
trackables_attrs(trackable_class_name, permitted_params)
)
trackable_class = trackable_class_name.constantize
trackable_id_key = "#{trackable_class_name.downcase}_id".to_sym
removed_trackables.each do |t|
trackable_usage = TrackableUsage.find_by(
user: current_user,
trackable: trackable_class.find(t[trackable_id_key])
)
next if trackable_usage.blank?
if trackable_usage.count.eql?(1)
trackable_usage.destroy
else
trackable_usage.decrement! :count
end
end
end
def removed_trackables_attrs(trackables_attrs)
trackables_attrs.select { |attrs| attrs[:_destroy].eql?("1") }
end
def trackables_attrs(trackable_class_name, params)
attrs_key = "#{trackable_class_name.downcase.pluralize}_attributes".to_sym
trackables_attrs = params[attrs_key]
return [] if trackables_attrs.blank?
trackables_attrs
end
# For trackables that have been added to checkin, look for TrackableUsage records
# for the current user. When a record exists its count is incremented,
# else a new record is created
def update_added_trackables_usages(trackable_class_name)
added_trackables = added_trackables_attrs(
trackables_attrs(trackable_class_name, permitted_params)
)
trackable_class = trackable_class_name.constantize
trackable_id_key = "#{trackable_class_name.downcase}_id".to_sym
added_trackables.each do |t|
trackable = trackable_class.find(t[trackable_id_key])
TrackableUsage.create_or_update_by(user: current_user, trackable: trackable)
end
end
def added_trackables_attrs(trackables_attrs)
trackables_attrs.select { |attrs| attrs[:id].blank? }
end
def update_health_factors_trackable_usages(trackable_class_name)
trackable_class = trackable_class_name.constantize
trackable_key = "#{trackable_class_name.downcase}_ids"
trackable_class.where(id: permitted_params[trackable_key]).each do |health_factor|
TrackableUsage.create_or_update_by(user: current_user, trackable: health_factor)
end
end
end

View File

@@ -0,0 +1,60 @@
class CollectionRetriever
attr_reader :occurrences
def initialize(model, scope, current_user = nil)
# FIXME
# rubocop:disable Style/SignalException
fail ArgumentError unless [:most_popular, :most_recent].include?(scope)
# rubocop:enable Style/SignalException
@model = model
@scope = scope
@ids_key = "#{model.name.underscore}_ids"
@ids_field = "$#{@ids_key}"
@current_user = current_user
end
def retrieve
send @scope
end
private
def most_popular
@occurrences = Checkin.collection.aggregate(
[
# Step 1: have one entry per object id
{"$unwind" => @ids_field},
# Step 2: group by object id and count occurrences
{"$group" => {_id: @ids_field, count: {"$sum" => 1}}},
# Step 3: sort by occurrences, descending
{"$sort" => {count: -1}},
# Step 4: limit to 10 results
{"$limit" => 10}
]
)
@model.where(id: occurrences.map { |o| o["_id"] }, global: true)
end
def most_recent
checkin_relations = Checkin.collection.aggregate(
[
# Step 1: filter by user
{"$match" => {encrypted_user_id: @current_user.encrypted_id}},
# Step 2: have one entry per object id
{"$unwind" => @ids_field},
# Step 3: sort by checkin date, descending
{"$sort" => {date: -1}},
# Step 4: limit to 10 results
{"$limit" => 10}
]
)
@model.accessible_by(current_ability).where(id: checkin_relations.map { |o| o[@ids_key] })
end
def current_ability
Ability.new(@current_user)
end
end

View File

@@ -0,0 +1,43 @@
class DiscussionPosts < AdditionalPosts
attr_accessor :params, :user
TOP_POSTS_DAYS = 7
TOP_POSTS_COUNT = 5
def initialize(params, user)
@params = params
@user = user
end
def show_list
return Post.none unless params
@posts = Post.accessible_by(current_ability).where(_type: "Post")
@posts = @posts.fts(params[:query]) if params[:query].present?
@posts = if params[:id].present? && Post::TOPIC_TYPES.include?(params[:type])
@posts.where("#{params[:type]}_ids": params[:id].to_i)
elsif params[:following].present?
@posts.by_followings(user.topic_following)
else
@posts
end
end
def full_top_post_list
Post.accessible_by(current_ability)
.where(_type: "Post")
.by_followings(user.topic_following)
.where(:created_at.gte => TOP_POSTS_DAYS.days.ago)
.order(comment_reaction_count: :desc)
end
def refined_top_list
posts = full_top_post_list.limit(TOP_POSTS_COUNT)
if Array(posts).size == TOP_POSTS_COUNT
posts
else
posts + add_last_new_posts(TOP_POSTS_COUNT - Array(posts).size, posts.map(&:_id))
end
end
end

View File

@@ -0,0 +1,32 @@
module Google
class RecaptchaVerifier
def initialize
@secret = ENV["RECAPTCHA_SECRET_KEY"]
@url = "https://www.google.com/recaptcha/api/siteverify"
end
def self.exec(response, remoteip = nil)
new.invoke_service(response, remoteip)
end
def invoke_service(response, remoteip)
params = {
secret: @secret,
response: response,
remoteip: remoteip
}
response = connection.post @url, params
return JSON.parse(response.body)["success"] if response.status.eql?(200)
# FIXME
# rubocop:disable Style/SignalException
fail StandardError, "#{self.class}##{__method__} Got HTTP #{response.status}"
# rubocop:enable Style/SignalException
end
def connection
Faraday.new(headers: {"Accept" => "application/json"})
end
end
end

View File

@@ -0,0 +1,26 @@
class PatternCreator
attr_accessor :name, :includes, :user, :start_at, :end_at
def initialize(options)
@name = options[:name]
@start_at = options[:start_at]
@end_at = options[:end_at]
@includes = options[:includes]
@user = User.find_by(id: options[:user_id])
end
def create
Pattern.create(
name: name,
includes: includes,
encrypted_user_id: encrypted_user_id
)
end
private
def encrypted_user_id
SymmetricEncryption.encrypt(user.id)
end
end

View File

@@ -0,0 +1,33 @@
class SummaryPosts < AdditionalPosts
attr_accessor :user, :topic_following, :posts
SUMMARY_HOURS = 24.hours.ago.strftime("%Y-%m-%d")
SUMMARY_POSTS = 3
def initialize(user)
@user = user
@topic_following = user.topic_following
@posts = Post.accessible_by(current_ability)
.where(_type: "Post")
.by_followings(@topic_following)
.where(:created_at.gte => SUMMARY_HOURS)
end
def show_list
posts = list.first(SUMMARY_POSTS)
(posts.count == SUMMARY_POSTS) ? posts : posts + add_last_new_posts(SUMMARY_POSTS - posts.count, posts.map(&:_id))
end
def list
topic_frequency.each_with_object([]) do |obj, array|
array << Post.where(_type: "Post").find(obj[0])
array.last.extend(PostExtender).frequency_topic_priority(topic_frequency[obj[0]])
end
end
protected
def topic_frequency
posts.frequency_by(topic_following).sort_by { |_k, v| v }.reverse.to_h
end
end

View File

@@ -0,0 +1,42 @@
class TrackableCreator
# Assumes trackable initialized by CanCanCan
def initialize(trackable, user)
@trackable = trackable
@user = user
end
def create!
searchable_attr = @trackable.instance_of?(Food) ? "long_desc" : "name"
same_trackable = @trackable
.class
.with_translations(I18n.locale)
.where("#{searchable_attr} ILIKE :name", name: @trackable.name)
.first
@trackable = same_trackable if same_trackable.present?
unless @trackable.global? || user_trackable_exists?
user_trackable_class.create!(
:user => @user, trackable_attr.to_sym => @trackable
)
@trackable.reload
end
@trackable
end
private
def user_trackable_class
"User#{@trackable.class.name}".constantize
end
def trackable_attr
@trackable.class.name.underscore
end
def user_trackable_exists?
return false if @trackable.id.nil?
user_trackable_class.find_by(
:user_id => @user.id, "#{trackable_attr}_id".to_sym => @trackable.id
).present?
end
end

View File

@@ -0,0 +1,24 @@
class TrackingDestroyer
attr_reader :user, :tracking, :date
def initialize(user, tracking, date)
@user = user
@tracking = tracking
@date = date
end
def destroy
# If a tracking for the same trackable started at the same date is found,
# then it can be destroyed because we're in the case of an unwanted action
same_tracking = user.trackings.find_by(
trackable_id: tracking.trackable_id,
trackable_type: tracking.trackable_type,
start_at: date
)
if same_tracking.present?
tracking.destroy
else
tracking.update!(end_at: Time.zone.today)
end
end
end

View File

@@ -0,0 +1,93 @@
class WeatherRetriever
class << self
def get(date, postal_code)
position = Position.find_or_create_by(postal_code: postal_code)
weather = Weather.find_by(date: date, position_id: position&.id)
return weather if weather.present?
if position&.latitude.blank? || position&.longitude.blank?
Rails.logger.warn "No coordinates found for postal_code #{postal_code}: #{position.inspect}"
return
end
forecast = get_forecast(date, position)
if forecast.status != 200
Rails.logger.warn "No forecast found for position #{position.inspect}: response code was #{forecast.status}, headers were #{forecast.headers}, body contained #{forecast.body}"
return
end
create_weather(forecast, position.id)
end
private
def get_forecast(date, position)
Tomorrowiorb.forecast(
"#{position.latitude},#{position.longitude}",
["1d"],
"imperial"
)
end
def create_weather(forecast, position_id)
today = JSON.parse(forecast.body, symbolize_names: true).dig(:timelines, :daily, 0)
the_time = today.dig(:time)
today = today.dig(:values)
rain_intensity = today.dig(:rainIntensityAvg)
sleet_intensity = today.dig(:sleetIntensityAvg)
snow_intensity = today.dig(:snowIntensityAvg)
icon = get_icon_legacy(today)
summary = "General conditions are #{icon}, with an average temperature of #{today[:temperatureAvg]}."
Weather.create(
date: Date.strptime(the_time, "%Y-%m-%d"),
humidity: today.dig(:humidityAvg).round,
icon: icon,
position_id: position_id,
precip_intensity: rain_intensity + sleet_intensity + snow_intensity,
pressure: today.dig(:pressureSurfaceLevelAvg),
summary: summary,
temperature_max: today.dig(:temperatureMax),
temperature_min: today.dig(:temperatureMin)
)
end
def get_icon_legacy(today)
# Our icons do not coverage their full range of weather codes. We could pull in their icons (linked below) on the frontend to expand options
# This method adapts their weather codes to our existing icons as best as possible
# Icons and codes found here: https://docs.tomorrow.io/reference/data-layers-weather-codes
# Icon files here: https://github.com/Tomorrow-IO-API/tomorrow-weather-codes
# Daily forecast is always daytime weather codes / icons regardless of actual time
code = if today["weatherCodeMin"]
today["weatherCodeMin"]
elsif today["weatherCodeFullDay"]
today["weatherCodeFullDay"]
else
today["weatherCode"]
end
case code
when 1000, 1100
"clear-day"
when 1101
"partly-cloudy-day"
when 1102, 1001, 8000
"cloudy"
when 2000, 2100
"fog"
when 4000, 4001, 4200, 4201
"rain"
when 5000, 5001, 5100, 5101
"snow"
when 6000, 6001, 6200, 6201, 7000, 7101, 7102
"sleet"
else
"default"
end
end
end
end