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,3 @@
//= link_tree ../images
//= link_directory ../javascripts .js
//= link_directory ../stylesheets .css

View File

@@ -0,0 +1,28 @@
module Api
module V1
class AwsSesController < ApplicationController
skip_authorize_resource only: [:mail_it, :notification]
skip_before_action :authenticate_user!, only: [:mail_it, :notification]
def notification
message_type = request.headers["x-amz-sns-message-type"]
# sns_topic = request.headers['x-amz-sns-topic-arn']
raw_post = request.raw_post
if message_type.include? "Confirmation"
send_subscription_confirmation(raw_post)
elsif message_type.include? "Notification"
EmailRejectDispatcher.perform_async(raw_post)
end
render nothing: true, status: 200
end
def send_subscription_confirmation(raw_post)
json = JSON.parse(raw_post)
open(json["SubscribeURL"])
end
end
end
end

View File

@@ -0,0 +1,9 @@
module Api
module V1
class ChartListsController < ApplicationController
def show
render json: ChartListService.new(current_user: current_user).as_json
end
end
end
end

View File

@@ -0,0 +1,32 @@
module Api
module V1
class ChartsController < ApplicationController
def show
chart = Chart.new(chart_params)
# FIXME
# rubocop:disable Style/SignalException
fail(ActiveRecord::RecordInvalid, chart) if chart.invalid?
# rubocop:enable Style/SignalException
render json: chart
end
def chart_params
includes_params = {
tags: [],
foods: [],
symptoms: [],
conditions: [],
treatments: [],
weathersMeasures: [],
harveyBradshawIndices: []
}
params.permit(:id, :start_at, :end_at, includes: includes_params).tap do |whitelist|
whitelist[:user] = current_user
end
end
end
end
end

View File

@@ -0,0 +1,31 @@
module Api
module V1
class ChartsPatternController < ApplicationController
skip_before_action :authenticate_user!, only: [:index]
def index
offset = charts_pattern_params[:offset].to_i
start_at = (charts_pattern_params[:start_at].to_date - offset.days).to_s
end_date = charts_pattern_params[:end_at].to_date
end_at = ((Time.current.to_date == end_date) ? end_date : (end_date + offset.days)).to_s
@patterns = Pattern.where(id: {"$in": charts_pattern_params[:pattern_ids] || []})
@extended_patterns = @patterns.map do |pattern|
pattern.extend(PatternExtender).form_chart_data(start_at: start_at,
end_at: end_at,
pattern: pattern)
end
render json: @extended_patterns, meta: {color_ids: Flaredown::Colorable::IDS}
end
private
def charts_pattern_params
params.permit(:start_at, :end_at, :offset, pattern_ids: [])
end
end
end
end

View File

@@ -0,0 +1,40 @@
module Api
module V1
class CheckinsController < ApplicationController
def index
date = params[:date]
if date.blank? && params.require(:page)
render json: current_user.checkins.where(:note.nin => [nil, ""]).order_by(date: :desc).page(params[:page]).per(10)
else
render json: current_user.checkins.includes([:harvey_bradshaw_index, :promotion_rate, :conditions, :symptoms, :treatments]).select { |x|
x.date.to_date == Date.parse(date)
}
end
end
def show
render json: Checkin.find(id)
end
def create
date = params.require(:checkin).require(:date)
parsed = DateTime.parse(date)
now = DateTime.current
save_date = DateTime.new(parsed.year, parsed.month, parsed.day, now.hour, now.minute, now.second)
checkin = Checkin::Creator.new(current_user.id, save_date).create!
render json: checkin
end
def update
render json: Checkin::Updater.new(current_user, params).update!
end
private
def id
params.require(:id)
end
end
end
end

View File

@@ -0,0 +1,45 @@
module Api
module V1
class CommentsController < ApplicationController
load_and_authorize_resource
skip_before_action :authenticate_user!, only: [:index]
def index
render json: @comments.where(:id.in => params[:ids]).order_by(created_at: :asc)
end
def show
render json: @comment
end
def create
@comment.encrypted_user_id = current_user.encrypted_id
if @comment.save
UpdatePostCountersJob.perform_async(parent_id: create_params[:post_id], parent_type: "Post")
unless @comment.encrypted_user_id == @comment.post.encrypted_user_id
Notification.create(
kind: :comment,
notificateable: @comment,
encrypted_user_id: @comment.encrypted_user_id,
encrypted_notify_user_id: @comment.post.encrypted_user_id
)
end
DiscussionMention.perform_async(current_user.encrypted_id, @comment.id.to_s)
render json: @comment, status: :created
else
render json: {errors: @comment.errors}, status: :unprocessable_entity
end
end
private
def create_params
params.require(:comment).permit(:body, :post_id)
end
end
end
end

View File

@@ -0,0 +1,33 @@
module Api
module V1
class ConditionsController < ApplicationController
load_and_authorize_resource
skip_before_action :authenticate_user!, only: [:show]
def index
@conditions = @conditions.includes(:translations)
@conditions = ids.present? ? @conditions.where(id: ids) : @conditions.order(:name).limit(50)
render json: @conditions
end
def show
render json: @condition
end
def create
render json: TrackableCreator.new(@condition, current_user).create!
end
private
def create_params
params.require(:condition).permit(:name)
end
def ids
@ids ||= params[:ids] if params[:ids].is_a?(Array)
end
end
end
end

View File

@@ -0,0 +1,32 @@
module Api
module V1
class CountriesController < ApplicationController
skip_before_action :authenticate_user!
def index
render json: Country.all, each_serializer: CountrySerializer
end
def show
country = Country.find_country_by_alpha2(alpha2)
# FIXME
# rubocop:disable Style/SignalException
fail ActiveRecord::RecordNotFound if country.nil?
# rubocop:enable Style/SignalException
render json: country, serializer: CountrySerializer
end
private
def alpha2
id = params.require(:id)
match_data = /^[[:alpha:]]{2}$/.match(id)
# FIXME
# rubocop:disable Style/SignalException
fail(ActionController::BadRequest, "id param must be a 2 alphabetic characters string") if match_data.nil?
# rubocop:enable Style/SignalException
match_data[0]
end
end
end
end

View File

@@ -0,0 +1,11 @@
module Api
module V1
class DataExportSchedulesController < ApplicationController
def create
DataExportJob.perform_later(current_user.id)
head :created
end
end
end
end

View File

@@ -0,0 +1,27 @@
module Api
module V1
class DayHabitsController < ApplicationController
skip_before_action :authenticate_user!
def index
render json: DayHabit.all
end
def show
day_habit = DayHabit.find(day_habit_id)
render json: day_habit
end
private
def day_habit_id
id = params.require(:id)
# FIXME
# rubocop:disable Style/SignalException
fail(ActionController::BadRequest, "id param is not a valid day_habit id") unless DayHabit.all_ids.include?(id)
# rubocop:enable Style/SignalException
id
end
end
end
end

View File

@@ -0,0 +1,9 @@
module Api
module V1
class DiscoursesController < ApplicationController
def create
render json: {url: DiscourseClient.new(current_user, params).generate_url}
end
end
end
end

View File

@@ -0,0 +1,29 @@
module Api
module V1
class EducationLevelsController < ApplicationController
skip_before_action :authenticate_user!
def index
render json: EducationLevel.all
end
def show
education_level = EducationLevel.find(education_level_id)
render json: education_level
end
private
def education_level_id
id = params.require(:id)
# FIXME
# rubocop:disable Style/SignalException
unless EducationLevel.all_ids.include?(id)
fail(ActionController::BadRequest, "id param is not a valid education_level id")
end
# rubocop:enable Style/SignalException
id
end
end
end
end

View File

@@ -0,0 +1,27 @@
module Api
module V1
class EthnicitiesController < ApplicationController
skip_before_action :authenticate_user!
def index
render json: Ethnicity.all
end
def show
ethnicity = Ethnicity.find(ethnicity_id)
render json: ethnicity
end
private
def ethnicity_id
id = params.require(:id)
# FIXME
# rubocop:disable Style/SignalException
fail(ActionController::BadRequest, "id param is not a valid ethnicity id") unless Ethnicity.all_ids.include?(id)
# rubocop:enable Style/SignalException
id
end
end
end
end

View File

@@ -0,0 +1,42 @@
module Api
module V1
class FoodsController < ApplicationController
load_and_authorize_resource
def index
@foods = @foods.includes(:translations)
foods =
if ids.present?
@foods.where(id: ids)
elsif scope.present?
CollectionRetriever.new(Food, scope, current_user).retrieve
end
render json: foods
end
def show
render json: @food
end
def create
render json: TrackableCreator.new(@food, current_user).create!
end
private
def create_params
{long_desc: params.require(:food).require(:name)}
end
def ids
@ids ||= params[:ids]
end
def scope
@scope ||= params[:scope]&.to_sym
end
end
end
end

View File

@@ -0,0 +1,28 @@
module Api
module V1
class HarveyBradshawIndicesController < ApplicationController
load_and_authorize_resource
def show
render json: @harvey_bradshaw_index
end
def create
@harvey_bradshaw_index.save
render json: @harvey_bradshaw_index
end
private
def create_params
params.require(:harvey_bradshaw_index).permit(
:abdominal_mass, :abdominal_pain, :abscess,
:anal_fissure, :aphthous_ulcers, :arthralgia,
:checkin_id, :erythema_nodosum, :new_fistula,
:pyoderma_gangrenosum, :stools, :uveitis, :well_being
)
end
end
end
end

View File

@@ -0,0 +1,19 @@
module Api
module V1
class InvitationsController < ApplicationController
skip_before_action :authenticate_user!
def show
render json: Invitation.find(params[:id])
end
def update
invitation = Invitation.find(params[:id])
invitation.accept!(
params.require(:invitation).permit(:email, :password, :password_confirmation)
)
render json: invitation
end
end
end
end

View File

@@ -0,0 +1,52 @@
module Api
module V1
class NotificationsController < ApplicationController
def index
notifications = Notification.where(encrypted_notify_user_id: current_user.encrypted_id)
authorize_collection :index, notifications
render json: {notifications: notifications.aggregated_by_kind_and_subject}
end
def update
notifications = Notification.where(notification_params)
authorize_collection :update, notifications
if notifications.update_all(unread: false)
render json: {notifications: notifications.aggregated_by_kind_and_subject}
else
render json: {errors: notifications.map(&:errors).compact}, status: :unprocessable_entity
end
end
def destroy
notifications = Notification.where(notification_params)
authorize_collection :destroy, notifications
if notifications.destroy
head :no_content
else
render json: {errors: notifications.map(&:errors).compact}, status: :unprocessable_entity
end
end
private
def notification_params
parameters = params.permit(:notificateable_id, :notificateable_type)
parameters[:notificateable_type] = parameters[:notificateable_type].titleize
parameters[:encrypted_notify_user_id] = current_user.encrypted_id
parameters
end
def authorize_collection(name, collection)
collection.each { |element| authorize! name, element }
end
end
end
end

View File

@@ -0,0 +1,35 @@
module Api
module V1
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
Devise.omniauth_providers.each do |provider|
define_method provider do
handle_omniauth
end
end
def failure
Rails.logger.warn("Api::V1::OmniauthCallbacksController#failure: #{failure_message}".yellow)
render json: {errors: failure_message}, status: 401
end
private
def handle_omniauth
user = User.find_for_database_authentication(email: email_param)
if user && user.invitation_token.nil?
render json: user, root: false, serializer: SessionSerializer
else
render json: {errors: "User not found"}, status: 401
end
end
def oauth_params
@oauth_params ||= ActionController::Parameters.new(request.env["omniauth.auth"])
end
def email_param
oauth_params.fetch(:info).fetch(:email)
end
end
end
end

View File

@@ -0,0 +1,60 @@
module Api
module V1
class OracleRequestsController < ApplicationController
skip_before_action :authenticate_user!
serialization_scope :oracle_token
load_resource
def show
render json: @oracle_request
end
def create
if oracle_token.present?
@oracle_request.token = oracle_token
else
loop do
@oracle_request.token = SecureRandom.uuid
break unless OracleRequest.where(token: @oracle_request.token).exists?
end
end
@oracle_request.save
render json: @oracle_request, serializer: OracleRequestWithTokenSerializer
end
def update
if @oracle_request.can_edit?(oracle_token)
@oracle_request.update!(create_params)
render json: @oracle_request
else
render json: {errors: "Unauthorized"}, status: :unauthorised
end
end
private
def create_params
params.require(:oracle_request).permit(
:age,
:sex_id,
responce: [
:name,
:confidence,
:correction
],
symptom_ids: []
)
end
def oracle_token
request.headers["X-Oracle-Token"]
end
end
end
end

View File

@@ -0,0 +1,56 @@
module Api
module V1
class PasswordsController < ApplicationController
skip_before_action :authenticate_user!
def show
user = user_signed_in? ? current_user : User.with_reset_password_token(params[:id])
if user.blank?
raise ActiveRecord::RecordNotFound, "User not found"
else
render json: user, token: params[:id], serializer: PasswordSerializer
end
end
def create
user = User.find_by!(email: email_param.downcase)
return unless user.send_reset_password_instructions
render json: user, serializer: PasswordSerializer
end
def update
if user_signed_in?
if current_user.update_with_password(update_password_params)
render json: current_user, token: params[:id], serializer: PasswordSerializer
else
render json: {errors: current_user.errors}, status: :unprocessable_entity
end
else
user = User.reset_password_by_token(update_password_by_token_params)
if user.errors.empty?
render json: user, token: params[:id], serializer: PasswordSerializer
else
render json: {errors: user.errors}, status: :unprocessable_entity
end
end
end
private
def email_param
params.require(:password).fetch(:email)
end
def update_password_params
params.require(:password).permit(:current_password, :password, :password_confirmation)
end
def update_password_by_token_params
params.require(:password).permit(:reset_password_token, :password, :password_confirmation)
end
end
end
end

View File

@@ -0,0 +1,68 @@
module Api
module V1
class PatternsController < ApplicationController
load_and_authorize_resource
skip_before_action :authenticate_user!, only: [:index]
def index
page = params[:page] || 1
pattern_ids = params[:pattern_ids]
@patterns =
if pattern_ids.present?
Pattern.where(id: {"$in" => pattern_ids})
else
Pattern.accessible_by(current_ability).where(encrypted_user_id: encrypted_user_id)
end
render json: @patterns.page(page).per(10)
end
def show
pattern = Pattern.find_by(id: pattern_params[:id])
render json: pattern
end
def create
@pattern = PatternCreator.new(pattern_params.to_h).create
render json: @pattern
end
def update
@pattern.update(pattern_params)
render json: @pattern
end
def destroy
pattern = Pattern.find_by(id: params[:id])
authorize! :destroy, pattern
if pattern.destroy
head :no_content
else
render json: {errors: pattern.errors}, status: :unprocessable_entity
end
end
private
def pattern_params
params.require(:pattern)
.permit(:name, :start_at, :end_at, includes: [:id, :category, :label])
.merge(user_id: current_user.id)
end
def current_ability
@current_ability ||= Ability.new(current_user)
end
def encrypted_user_id
@encrypted_user_id ||= SymmetricEncryption.encrypt(current_user.id)
end
end
end
end

View File

@@ -0,0 +1,18 @@
module Api
module V1
class PostablesController < ApplicationController
load_and_authorize_resource
def index
render json: PostableSerializer.new(
@postables
.where(encrypted_user_id: current_user.encrypted_id)
.order_by(created_at: :desc)
.page(params[:page])
.per(20),
current_user
)
end
end
end
end

View File

@@ -0,0 +1,46 @@
module Api
module V1
class PostsController < ApplicationController
load_and_authorize_resource
skip_before_action :authenticate_user!, only: [:index, :show]
def index
if params[:summary]
render json: SummaryPosts.new(current_user).show_list
else
@posts = DiscussionPosts.new(params, current_user).show_list
results = @posts
.includes([:comments, :notifications, :reactions])
.order(last_commented: :desc, created_at: :desc)
.page(params[:page])
.per(10)
render json: results
end
end
def show
render json: @post
end
def create
@post.encrypted_user_id = current_user.encrypted_id
if @post.save
render json: @post, status: :created
else
render json: {errors: @post.errors}, status: :unprocessable_entity
end
end
private
def create_params
params.require(:post).permit(
:title, :body,
tag_ids: [], symptom_ids: [], condition_ids: [], treatment_ids: []
)
end
end
end
end

View File

@@ -0,0 +1,75 @@
module Api
module V1
class ProfilesController < ApplicationController
require "sidekiq/api"
load_and_authorize_resource
skip_before_action :authenticate_user!, only: [:index]
def index
post = Post.find(params[:post_id])
encrypted_user_ids =
(post.comments.distinct(:encrypted_user_id) << post.encrypted_user_id).uniq.map do |encrypted_id|
SymmetricEncryption.decrypt(encrypted_id)
end
@profiles = Profile.where(user_id: encrypted_user_ids).where.not(slug_name: nil)
render json: @profiles.map { |profile| profile.attributes.slice("screen_name", "slug_name") }
end
def show
render json: @profile
end
def update
initial_onboarding_reminder = params.dig(:profile, :onboarding_reminder)
@profile.assign_attributes(update_params.merge(transform_hash_time))
time_changed = @profile.checkin_reminder_at_changed? || @profile.time_zone_name_changed?
@profile.save!
if time_changed || initial_onboarding_reminder
delete_old_job(@profile.reminder_job_id)
job_id = CheckinReminderJob.perform_in(get_reminder_time.minutes, @profile.id, @profile.checkin_reminder_at)
@profile.update_column(:reminder_job_id, job_id)
end
current_user.profile.reload
set_locale
render json: @profile
end
private
def update_params
params.require(:profile).permit(
:country_id, :sex_id, :onboarding_step_id, :birth_date,
:day_habit_id, :education_level_id, :day_walking_hours,
:pressure_units, :temperature_units, :screen_name, :notify,
:checkin_reminder, :time_zone_name, :notify_top_posts, ethnicity_ids: []
)
end
def transform_hash_time
checkin_reminder_at = params.require(:profile)[:checkin_reminder_at]
user_time = checkin_reminder_at && checkin_reminder_at.values.join(":")
{checkin_reminder_at: user_time.try(:to_time, :utc)}
end
def get_reminder_time
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
def delete_old_job(enqueued_job_id)
Sidekiq::ScheduledSet.new.find_job(enqueued_job_id)&.delete
end
end
end
end

View File

@@ -0,0 +1,35 @@
module Api
module V1
class PromotionRatesController < ApplicationController
load_and_authorize_resource
def show
render json: @promotion_rate
end
def create
@promotion_rate.save
render json: @promotion_rate
end
def update
@promotion_rate.update(resource_params.merge(additional_params))
render json: @promotion_rate
end
private
def resource_params
params.require(:promotion_rate).permit(:checkin_id, :score, :feedback)
end
def additional_params
user = @promotion_rate.checkin.user
{user_created_at: user.created_at}
end
end
end
end

View File

@@ -0,0 +1,17 @@
module Api
module V1
class PushersController < ApplicationController
def create
render json: Flaredown.pusher.authenticate!(current_user, socket_id)
rescue
render json: {errors: "Bad authentication"}, status: "403"
end
private
def socket_id
params.require(:socket_id)
end
end
end
end

View File

@@ -0,0 +1,77 @@
module Api
module V1
class ReactionsController < ApplicationController
def create
react(__method__)
end
def update
react(__method__)
end
def destroy
reaction = Reaction.where(reaction_params).first
authorize! :destroy, reaction
if reaction.destroy
UpdatePostCountersJob.perform_async(parent_id: reaction_params[:reactable_id],
parent_type: reaction_params[:reactable_type])
head :no_content
else
render json: {errors: reaction.errors}, status: :unprocessable_entity
end
end
private
def react(method_name)
reaction = Reaction.find_or_initialize_by(reaction_params)
authorize! method_name, reaction
if reaction.save
UpdatePostCountersJob.perform_async(parent_id: reaction_params[:reactable_id],
parent_type: reaction_params[:reactable_type])
unless reaction.encrypted_user_id == reaction.reactable.encrypted_user_id
Notification.create(
kind: :reaction,
notificateable: reaction.reactable,
encrypted_user_id: reaction.encrypted_user_id,
encrypted_notify_user_id: reaction.reactable.encrypted_user_id
)
end
reaction.id = params[:id] if params[:id].present?
render json: serialized_reaction(reaction), status: :created
else
render json: {errors: reaction.errors}, status: :unprocessable_entity
end
end
def reaction_params
reaction = params.require(:reaction)
{
value: reaction[:value],
reactable_id: reaction[:reactable_id],
reactable_type: reaction[:reactable_type].titleize,
encrypted_user_id: current_user.encrypted_id
}
end
def serialized_reaction(reaction)
ReactionSerializer
.new(
Reaction.similar_to(reaction).values_count_with_participated(current_user.encrypted_id),
reaction.reactable_id.to_s,
reaction.reactable_type
)
.serialize_one
end
end
end
end

View File

@@ -0,0 +1,15 @@
module Api
module V1
class RegistrationsController < ApplicationController
skip_before_action :authenticate_user!
def create
render json: Registration.create!(params)
end
def destroy
render json: Registration.delete!(params)
end
end
end
end

View File

@@ -0,0 +1,34 @@
module Api
module V1
class SearchesController < ApplicationController
SEARCH_MAPPER = {
"dose" => Search::ForDose,
"food" => Search::ForFood,
"topic" => Search::ForTopic
}.freeze
skip_before_action :authenticate_user!, only: :show
def show
search = (SEARCH_MAPPER[resource_param] || Search).new(search_params)
# FIXME
# rubocop:disable Style/SignalException
fail(ActiveRecord::RecordInvalid, search) if search.invalid?
# rubocop:enable Style/SignalException
render json: search, serializer: SearchSerializer
end
def search_params
params.permit(:resource, :scope, query: [:name, :treatment_id]).tap do |params|
params[:user] = current_user
end
end
def resource_param
params[:resource]
end
end
end
end

View File

@@ -0,0 +1,29 @@
module Api
module V1
class SessionsController < ApplicationController
skip_before_action :authenticate_user!
def create
# FIXME
# rubocop:disable Style/SignalException
fail "missing information" if params[:user].nil?
fail "invalid email or password" if user.nil?
# rubocop:enable Style/SignalException
render json: user, root: false, serializer: SessionSerializer
rescue => e
render json: {errors: Array(e.message)}, status: 401
end
private
def user
@user ||=
begin
user = User.find_for_database_authentication(email: params[:user][:email])
user if user && user.valid_password?(params[:user][:password])
end
end
end
end
end

View File

@@ -0,0 +1,27 @@
module Api
module V1
class SexesController < ApplicationController
skip_before_action :authenticate_user!
def index
render json: Sex.all
end
def show
sex = Sex.find(sex_id)
render json: sex
end
private
def sex_id
id = params.require(:id)
# FIXME
# rubocop:disable Style/SignalException
fail(ActionController::BadRequest, "id param is not a valid sex id") unless Sex.all_ids.include?(id)
# rubocop:enable Style/SignalException
id
end
end
end
end

View File

@@ -0,0 +1,33 @@
module Api
module V1
class SymptomsController < ApplicationController
load_and_authorize_resource
skip_before_action :authenticate_user!, only: [:show]
def index
@symptoms = @symptoms.includes(:translations)
@symptoms = ids.present? ? @symptoms.where(id: ids) : @symptoms.order(:name).limit(50)
render json: @symptoms
end
def show
render json: @symptom
end
def create
render json: TrackableCreator.new(@symptom, current_user).create!
end
private
def create_params
params.require(:symptom).permit(:name)
end
def ids
@ids ||= params[:ids] if params[:ids].is_a?(Array)
end
end
end
end

View File

@@ -0,0 +1,40 @@
module Api
module V1
class TagsController < ApplicationController
load_and_authorize_resource
skip_before_action :authenticate_user!, only: [:show]
def index
@tags = @tags.includes(:translations)
if ids.present?
@tags = @tags.where(id: ids)
elsif scope.present?
@tags = CollectionRetriever.new(Tag, scope, current_user).retrieve
end
render json: @tags
end
def show
render json: @tag
end
def create
render json: TrackableCreator.new(@tag, current_user).create!
end
private
def create_params
params.require(:tag).permit(:name)
end
def ids
@ids ||= params[:ids]
end
def scope
@scope ||= params[:scope].try(:to_sym)
end
end
end
end

View File

@@ -0,0 +1,32 @@
module Api
module V1
class TopicFollowingsController < ApplicationController
load_and_authorize_resource
def show
render json: @topic_following
end
def update
if @topic_following.update(update_params)
render json: @topic_following, status: :ok
else
render json: {errors: @topic_following.errors}, status: :unprocessable_entity
end
end
private
def update_params
empty_params = {
"tag_ids" => [],
"symptom_ids" => [],
"condition_ids" => [],
"treatment_ids" => []
}
empty_params.merge(params.require(:topic_following).permit(empty_params))
end
end
end
end

View File

@@ -0,0 +1,46 @@
module Api
module V1
class TrackingsController < ApplicationController
load_and_authorize_resource except: :create
def index
render json: @trackings.by_trackable_type(trackable_type).active_at(at)
end
def show
render json: @tracking
end
def create
tracking = Tracking.new(create_params.merge(start_at: Time.zone.today, user: current_user))
authorize! :create, tracking
tracking.save!
current_user.topic_following.add_topic("#{tracking.trackable_type.downcase}_ids", tracking.trackable_id)
render json: tracking
end
def destroy
TrackingDestroyer.new(current_user, @tracking, Time.zone.today).destroy
head :no_content
end
private
def at
Time.zone.parse(params.require(:at))
end
def trackable_type
params.require(:trackable_type)
end
def create_params
params.require(:tracking).permit(:trackable_id, :trackable_type, :color_id)
end
end
end
end

View File

@@ -0,0 +1,33 @@
module Api
module V1
class TreatmentsController < ApplicationController
load_and_authorize_resource
skip_before_action :authenticate_user!, only: [:show]
def index
@treatments = @treatments.includes(:translations)
@treatments = ids.present? ? @treatments.where(id: ids) : @treatments.order(:name).limit(50)
render json: @treatments
end
def show
render json: @treatment
end
def create
render json: TrackableCreator.new(@treatment, current_user).create!
end
private
def create_params
params.require(:treatment).permit(:name)
end
def ids
@ids ||= params[:ids] if params[:ids].is_a?(Array)
end
end
end
end

View File

@@ -0,0 +1,30 @@
module Api
module V1
class UnsubscribesController < ApplicationController
skip_before_action :authenticate_user!
def update
@profile = Profile.find_by(notify_token: activation_params[:notify_token])
@profile&.update_column(attribute_dispatcher_key, false)
render json: @profile
end
private
def activation_params
params.permit(:notify_token, :notify_top_posts, :stop_remind)
end
def attribute_dispatcher_key
if activation_params[:stop_remind]
:checkin_reminder
elsif activation_params[:notify_top_posts]
:notify_top_posts
else
:notify
end
end
end
end
end

View File

@@ -0,0 +1,23 @@
module Api
module V1
class UsersController < ApplicationController
load_and_authorize_resource
def show
render json: @user
end
def update
@user.update!(user_params)
render json: @user
end
private
def user_params
params.require(:user).permit(:email)
end
end
end
end

View File

@@ -0,0 +1,19 @@
module Api
module V1
class WeathersController < ApplicationController
def index
unless params[:postal_code].blank?
weather = WeatherRetriever.get(Date.parse(params.require(:date)), params.require(:postal_code))
end
authorize! :read, weather
if weather.present?
render json: weather
else
render json: {weathers: []}
end
end
end
end
end

View File

@@ -0,0 +1,50 @@
class ApplicationController < ActionController::API
include CanCan::ControllerAdditions
include ActionController::Serialization
include ExceptionLogger
before_action :authenticate_user_from_token!, if: :presence_of_authentication_token?
before_action :authenticate_user!, except: [:root]
before_action :set_locale
def root
render json: {
ios: {
major: 0,
minor: 0
},
android: {
major: 0,
minor: 0
}
}
end
protected
def set_locale
I18n.locale = user_signed_in? ? current_user.locale : I18n.default_locale
rescue I18n::InvalidLocale
# FIXME
Rails.logger.warn("'#{current_user.profile.locale}' locale for user '#{current_user.email}' not available or invalid, using default")
I18n.locale = I18n.default_locale
end
private
def authenticate_user_from_token!
user = User.find_by(authorization)
sign_in user, store: false if user
end
def authorization
@authorization ||=
/^Token token="(?<token>.*)", email="(?<email>.*)"$/.match(request.headers["Authorization"]) || {}
{authentication_token: @authorization[:token], email: @authorization[:email]}
end
def presence_of_authentication_token?
authorization[:authentication_token].present?
end
end

View File

@@ -0,0 +1,56 @@
module ExceptionLogger
extend ActiveSupport::Concern
included do
# keep this on top
if Rails.env.production?
rescue_from "Exception" do |exception|
render json: {errors: exception.message}, status: :unprocessable_entity
end
end
rescue_from "ActiveRecord::RecordNotFound", "Mongoid::Errors::DocumentNotFound" do
render json: {errors: ["Resource Not Found"]}, status: 404
end
rescue_from "ActionController::UnknownFormat" do
render json: {errors: ["Format not supported"]}, status: 406
end
rescue_from "ActiveRecord::RecordInvalid", "Mongoid::Errors::Validations" do |exception|
log_exception(exception)
render json: {errors: exception.record.errors}, status: :unprocessable_entity
end
rescue_from "ActionController::ParameterMissing" do |exception|
render json: {errors: ["Required parameter missing: #{exception.param}"]}, status: :unprocessable_entity
end
rescue_from "ActionController::BadRequest" do |exception|
render json: {errors: ["Bad request: #{exception.message}"]}, status: :bad_request
end
rescue_from "CanCan::AccessDenied" do |exception|
log_exception(exception)
render json: {errors: ["Unauthorized"]}, status: :unauthorized
end
end
protected
# --- FOR RAILS 4: ---
## @exception = env["action_dispatch.exception"]
## exception_wrapper = ActionDispatch::ExceptionWrapper.new(env, @exception)
# --- FOR RAILS 5: ---
def log_exception(exception)
application_trace = ActionDispatch::ExceptionWrapper.new(request.env["action_dispatch.backtrace_cleaner"], exception)
trace = application_trace.application_trace
trace = trace.map! { |t| " #{t}\n" }
Rails.logger.error "\n#{exception.class.name} (#{exception.message}):\n#{trace.join}"
end
end

View File

@@ -0,0 +1,18 @@
module PatternExtender
attr_accessor :start_at, :end_at, :pattern, :user, :chart_data
def form_chart_data(options)
@start_at = options[:start_at]
@end_at = options[:end_at]
@pattern = options[:pattern]
@user = options[:user]
@chart_data = ChartsPattern.new(start_at: start_at, end_at: end_at, pattern: pattern, user: user).chart_data
chart_data.merge(used_color_ids: used_color_ids)
end
def used_color_ids
chart_data[:series].map { |item| item[:color_id] }.compact
end
end

View File

@@ -0,0 +1,7 @@
module PostExtender
attr_reader :priority
def frequency_topic_priority(priority)
@priority = priority
end
end

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

View File

@@ -0,0 +1,9 @@
class ApplicationMailer < ActionMailer::Base
default from: Rails.application.secrets.smtp_email_from
REGEXP = /\A\s*([-\p{L}\d+._]{1,64})@((?:[-\p{L}\d]+\.)+\p{L}{2,})\s*\z/i
def valid_email?(email)
email =~ REGEXP
end
end

View File

@@ -0,0 +1,21 @@
class CheckinReminderMailer < ApplicationMailer
layout "mailer_layout"
def remind(notification_hash)
@email = notification_hash[:email]
return unless valid_email?(@email)
user = User.find_by(email: @email)
notify_token = user&.notify_token
return unless notify_token
return if user&.rejected_type.present? # Rejected via AWS SES
@click_here_link = Rails.application.secrets.base_url
@unsubscribe_link =
Rails.application.secrets.base_url + "/unsubscribe/#{User.find_by(email: @email).notify_token}?stop_remind"
attachments.inline["optional_email_img.png"] = File.read("public/images/optional_email_img.png")
mail(to: @email, subject: I18n.t("checkin_reminder_mailer.subject"))
end
end

View File

@@ -0,0 +1,23 @@
class NotificationsMailer < ApplicationMailer
layout "mailer_layout"
helper_method :resource_title, :resource_url
def notify(notification_hash)
@email = notification_hash[:email]
return unless valid_email?(@email)
@unsubscribe_link = Rails.application.secrets.base_url + "/unsubscribe/#{User.find_by(email: @email).notify_token}"
@data = notification_hash[:data]
mail(to: @email, subject: "New response to your Flaredown message")
end
def resource_title(object_id, class_name = "Post")
class_name.constantize.find(object_id).title.to_s
end
def resource_url(object_id, class_name = "Post")
[ENV["BASE_URL"], class_name.tableize, object_id].map(&:to_s).join("/")
end
end

View File

@@ -0,0 +1,12 @@
class PromotionRate::LowRateMailer < ApplicationMailer
layout "mailer_layout"
def show(email, objects, start_date, end_date)
@email = email
@objects = objects
@start_date = start_date
@end_date = end_date
mail(to: @email, subject: I18n.t("promotion_rate.low_rate.subject"))
end
end

View File

@@ -0,0 +1,12 @@
class PromotionRate::StatisticMailer < ApplicationMailer
layout "mailer_layout"
def show(email, objects, start_date, end_date)
@email = email
@objects = objects
@start_date = start_date
@end_date = end_date
mail(to: @email, subject: I18n.t("promotion_rate.statistic.subject"))
end
end

View File

@@ -0,0 +1,14 @@
class TopPostsMailer < ApplicationMailer
layout "mailer_layout"
def notify(mailer_hash)
@email = mailer_hash[:email]
return unless valid_email?(@email)
@unsubscribe_link =
Rails.application.secrets.base_url + "/unsubscribe/#{mailer_hash[:notify_token]}?notify_top_posts=false"
@top_posts = Post.where(:_type => "Post", :_id.in => mailer_hash[:top_posts_ids])
mail(to: @email, subject: 'Weekly "top posts"')
end
end

View File

@@ -0,0 +1,7 @@
class UserDataMailer < ApplicationMailer
def trackings_csv(email, csv_data)
attachments["data.csv"] = csv_data
mail to: email
end
end

View File

@@ -0,0 +1,100 @@
class Ability
include CanCan::Ability
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
def initialize(user)
user ||= User.new # guest user (not logged in)
can :manage, User, id: user.id
can :manage, Profile, user_id: user.id
can :show, Condition
can :index, Condition, global: true
cannot :index, Condition, global: false
can :index, Condition, global: false, id: user.trackings.where(trackable_type: "Condition").pluck(:trackable_id)
can :read, Condition, global: false, id: popular_trackable_ids("Condition")
can :create, Condition, global: false
can :manage, Condition, id: user.condition_ids
can :show, Food
can :index, Food, global: true
cannot :index, Food, global: false
can :index, Food, global: false, id: user.food_ids
can :create, Food, global: false
can :manage, Food, id: user.food_ids
can :read, HarveyBradshawIndex, encrypted_user_id: user.encrypted_id
can :create, HarveyBradshawIndex do |hbi|
checkin = hbi.checkin
checkin.encrypted_user_id == user.encrypted_id && checkin.available_for_hbi?
end
can :read, PromotionRate, encrypted_user_id: user.encrypted_id
can [:create, :update], PromotionRate do |rate|
checkin = rate.checkin
checkin.encrypted_user_id == user.encrypted_id && checkin.available_for_promotion?
end
can [:create], Pattern
can [:read, :update, :destroy], Pattern, encrypted_user_id: user.encrypted_id
can [:read], ChartsPattern
can :read, [Comment, Post]
can :create, [Comment, Post], encrypted_user_id: user.encrypted_id
can :read, Notification
can :destroy, Notification, encrypted_notify_user_id: user.encrypted_id
can :update, Notification, encrypted_notify_user_id: user.encrypted_id
can :read, Postable, encrypted_user_id: user.encrypted_id
can :read, Reaction
can [:create, :update, :destroy], Reaction, encrypted_user_id: user.encrypted_id
can :show, Symptom
can :index, Symptom, global: true
cannot :index, Symptom, global: false
can :index, Symptom, global: false, id: user.trackings.where(trackable_type: "Symptom").pluck(:trackable_id)
can :read, Symptom, global: false, id: popular_trackable_ids("Symptom")
can :create, Symptom, global: false
can :manage, Symptom, id: user.symptom_ids
can [:read, :update], TopicFollowing, encrypted_user_id: user.encrypted_id
can :show, Treatment
can :index, Treatment, global: true
cannot :index, Treatment, global: false
can :index, Treatment, global: false, id: user.trackings.where(trackable_type: "Treatment").pluck(:trackable_id)
can :read, Symptom, global: false, id: popular_trackable_ids("Treatment")
can :create, Treatment, global: false
can :manage, Treatment, id: user.treatment_ids
can :manage, Tracking, user_id: user.id
can :show, Tag
can :index, Tag, global: true
cannot :index, Tag, global: false
can :index, Tag, global: false, id: user.tag_ids
can :create, Tag, global: false
can :manage, Tag, id: user.tag_ids
can :read, Weather if user.persisted?
can :read, NilClass if user.persisted?
end
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
private
def popular_trackable_ids(trackable_class_name)
user_trackable_class = "User#{trackable_class_name}".constantize
trackable_id_attr = "#{trackable_class_name.underscore}_id".to_sym
counts = user_trackable_class.group(trackable_id_attr).count
min_popularity = Flaredown.config.trackables_min_popularity
counts.select { |_id, count| count >= min_popularity }.keys
end
end

View File

@@ -0,0 +1,3 @@
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end

View File

@@ -0,0 +1,38 @@
class Chart
include ActiveModel::Serialization
include ActiveModel::Model
attr_accessor :id, :user, :start_at, :end_at, :includes
#
# Validations
#
validates :user, :start_at, :end_at, presence: true
def checkins
@checkins ||= user.checkins.by_date(start_at.to_date, end_at.to_date).map do |checkin|
checkin.includes = includes
checkin
end
end
def trackables
@_trackables ||= trackings.map(&:trackable)
end
private
def trackings
return Tracking.none if includes.blank?
relation = Tracking.includes(:trackable)
relation
.where(trackable_type: :Condition, trackable_id: includes[:conditions] || [])
.or(relation.where(trackable_type: :Symptom, trackable_id: includes[:symptoms] || []))
.or(relation.where(trackable_type: :Treatment, trackable_id: includes[:treatments] || []))
.where(user_id: user.id)
.active_in_range(start_at.to_date, end_at.to_date)
end
end

View File

@@ -0,0 +1,165 @@
class Checkin
include Mongoid::Document
HBI_PERIODICITY = 7
PR_PERIODICITY = 7
PR_START_FROM = 7
attr_accessor :includes
FIELD_TYPE = %w[tag food].freeze
#
# Fields
#
field :date, type: DateTime
field :food_ids, type: Array, default: []
field :note, type: String
field :postal_code, type: String
field :tag_ids, type: Array
field :weather_id, type: Integer
field :encrypted_user_id, type: String, encrypted: {type: :integer}
field :position_id, type: Integer
field :promotion_skipped_at, type: Date
#
# Relations
#
has_one :harvey_bradshaw_index
has_one :promotion_rate, dependent: :destroy
has_many :treatments, class_name: "Checkin::Treatment"
has_many :conditions, class_name: "Checkin::Condition"
has_many :symptoms, class_name: "Checkin::Symptom"
accepts_nested_attributes_for :conditions, :symptoms, :treatments, allow_destroy: true
#
# Indexes
#
index(encrypted_user_id: 1)
index(date: 1, encrypted_user_id: 1)
#
# Validations
#
validates :encrypted_user_id, presence: true
validates :date, presence: true
#
# Scopes
#
scope :by_date, ->(startkey, endkey) { where(:date.gte => startkey, :date.lte => endkey) }
def user
@user ||= User.find(user_id)
end
def weather
@weather ||= Weather.find_by(id: weather_id)
end
def position
@position ||= Position.find_by(id: position_id)
end
def tags
if includes
@_tags_included ||= Tag.where(id: tag_ids - (includes[:tags] || []))
else
@_tags ||= Tag.where(id: tag_ids)
end
end
def foods
if includes
@_foods_included ||= Food.where(id: food_ids - (includes[:foods] || []))
else
@_foods ||= Food.where(id: food_ids)
end
end
def available_for_hbi?
return true if harvey_bradshaw_index
return true unless latest_hbi
HBI_PERIODICITY - ((latest_hbi.date)...date).count < 1
end
def available_for_promotion?
return true if promotion_rate
return false if user_has_already_rated?
return false unless date.today?
return ready_for_promotion? if latest_skipped_pr_at.blank?
PR_PERIODICITY - ((latest_skipped_pr_at)...date).count < 1
end
class Condition
include Mongoid::Document
include Checkin::Trackable
include Checkin::Fiveable
field :condition_id, type: Integer
validates :condition_id, uniqueness: {scope: :checkin_id}
end
class Symptom
include Mongoid::Document
include Checkin::Trackable
include Checkin::Fiveable
field :symptom_id, type: Integer
validates :symptom_id, uniqueness: {scope: :checkin_id}
end
class Treatment
include Mongoid::Document
include Checkin::Trackable
#
# Fields
#
field :treatment_id, type: Integer
field :value, type: String
field :is_taken, type: Boolean
#
# Indexes
#
index(treatment_id: 1)
index(treatment_id: 1, is_taken: 1, value: 1)
#
# Validations
#
validates :treatment_id, uniqueness: {scope: :checkin_id}
end
def self.ids_by_category_attrs(category_name, trackable_id)
where(id: {"$in" => "Checkin::#{category_name.camelize}".constantize
.where(:"#{category_name}_id" => trackable_id, :value.ne => nil).pluck(:checkin_id)})
end
private
def latest_hbi
@_latest_hbi ||= HarveyBradshawIndex.where(encrypted_user_id: encrypted_user_id).order(date: :desc).first
end
def latest_skipped_pr_at
@_lates_skipped_pr ||=
Checkin
.where(encrypted_user_id: encrypted_user_id)
.order_by(promotion_skipped_at: :desc)
.first&.promotion_skipped_at
end
def ready_for_promotion?
user.created_at <= PR_START_FROM.day.ago
end
def user_has_already_rated?
@_rated ||= PromotionRate.where(encrypted_user_id: encrypted_user_id).any?
end
end

View File

@@ -0,0 +1,8 @@
module Checkin::Fiveable
extend ActiveSupport::Concern
included do
field :value, type: Integer
validates :value, inclusion: {in: (0..4), allow_blank: true}
end
end

View File

@@ -0,0 +1,16 @@
module Checkin::Trackable
extend ActiveSupport::Concern
included do
#
# Fields
#
field :position, type: Integer
field :color_id, type: String
#
# Relations
#
belongs_to :checkin, index: true
end
end

View File

@@ -0,0 +1,20 @@
class Comment
include Mongoid::Document
include Mongoid::Timestamps
include Usernameable
store_in collection: "postables"
field :body, type: String
field :_type, type: String, default: -> { self.class.name }
field :encrypted_user_id, type: String, encrypted: {type: :integer}
validates :body, :post, :encrypted_user_id, presence: true
belongs_to :post, index: true, counter_cache: true
has_many :reactions, as: :reactable, dependent: :destroy
has_many :notifications, as: :notificateable, dependent: :destroy
after_create { post.update(last_commented: Time.current) }
end

View File

@@ -0,0 +1,21 @@
module Authenticatable
extend ActiveSupport::Concern
included do
#
# Devise
#
devise :database_authenticatable,
:rememberable,
:recoverable,
:trackable,
:validatable,
:invitable,
:omniauthable
#
# Validates
#
validates :password, confirmation: true
end
end

View File

@@ -0,0 +1,35 @@
module Topicable
extend ActiveSupport::Concern
include Mongoid::Document
include Mongoid::Timestamps
TOPIC_TYPES = %w[tag symptom condition treatment].freeze
included do
field :tag_ids, type: Array, default: []
field :symptom_ids, type: Array, default: []
field :condition_ids, type: Array, default: []
field :treatment_ids, type: Array, default: []
field :encrypted_user_id, type: String, encrypted: {type: :integer}
validates :encrypted_user_id, presence: true
TOPIC_TYPES.each do |relative|
pluralized_relative = relative.pluralize
define_method(:"#{pluralized_relative}") do
ivar_name = :"@_#{pluralized_relative}"
value = instance_variable_get(ivar_name)
return value if value.present?
value = instance_variable_set(ivar_name, relative.classify.constantize.where(id: send("#{relative}_ids")))
value
end
end
end
end

View File

@@ -0,0 +1,7 @@
module Usernameable
def user_name
Profile.select(:screen_name).find_by!(user_id: SymmetricEncryption.decrypt(encrypted_user_id)).screen_name
rescue SymmetricEncryption::CipherError
""
end
end

View File

@@ -0,0 +1,19 @@
# == Schema Information
#
# Table name: conditions
#
# id :integer not null, primary key
# global :boolean default(TRUE)
# created_at :datetime not null
# updated_at :datetime not null
# trackable_usages_count :integer default(0)
#
class Condition < ActiveRecord::Base
has_many :condition_translations, class_name: "Condition::Translation"
#
# Localized attributes
#
translates :name
end

View File

@@ -0,0 +1,15 @@
class DayHabit < RankedEnum
class << self
def all_ids
%w[lying_down sitting_down on_feet driving working_with_hands]
end
end
def name
I18n.t "#{key}.#{id}.name"
end
def description
I18n.t "#{key}.#{id}.description"
end
end

View File

@@ -0,0 +1,10 @@
class Dose
include ActiveModel::Serialization
include ActiveModel::Model
attr_accessor :name
def id
name.parameterize
end
end

View File

@@ -0,0 +1,7 @@
class EducationLevel < RankedEnum
class << self
def all_ids
%w[less_than_hi_school hi_school college_degree bachelors_degree advanced_degree]
end
end
end

View File

@@ -0,0 +1,8 @@
class Ethnicity < RankedEnum
class << self
def all_ids
%w[ latino white east_asian south_asian black oceanian
middle_eastern native_american african other not_sure ]
end
end
end

View File

@@ -0,0 +1,20 @@
class Feedback
include Mongoid::Document
include Mongoid::Timestamps
#
# Fields
#
field :email, type: String
field :delete_reason, type: String
#
# Indexes
#
index(date: 1, encrypted_user_id: 1)
#
# Validations
#
validates :email, :delete_reason, presence: true
end

View File

@@ -0,0 +1,54 @@
class Food < ActiveRecord::Base
LANG_MAP = {
en: :english,
it: :italian
}.freeze
has_many :food_translations, class_name: "Food::Translation"
translates :long_desc, :shrt_desc, :comname, :sciname
alias_method :name, :long_desc
class << self
def fts(query, limit, user_id)
find_by_sql(
[
fts_sql,
{
user_id: user_id,
lang: LANG_MAP[I18n.locale] || :simple,
query: query.strip.split(/(\s*,\s*)|\s+/).map { |s| "#{s}:*" }.join("&"),
limit: limit,
locale: I18n.locale
}
]
)
end
private
def fts_sql
<<-SQL.strip_heredoc
SELECT id, long_desc, searchable, global
FROM (
SELECT
to_tsvector(:lang, ft.long_desc) AS searchable,
foods.id AS id,
foods.global AS global,
ft.long_desc AS long_desc
FROM foods
INNER JOIN food_translations AS ft ON ft.food_id = foods.id
WHERE ft.locale = :locale AND foods.id IN (
SELECT uf.food_id
FROM user_foods AS uf
WHERE uf.user_id = :user_id
) OR foods.global IS TRUE
) f
WHERE f.searchable @@ to_tsquery(:lang, :query)
ORDER BY ts_rank_cd(f.searchable, to_tsquery(:lang, :query), 1) DESC
LIMIT :limit
SQL
end
end
end

View File

@@ -0,0 +1,69 @@
class HarveyBradshawIndex
include Mongoid::Document
#
# Fields
#
field :date, type: Date
field :score, type: Integer
field :stools, type: Integer
field :well_being, type: Integer
field :abdominal_mass, type: Integer
field :abdominal_pain, type: Integer
field :abscess, type: Boolean
field :uveitis, type: Boolean
field :arthralgia, type: Boolean
field :new_fistula, type: Boolean
field :anal_fissure, type: Boolean
field :aphthous_ulcers, type: Boolean
field :erythema_nodosum, type: Boolean
field :pyoderma_gangrenosum, type: Boolean
field :encrypted_user_id, type: String, encrypted: {type: :integer}
#
# Relations
#
belongs_to :checkin, index: true
#
# Indexes
#
index(date: 1, encrypted_user_id: 1)
#
# Validations
#
validates :checkin, :stools, :well_being, :abdominal_mass, :abdominal_pain, presence: true
validates :checkin_id, uniqueness: true
#
# Callbacks
#
before_save :calculate_score
before_create :set_date_and_user_id
private
def set_date_and_user_id
self.date = checkin.date
self.encrypted_user_id = checkin.encrypted_user_id
end
def calculate_score
self.score = stools + well_being + abdominal_mass + abdominal_pain
self.score += [
abscess,
uveitis,
arthralgia,
new_fistula,
anal_fissure,
aphthous_ulcers,
erythema_nodosum,
pyoderma_gangrenosum
].count { |v| v }
end
end

View File

@@ -0,0 +1,27 @@
class Invitation
include ActiveModel::Serialization
attr_reader :id, :email, :user
def initialize(id, user)
@id = id
@user = user
@email = user.email
end
def accept!(params = {})
User.accept_invitation!({invitation_token: id}.merge(params))
end
def self.find(id)
# FIXME
# rubocop:disable Rails/DynamicFindBy
user = User.find_by_invitation_token(id, true)
# rubocop:enable Rails/DynamicFindBy
# FIXME
# rubocop:disable Style/SignalException
fail ActiveRecord::RecordNotFound if user.nil?
# rubocop:enable Style/SignalException
new(id, user)
end
end

Some files were not shown because too many files have changed in this diff Show More