added 260907 version of worker toolkit
This commit is contained in:
100
worker-toolkit-flaredown/repo/backend/app/models/ability.rb
Normal file
100
worker-toolkit-flaredown/repo/backend/app/models/ability.rb
Normal 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
|
||||
@@ -0,0 +1,3 @@
|
||||
class ApplicationRecord < ActiveRecord::Base
|
||||
self.abstract_class = true
|
||||
end
|
||||
38
worker-toolkit-flaredown/repo/backend/app/models/chart.rb
Normal file
38
worker-toolkit-flaredown/repo/backend/app/models/chart.rb
Normal 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
|
||||
165
worker-toolkit-flaredown/repo/backend/app/models/checkin.rb
Normal file
165
worker-toolkit-flaredown/repo/backend/app/models/checkin.rb
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
20
worker-toolkit-flaredown/repo/backend/app/models/comment.rb
Normal file
20
worker-toolkit-flaredown/repo/backend/app/models/comment.rb
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
10
worker-toolkit-flaredown/repo/backend/app/models/dose.rb
Normal file
10
worker-toolkit-flaredown/repo/backend/app/models/dose.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
class Dose
|
||||
include ActiveModel::Serialization
|
||||
include ActiveModel::Model
|
||||
|
||||
attr_accessor :name
|
||||
|
||||
def id
|
||||
name.parameterize
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
@@ -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
|
||||
20
worker-toolkit-flaredown/repo/backend/app/models/feedback.rb
Normal file
20
worker-toolkit-flaredown/repo/backend/app/models/feedback.rb
Normal 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
|
||||
54
worker-toolkit-flaredown/repo/backend/app/models/food.rb
Normal file
54
worker-toolkit-flaredown/repo/backend/app/models/food.rb
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,84 @@
|
||||
class Notification
|
||||
include Mongoid::Document
|
||||
include Mongoid::Timestamps
|
||||
include Usernameable
|
||||
|
||||
after_initialize :set_defaults
|
||||
|
||||
field :kind, type: String
|
||||
field :encrypted_user_id, type: String, encrypted: {type: :integer}
|
||||
field :encrypted_notify_user_id, type: String, encrypted: {type: :integer}
|
||||
field :delivered, type: Boolean, default: false
|
||||
field :post_id, type: String
|
||||
field :unread, type: Boolean, default: true
|
||||
|
||||
validates :notificateable, :encrypted_user_id, :encrypted_notify_user_id, presence: true
|
||||
|
||||
belongs_to :notificateable, polymorphic: true
|
||||
|
||||
index notificateable_id: 1, notificateable_type: 1
|
||||
|
||||
def set_defaults
|
||||
assign_attributes(post_id: ((notificateable._type == "Comment") ? notificateable.post_id : notificateable.id).to_s)
|
||||
end
|
||||
|
||||
class << self
|
||||
def aggregated_by_kind_and_subject
|
||||
group = {}
|
||||
|
||||
each do |n|
|
||||
key = [n.kind, n.notificateable_id.to_s, n.notificateable_type.downcase]
|
||||
|
||||
group[key] ||= []
|
||||
group[key] << n
|
||||
end
|
||||
|
||||
normalized_notifications(group)
|
||||
end
|
||||
|
||||
def count_by_types
|
||||
group = criteria.group(_id: "$kind", count: {"$sum": 1})
|
||||
aggregate = collection.aggregate(group.pipeline)
|
||||
aggregate.reduce({}) { |acc, notification|
|
||||
h = {notification["_id"] => notification["count"]}
|
||||
acc.merge(h)
|
||||
}
|
||||
end
|
||||
|
||||
def groupped_by_post_and_kind(encrypted_user_id)
|
||||
where(encrypted_notify_user_id: encrypted_user_id)
|
||||
.group_by(&:post_id).deep_transform_keys!(&:to_s)
|
||||
.each_with_object({}) { |obj, hash| hash[obj[0]] = aggregate_group(obj[1].group_by(&:kind)) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def normalized_notifications(group)
|
||||
group.map do |group_keys, notifications|
|
||||
notificateable = notifications.first.notificateable
|
||||
is_comment = notificateable._type == "Comment"
|
||||
|
||||
{
|
||||
id: [notifications.map(&:created_at).max.to_i].concat(group_keys).join("_"),
|
||||
kind: group_keys.first,
|
||||
count: notifications.count,
|
||||
post_id: is_comment ? notificateable.post_id.to_s : group_keys[1],
|
||||
post_title: (is_comment ? notificateable.post : notificateable)&.title,
|
||||
notificateable_id: group_keys[1],
|
||||
notificateable_type: group_keys.last,
|
||||
unread: notifications.map(&:unread).last,
|
||||
notifier_username: notifier_username(notifications)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def aggregate_group(groupped_by_kind)
|
||||
groupped_by_kind.each_with_object({}) { |kind, hash| hash[kind[0]] = kind[1].count }
|
||||
end
|
||||
|
||||
def notifier_username(notifications)
|
||||
notifier_user_id = SymmetricEncryption.decrypt(notifications.first.encrypted_user_id)
|
||||
Profile.find_by(user_id: notifier_user_id)&.screen_name
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
class OracleRequest
|
||||
include Mongoid::Document
|
||||
|
||||
field :age, type: Integer
|
||||
|
||||
field :token, type: String
|
||||
field :sex_id, type: String
|
||||
|
||||
field :responce, type: Array, default: []
|
||||
field :symptom_ids, type: Array, default: []
|
||||
|
||||
index(token: 1)
|
||||
|
||||
def sex
|
||||
@_sex ||= Sex.find(sex_id)
|
||||
end
|
||||
|
||||
def symptoms
|
||||
@_symptoms ||= Symptom.where(id: symptom_ids)
|
||||
end
|
||||
|
||||
def can_edit?(oracle_token)
|
||||
token == oracle_token
|
||||
end
|
||||
end
|
||||
18
worker-toolkit-flaredown/repo/backend/app/models/pattern.rb
Normal file
18
worker-toolkit-flaredown/repo/backend/app/models/pattern.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
class Pattern
|
||||
include Mongoid::Document
|
||||
include Mongoid::Timestamps
|
||||
|
||||
field :encrypted_user_id, type: String, encrypted: {type: :integer}
|
||||
field :start_at, type: Date
|
||||
field :end_at, type: Date
|
||||
field :includes, type: Array, default: []
|
||||
field :name, type: String
|
||||
|
||||
index(encrypted_user_id: 1)
|
||||
|
||||
validates :encrypted_user_id, presence: true
|
||||
|
||||
def author
|
||||
@author ||= User.find_by(id: SymmetricEncryption.decrypt(encrypted_user_id))
|
||||
end
|
||||
end
|
||||
24
worker-toolkit-flaredown/repo/backend/app/models/position.rb
Normal file
24
worker-toolkit-flaredown/repo/backend/app/models/position.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
class Position < ActiveRecord::Base
|
||||
validates :postal_code, presence: true
|
||||
validate :geocoder_position_present
|
||||
|
||||
before_create :set_location_name
|
||||
|
||||
def set_location_name
|
||||
state = geocoder_position.state || geocoder_position.province
|
||||
|
||||
self.location_name = [geocoder_position.city, state, geocoder_position.country].compact.join(", ")
|
||||
self.latitude = geocoder_position.latitude
|
||||
self.longitude = geocoder_position.longitude
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def geocoder_position_present
|
||||
errors.add(:postal_code, "No coordinates found for postal_code #{postal_code}}") if geocoder_position.blank?
|
||||
end
|
||||
|
||||
def geocoder_position
|
||||
@geocoder_position ||= Geocoder.search(postal_code).first
|
||||
end
|
||||
end
|
||||
63
worker-toolkit-flaredown/repo/backend/app/models/post.rb
Normal file
63
worker-toolkit-flaredown/repo/backend/app/models/post.rb
Normal file
@@ -0,0 +1,63 @@
|
||||
class Post
|
||||
include Topicable
|
||||
include Usernameable
|
||||
|
||||
attr_accessor :postable_id
|
||||
|
||||
store_in collection: "postables"
|
||||
|
||||
field :body, type: String
|
||||
field :title, type: String
|
||||
field :_type, type: String, default: -> { self.class.name }
|
||||
|
||||
field :comments_count, type: Integer, default: 0
|
||||
field :last_commented, type: DateTime, default: -> { Time.current }
|
||||
field :comment_reaction_count, type: Integer, default: 0
|
||||
|
||||
validates :body, :title, presence: true
|
||||
|
||||
validate :topic_presence
|
||||
|
||||
has_many :comments, dependent: :destroy
|
||||
has_many :reactions, as: :reactable, dependent: :destroy
|
||||
has_many :notifications, as: :notificateable, dependent: :destroy
|
||||
|
||||
index(body: "text", title: "text")
|
||||
|
||||
def update_counters
|
||||
set comment_reaction_count: comments_count + reactions.count
|
||||
end
|
||||
|
||||
def self.fts(q)
|
||||
where("$text": {"$search": q, "$language": I18n.locale.to_s})
|
||||
end
|
||||
|
||||
def self.by_followings(followings)
|
||||
any_of(
|
||||
{:tag_ids.in => followings.tag_ids},
|
||||
{:symptom_ids.in => followings.symptom_ids},
|
||||
{:condition_ids.in => followings.condition_ids},
|
||||
{:treatment_ids.in => followings.treatment_ids} # rubocop:disable Style/BracesAroundHashParameters
|
||||
)
|
||||
end
|
||||
|
||||
def self.frequency_by(topic_following)
|
||||
where(_type: "Post")
|
||||
.by_followings(topic_following)
|
||||
.each_with_object({}) do |post, hash|
|
||||
result_count = 0
|
||||
%w[symptom_ids condition_ids tag_ids treatment_ids].each do |trackable|
|
||||
result_count += (post.send(trackable) & topic_following.send(trackable)).count
|
||||
end
|
||||
hash[post.id] = result_count
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def topic_presence
|
||||
TOPIC_TYPES.each { |topic| return true if send(topic.pluralize).any? }
|
||||
|
||||
errors.add(:topics, "should have at least one of associated #{TOPIC_TYPES.join(",")}")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
class Postable
|
||||
include Mongoid::Document
|
||||
end
|
||||
168
worker-toolkit-flaredown/repo/backend/app/models/profile.rb
Normal file
168
worker-toolkit-flaredown/repo/backend/app/models/profile.rb
Normal file
@@ -0,0 +1,168 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: profiles
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# user_id :integer
|
||||
# country_id :string
|
||||
# birth_date :date
|
||||
# sex_id :string
|
||||
# onboarding_step_id :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# ethnicity_ids_string :string
|
||||
# day_habit_id :string
|
||||
# education_level_id :string
|
||||
# day_walking_hours :integer
|
||||
# most_recent_doses :hstore
|
||||
# screen_name :string
|
||||
# most_recent_conditions_positions :hstore
|
||||
# most_recent_symptoms_positions :hstore
|
||||
# most_recent_treatments_positions :hstore
|
||||
#
|
||||
|
||||
class Profile < ActiveRecord::Base
|
||||
enum pressure_units: %i[mb in]
|
||||
enum temperature_units: %i[f c]
|
||||
|
||||
#
|
||||
# Associations
|
||||
#
|
||||
belongs_to :user
|
||||
|
||||
delegate :email, to: :user
|
||||
|
||||
MIN_AGE = 16
|
||||
|
||||
#
|
||||
# Validations
|
||||
#
|
||||
|
||||
validates :country_id, inclusion: {
|
||||
in: Country.codes
|
||||
}, if: -> { country_id.present? }
|
||||
|
||||
validates :sex_id, inclusion: {
|
||||
in: Sex.all_ids
|
||||
}, if: -> { sex_id.present? }
|
||||
|
||||
validates :day_habit_id, inclusion: {
|
||||
in: DayHabit.all_ids,
|
||||
message: "%{value} is not a valid day_habit_id"
|
||||
}, if: -> { day_habit_id.present? }
|
||||
|
||||
validates :education_level_id, inclusion: {
|
||||
in: EducationLevel.all_ids,
|
||||
message: "%{value} is not a valid education_level_id"
|
||||
}, if: -> { education_level_id.present? }
|
||||
|
||||
validate :ethnicity_ids_are_valid, if: -> { ethnicity_ids_string.present? }
|
||||
validate :age_of_birth, if: -> { birth_date.present? }
|
||||
|
||||
def ethnicity_ids_are_valid
|
||||
ethnicity_ids.each do |id|
|
||||
unless Ethnicity.all_ids.include?(id)
|
||||
errors.add(:ethnicity_ids, "#{id} is not a valid ethnicity_id")
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Callbacks
|
||||
#
|
||||
before_create :generate_notify_token
|
||||
before_save :ensure_slug_name, if: :screen_name_changed?
|
||||
|
||||
TIMEZONE_PARAMS = {
|
||||
time: [20, 0],
|
||||
time_zone_name: "America/New_York"
|
||||
}.freeze
|
||||
|
||||
NOTIFICATION_ATTRS = %w[checkin_reminder notify_top_posts notify].freeze
|
||||
|
||||
#
|
||||
# Instance Methods
|
||||
#
|
||||
|
||||
def country
|
||||
@country ||= Country.find_country_by_alpha2(country_id) if country_id.present?
|
||||
end
|
||||
|
||||
def locale
|
||||
country.languages.first if country.present?
|
||||
end
|
||||
|
||||
def age
|
||||
return if birth_date.nil?
|
||||
|
||||
today = Time.zone.today
|
||||
age = today.year - birth_date.year
|
||||
|
||||
(today < birth_date + age.years) ? age - 1 : age
|
||||
end
|
||||
|
||||
def ethnicity_ids
|
||||
ethnicity_ids_string&.split(",") || []
|
||||
end
|
||||
|
||||
def ethnicity_ids=(ids)
|
||||
self.ethnicity_ids_string = ids.join(",")
|
||||
end
|
||||
|
||||
def set_most_recent_dose(treatment_id, dose)
|
||||
most_recent_doses[treatment_id.to_s] = dose
|
||||
end
|
||||
|
||||
def most_recent_dose_for(treatment_id)
|
||||
most_recent_doses[treatment_id.to_s]
|
||||
end
|
||||
|
||||
def set_most_recent_trackable_position(trackable, position)
|
||||
trackable_type = trackable.class.name.underscore
|
||||
send("set_most_recent_#{trackable_type}_position", trackable, position)
|
||||
end
|
||||
|
||||
def most_recent_trackable_position_for(trackable)
|
||||
trackable_type = trackable.class.name.underscore
|
||||
send("most_recent_#{trackable_type}_position_for", trackable)
|
||||
end
|
||||
|
||||
%w[condition symptom treatment].each do |trackable_type|
|
||||
define_method "set_most_recent_#{trackable_type}_position" do |trackable, position|
|
||||
send(
|
||||
"most_recent_#{trackable_type.pluralize}_positions"
|
||||
)[trackable.id.to_s] = position.to_s
|
||||
end
|
||||
|
||||
define_method "most_recent_#{trackable_type}_position_for" do |trackable|
|
||||
trackables_positions = send("most_recent_#{trackable_type.pluralize}_positions")
|
||||
|
||||
return nil if trackables_positions.blank?
|
||||
|
||||
trackables_positions[trackable.id.to_s].to_i
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_notify_token
|
||||
self.notify_token =
|
||||
loop do
|
||||
random_token = SecureRandom.hex
|
||||
break random_token unless Profile.exists?(notify_token: random_token)
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_slug_name
|
||||
slug_name = screen_name && screen_name.split(/\s/).map(&:capitalize).join("")
|
||||
assign_attributes(slug_name: slug_name)
|
||||
end
|
||||
|
||||
def age_of_birth
|
||||
if age < MIN_AGE
|
||||
errors.add(:birth_date, "You must be #{MIN_AGE} years or older to use this app according
|
||||
to international privacy regulations. Sorry :(")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,66 @@
|
||||
class PromotionRate
|
||||
include Mongoid::Document
|
||||
include Mongoid::Timestamps
|
||||
|
||||
MIN_SCORE_FOR_REVIWS = 8
|
||||
|
||||
field :date, type: Date
|
||||
field :score, type: Integer
|
||||
field :feedback, type: String
|
||||
field :encrypted_user_id, type: String, encrypted: {type: :integer}
|
||||
field :user_created_at, type: Date
|
||||
|
||||
belongs_to :checkin, index: true
|
||||
|
||||
index(date: 1, encrypted_user_id: 1)
|
||||
|
||||
validates :checkin, presence: true
|
||||
validates :checkin_id, uniqueness: true
|
||||
|
||||
before_create :set_date_and_user_id
|
||||
|
||||
# '17-09-2017'
|
||||
def self.group_by_score_and_date(start_str, end_str)
|
||||
start_day = start_str.to_date
|
||||
end_day = end_str.to_date
|
||||
|
||||
PromotionRate.collection.aggregate(
|
||||
[
|
||||
{"$match": {
|
||||
date: {
|
||||
"$gte": start_day,
|
||||
"$lte": end_day
|
||||
}
|
||||
}},
|
||||
"$group": {_id: "$score", count: {"$sum": 1}}
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
# '17-09-2017'
|
||||
def self.filter_low_scores_by_date(start_str, end_str)
|
||||
start_day = start_str.to_date
|
||||
end_day = end_str.to_date
|
||||
|
||||
PromotionRate.collection.aggregate(
|
||||
[
|
||||
{"$match": {
|
||||
score: {
|
||||
"$lte": MIN_SCORE_FOR_REVIWS
|
||||
},
|
||||
date: {
|
||||
"$gte": start_day,
|
||||
"$lte": end_day
|
||||
}
|
||||
}}
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def set_date_and_user_id
|
||||
self.date = checkin.date
|
||||
self.encrypted_user_id = checkin.encrypted_user_id
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
class RankedEnum
|
||||
include ActiveModel::Serialization
|
||||
|
||||
attr_accessor :id, :rank
|
||||
|
||||
def initialize(id, rank)
|
||||
@id = id
|
||||
@rank = rank
|
||||
end
|
||||
|
||||
class << self
|
||||
def all_ids
|
||||
[]
|
||||
end
|
||||
|
||||
def find(id)
|
||||
all.find { |obj| obj.id.eql?(id) }
|
||||
end
|
||||
|
||||
def all
|
||||
Rails.cache.fetch(name.pluralize.underscore) do
|
||||
result = []
|
||||
all_ids.each_with_index { |id, i| result << new(id, i + 1) }
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def name
|
||||
I18n.t! "#{key}.#{id}"
|
||||
rescue I18n::MissingTranslationData
|
||||
# This avoids strings in the UI like "translation missing: pt.sex.male"
|
||||
I18n.t "#{key}.#{id}", locale: :en
|
||||
end
|
||||
|
||||
def key
|
||||
self.class.name.underscore
|
||||
end
|
||||
end
|
||||
36
worker-toolkit-flaredown/repo/backend/app/models/reaction.rb
Normal file
36
worker-toolkit-flaredown/repo/backend/app/models/reaction.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
class Reaction
|
||||
include Mongoid::Document
|
||||
|
||||
field :value, type: String
|
||||
field :encrypted_user_id, type: String, encrypted: {type: :integer}
|
||||
|
||||
validates :value, :reactable, :encrypted_user_id, presence: true
|
||||
|
||||
belongs_to :reactable, polymorphic: true
|
||||
|
||||
class << self
|
||||
def similar_to(reaction)
|
||||
where(
|
||||
value: reaction.value,
|
||||
reactable_id: reaction.reactable_id,
|
||||
reactable_type: reaction.reactable_type
|
||||
)
|
||||
end
|
||||
|
||||
def values_count_with_participated(encrypted_user_id)
|
||||
reactions_for_user = where(encrypted_user_id: encrypted_user_id).pluck(:value, :_id).to_h
|
||||
|
||||
aggregation = criteria.group(:_id => "$value", :count.sum => 1)
|
||||
collection.aggregate(aggregation.pipeline).map do |reaction|
|
||||
# A real ID is only required when the current user has reacted. Otherwise the value is meaningless
|
||||
# Use a random string to try to demonstrate that
|
||||
{
|
||||
id: (reactions_for_user[reaction["_id"]] || SecureRandom.hex(16)).to_s,
|
||||
value: reaction["_id"],
|
||||
count: reaction["count"],
|
||||
participated: reactions_for_user.key?(reaction["_id"])
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,77 @@
|
||||
class Registration
|
||||
include ActiveModel::Validations
|
||||
include ActiveModel::Serialization
|
||||
|
||||
attr_accessor :user, :errors, :captcha_response, :screen_name, :birth_date
|
||||
|
||||
validates :captcha_response, :screen_name, presence: true
|
||||
validate :captcha_response_verified
|
||||
|
||||
def initialize(params)
|
||||
@user_params = permitted(params).to_hash
|
||||
@screen_name = @user_params.delete("screen_name")
|
||||
@birth_date = @user_params.delete("birth_date")
|
||||
@captcha_response = @user_params.delete("captcha_response")
|
||||
@errors = ActiveModel::Errors.new(self)
|
||||
end
|
||||
|
||||
def save!
|
||||
# FIXME
|
||||
# rubocop:disable Style/SignalException
|
||||
fail ActiveRecord::RecordInvalid, self unless valid?
|
||||
# rubocop:enable Style/SignalException
|
||||
|
||||
begin
|
||||
@user = User.create!(@user_params)
|
||||
profile = @user.profile
|
||||
|
||||
profile.birth_date = birth_date
|
||||
profile.screen_name = screen_name if screen_name.present?
|
||||
|
||||
profile.save!
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
self.errors = e.record.errors
|
||||
raise ActiveRecord::RecordInvalid, self
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def self.create!(params)
|
||||
new(params).save!
|
||||
end
|
||||
|
||||
def self.delete!(params)
|
||||
email = params[:email]
|
||||
user = User.find_by(email: email)
|
||||
return if user.blank?
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
user.profile.destroy!
|
||||
user.checkins.or({:postal_code.ne => nil}, :position_id.ne => nil)
|
||||
&.update_all(postal_code: nil, position_id: nil)
|
||||
|
||||
user.destroy!
|
||||
|
||||
Feedback.create!(email: email, delete_reason: params[:delete_reason])
|
||||
end
|
||||
end
|
||||
|
||||
# Use AR's i18n scope so that we can raise RecordInvalid exceptions
|
||||
def self.i18n_scope
|
||||
:activerecord
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def permitted(params)
|
||||
params.require(:registration).permit(
|
||||
:email, :password, :password_confirmation, :screen_name, :captcha_response, :birth_date
|
||||
)
|
||||
end
|
||||
|
||||
def captcha_response_verified
|
||||
verified = ::Google::RecaptchaVerifier.exec(captcha_response)
|
||||
errors.add(:captcha_response, "verification failed or response expired") unless verified
|
||||
end
|
||||
end
|
||||
71
worker-toolkit-flaredown/repo/backend/app/models/search.rb
Normal file
71
worker-toolkit-flaredown/repo/backend/app/models/search.rb
Normal file
@@ -0,0 +1,71 @@
|
||||
class Search
|
||||
include ActiveRecord::Sanitization::ClassMethods
|
||||
include ActiveModel::Serialization
|
||||
include ActiveModel::Model
|
||||
|
||||
attr_accessor :resource, :scope, :user, :query
|
||||
|
||||
#
|
||||
# Validations
|
||||
#
|
||||
validates :resource, presence: true, inclusion: {in: %w[treatment symptom condition tag dose food topic]}
|
||||
|
||||
def id
|
||||
Digest::SHA256.base64digest(resource)
|
||||
end
|
||||
|
||||
def searchables
|
||||
# FIXME
|
||||
# rubocop:disable Rails/DynamicFindBy
|
||||
@searchables ||= find_by_query
|
||||
# rubocop:enable Rails/DynamicFindBy
|
||||
end
|
||||
|
||||
MAX_ROWS = 10
|
||||
|
||||
protected
|
||||
|
||||
def query_hash
|
||||
@query_hash ||= Hash(query)
|
||||
end
|
||||
|
||||
def find_by_query
|
||||
if query_hash.present?
|
||||
resources.where(*where_conditions).limit(MAX_ROWS)
|
||||
elsif scope.present?
|
||||
case scope
|
||||
when "random"
|
||||
resources.limit(MAX_ROWS).order("RANDOM()")
|
||||
end
|
||||
else
|
||||
resources.limit(MAX_ROWS)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def where_conditions
|
||||
@where_conditions ||=
|
||||
[].tap do |conditions|
|
||||
query_hash.each do |key, value|
|
||||
sanitized_value = sanitize_sql_like(value.squish.downcase)
|
||||
|
||||
pattern =
|
||||
if sanitized_value.length < 3
|
||||
"#{sanitized_value}%"
|
||||
else
|
||||
"%#{sanitized_value}%"
|
||||
end
|
||||
conditions << ["lower(#{key}) LIKE ?", pattern]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def resources
|
||||
resource.classify.constantize.accessible_by(current_ability).with_translations(I18n.locale)
|
||||
end
|
||||
|
||||
def current_ability
|
||||
@current_ability ||= Ability.new(user)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,35 @@
|
||||
class Search::ForDose < ::Search
|
||||
validates :query, presence: true
|
||||
|
||||
validate :query_must_include_treatment_id
|
||||
def query_must_include_treatment_id
|
||||
return if query[:treatment_id]
|
||||
|
||||
errors.add(:query, "must include treatment_id")
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def find_by_query
|
||||
q = name.present? ? treatments.where(value: /#{name}/i) : treatments
|
||||
q = q.order_by("checkin.date" => "desc").distinct(:value)
|
||||
q.slice(0..10).map { |value| ::Dose.new(name: value) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def treatments
|
||||
Checkin::Treatment.where(
|
||||
:value.ne => nil, :is_taken => true, :treatment_id => treatment_id
|
||||
)
|
||||
end
|
||||
|
||||
def treatment_id
|
||||
query[:treatment_id].to_i
|
||||
end
|
||||
|
||||
def name
|
||||
text = query[:name]
|
||||
Regexp.escape(text) if text
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
class Search::ForFood < ::Search
|
||||
validates :query, presence: true
|
||||
|
||||
protected
|
||||
|
||||
def find_by_query
|
||||
Food.fts(query[:name], MAX_ROWS, user.id)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
class Search::ForTopic < ::Search
|
||||
alias_method :super_query, :find_by_query
|
||||
|
||||
protected
|
||||
|
||||
def find_by_query
|
||||
(tags + symptoms + conditions + treatments).sort { |a, b| a.name.underscore <=> b.name.underscore }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
%w[tag symptom condition treatment].each do |trackable|
|
||||
define_method trackable.pluralize do
|
||||
@resource = trackable
|
||||
|
||||
super_query
|
||||
end
|
||||
end
|
||||
end
|
||||
7
worker-toolkit-flaredown/repo/backend/app/models/sex.rb
Normal file
7
worker-toolkit-flaredown/repo/backend/app/models/sex.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
class Sex < RankedEnum
|
||||
class << self
|
||||
def all_ids
|
||||
%w[male female other doesnt_say]
|
||||
end
|
||||
end
|
||||
end
|
||||
19
worker-toolkit-flaredown/repo/backend/app/models/symptom.rb
Normal file
19
worker-toolkit-flaredown/repo/backend/app/models/symptom.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: symptoms
|
||||
#
|
||||
# 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 Symptom < ActiveRecord::Base
|
||||
has_many :symptom_translations, class_name: "Symptom::Translation"
|
||||
|
||||
#
|
||||
# Localized attributes
|
||||
#
|
||||
translates :name
|
||||
end
|
||||
17
worker-toolkit-flaredown/repo/backend/app/models/tag.rb
Normal file
17
worker-toolkit-flaredown/repo/backend/app/models/tag.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: tags
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class Tag < ActiveRecord::Base
|
||||
has_many :tag_translations, class_name: "Tag::Translation"
|
||||
|
||||
#
|
||||
# Localized attributes
|
||||
#
|
||||
translates :name
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
class TopicFollowing
|
||||
include Topicable
|
||||
|
||||
def add_topic(key, value)
|
||||
return if send(key).include?(value)
|
||||
|
||||
send(key) << value
|
||||
|
||||
save
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: trackable_usages
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# user_id :integer
|
||||
# trackable_id :integer
|
||||
# trackable_type :string
|
||||
# count :integer default(1)
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class TrackableUsage < ActiveRecord::Base
|
||||
#
|
||||
# Associations
|
||||
#
|
||||
|
||||
belongs_to :user
|
||||
belongs_to :trackable, polymorphic: true, counter_cache: true
|
||||
|
||||
#
|
||||
# Validations
|
||||
#
|
||||
validates :count, numericality: {greater_than: 0}
|
||||
|
||||
#
|
||||
# Callbacks
|
||||
#
|
||||
after_commit ->(obj) { SwitchTrackableVisibility.perform_later(obj.id) }
|
||||
#
|
||||
# Class Methods
|
||||
#
|
||||
|
||||
class << self
|
||||
def create_or_update_by(user: nil, trackable: nil)
|
||||
trackable_usage = find_by(user: user, trackable: trackable)
|
||||
if trackable_usage.present?
|
||||
trackable_usage.increment! :count
|
||||
trackable_usage
|
||||
else
|
||||
create!(user: user, trackable: trackable)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
63
worker-toolkit-flaredown/repo/backend/app/models/tracking.rb
Normal file
63
worker-toolkit-flaredown/repo/backend/app/models/tracking.rb
Normal file
@@ -0,0 +1,63 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: trackings
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# user_id :integer
|
||||
# trackable_id :integer
|
||||
# trackable_type :string
|
||||
# start_at :date
|
||||
# end_at :date
|
||||
# color_id :integer
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class Tracking < ActiveRecord::Base
|
||||
#
|
||||
# Associations
|
||||
#
|
||||
belongs_to :user
|
||||
belongs_to :trackable, polymorphic: true
|
||||
|
||||
#
|
||||
# Validations
|
||||
#
|
||||
|
||||
TYPES = %w[Condition Symptom Treatment].freeze
|
||||
|
||||
validates :user, :trackable, :start_at, presence: true
|
||||
|
||||
validates :trackable_type, inclusion: {
|
||||
in: TYPES
|
||||
}
|
||||
|
||||
validates :user_id, uniqueness: {
|
||||
scope: %i[trackable_id trackable_type start_at], message: "is already tracking this trackable"
|
||||
}
|
||||
|
||||
#
|
||||
# Scopes
|
||||
#
|
||||
scope :active_at, ->(at) { where("start_at <= :at AND (end_at >= :at OR end_at IS NULL)", at: at.strftime("%F")) }
|
||||
scope :by_trackable_type, ->(trackable_type) { where(trackable_type: trackable_type) }
|
||||
|
||||
#
|
||||
# Callbacks
|
||||
#
|
||||
before_create :ensure_color_id
|
||||
|
||||
def self.active_in_range(range_start, range_end)
|
||||
where(
|
||||
"(start_at <= :range_start OR start_at <= :range_end) AND (end_at > :range_start OR end_at IS NULL)",
|
||||
range_start: range_start.strftime("%F"),
|
||||
range_end: range_end.strftime("%F")
|
||||
)
|
||||
end
|
||||
|
||||
def ensure_color_id
|
||||
return if color_id
|
||||
|
||||
self.color_id = Flaredown::Colorable.color_id_for(trackable, user)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: treatments
|
||||
#
|
||||
# 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 Treatment < ActiveRecord::Base
|
||||
has_many :treatment_translations, class_name: "Treatment::Translation"
|
||||
|
||||
#
|
||||
# Localized attributes
|
||||
#
|
||||
translates :name
|
||||
end
|
||||
133
worker-toolkit-flaredown/repo/backend/app/models/user.rb
Normal file
133
worker-toolkit-flaredown/repo/backend/app/models/user.rb
Normal file
@@ -0,0 +1,133 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: users
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# email :string default(""), not null
|
||||
# encrypted_password :string default(""), not null
|
||||
# reset_password_token :string
|
||||
# reset_password_sent_at :datetime
|
||||
# remember_created_at :datetime
|
||||
# sign_in_count :integer default(0), not null
|
||||
# current_sign_in_at :datetime
|
||||
# last_sign_in_at :datetime
|
||||
# current_sign_in_ip :string
|
||||
# last_sign_in_ip :string
|
||||
# authentication_token :string not null
|
||||
# invitation_token :string
|
||||
# invitation_created_at :datetime
|
||||
# invitation_sent_at :datetime
|
||||
# invitation_accepted_at :datetime
|
||||
# invitation_limit :integer
|
||||
# invited_by_id :integer
|
||||
# invited_by_type :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class User < ActiveRecord::Base
|
||||
include Authenticatable
|
||||
|
||||
#
|
||||
# Associations
|
||||
#
|
||||
|
||||
has_one :profile, dependent: :destroy
|
||||
|
||||
has_many :user_conditions, dependent: :destroy
|
||||
has_many :conditions, through: :user_conditions
|
||||
|
||||
has_many :user_symptoms, dependent: :destroy
|
||||
has_many :symptoms, through: :user_symptoms
|
||||
|
||||
has_many :user_treatments, dependent: :destroy
|
||||
has_many :treatments, through: :user_treatments
|
||||
|
||||
has_many :user_tags, dependent: :destroy
|
||||
has_many :tags, through: :user_tags
|
||||
|
||||
has_many :user_foods, dependent: :destroy
|
||||
has_many :foods, through: :user_foods
|
||||
|
||||
has_many :trackings, dependent: :destroy
|
||||
|
||||
has_many :trackable_usages, dependent: :destroy
|
||||
|
||||
#
|
||||
# Callbacks
|
||||
#
|
||||
before_create :generate_authentication_token
|
||||
after_create :init_profile
|
||||
|
||||
#
|
||||
# Delegates
|
||||
#
|
||||
delegate :locale, :notify, :notify_token, :screen_name, :rejected_type, to: :profile
|
||||
|
||||
def checkins
|
||||
Checkin.where(encrypted_user_id: encrypted_id)
|
||||
end
|
||||
|
||||
def patterns
|
||||
Pattern.where(encrypted_user_id: encrypted_id)
|
||||
end
|
||||
|
||||
def encrypted_id
|
||||
@_encrypted_id ||= SymmetricEncryption.encrypt(id)
|
||||
end
|
||||
|
||||
def last_checkin
|
||||
checkins.order_by(date: :desc).first
|
||||
end
|
||||
|
||||
def checkin_ids
|
||||
checkins.map(&:id)
|
||||
end
|
||||
|
||||
def notification_channel
|
||||
"private-#{id}"
|
||||
end
|
||||
|
||||
def push(event, message)
|
||||
Flaredown.pusher.push(notification_channel, event, message)
|
||||
end
|
||||
|
||||
def external_id
|
||||
"#{id}-#{Digest::SHA1.hexdigest(authentication_token).strip}"
|
||||
end
|
||||
|
||||
def topic_following
|
||||
TopicFollowing.find_or_create_by(encrypted_user_id: encrypted_id) do |tf|
|
||||
checkin = last_checkin
|
||||
|
||||
next if checkin.blank?
|
||||
|
||||
tf.symptom_ids = Checkin::Symptom.where(checkin_id: checkin.id).distinct(:symptom_id)
|
||||
tf.condition_ids = Checkin::Condition.where(checkin_id: checkin.id).distinct(:condition_id)
|
||||
tf.treatment_ids = Checkin::Treatment.where(checkin_id: checkin.id).distinct(:treatment_id)
|
||||
|
||||
tf.tag_ids = checkin.tag_ids || []
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_authentication_token
|
||||
self.authentication_token =
|
||||
loop do
|
||||
random_token = SecureRandom.hex
|
||||
break random_token unless User.exists?(authentication_token: random_token)
|
||||
end
|
||||
end
|
||||
|
||||
def init_profile
|
||||
create_profile!(
|
||||
checkin_reminder: true,
|
||||
onboarding_step_id: "onboarding-personal",
|
||||
most_recent_doses: {},
|
||||
most_recent_conditions_positions: {},
|
||||
most_recent_symptoms_positions: {},
|
||||
most_recent_treatments_positions: {}
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: user_conditions
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# user_id :integer
|
||||
# condition_id :integer
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class UserCondition < ActiveRecord::Base
|
||||
#
|
||||
# Associations
|
||||
#
|
||||
belongs_to :user
|
||||
belongs_to :condition
|
||||
|
||||
#
|
||||
# Validations
|
||||
#
|
||||
validates :user, :condition, presence: true
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
class UserFood < ActiveRecord::Base
|
||||
#
|
||||
# Associations
|
||||
#
|
||||
belongs_to :user
|
||||
belongs_to :food
|
||||
|
||||
#
|
||||
# Validations
|
||||
#
|
||||
validates :user, :food, presence: true
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: user_symptoms
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# user_id :integer
|
||||
# symptom_id :integer
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class UserSymptom < ActiveRecord::Base
|
||||
#
|
||||
# Associations
|
||||
#
|
||||
belongs_to :user
|
||||
belongs_to :symptom
|
||||
|
||||
#
|
||||
# Validations
|
||||
#
|
||||
validates :user, :symptom, presence: true
|
||||
end
|
||||
12
worker-toolkit-flaredown/repo/backend/app/models/user_tag.rb
Normal file
12
worker-toolkit-flaredown/repo/backend/app/models/user_tag.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
class UserTag < ActiveRecord::Base
|
||||
#
|
||||
# Associations
|
||||
#
|
||||
belongs_to :user
|
||||
belongs_to :tag
|
||||
|
||||
#
|
||||
# Validations
|
||||
#
|
||||
validates :user, :tag, presence: true
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: user_treatments
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# user_id :integer
|
||||
# treatment_id :integer
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class UserTreatment < ActiveRecord::Base
|
||||
#
|
||||
# Associations
|
||||
#
|
||||
belongs_to :user
|
||||
belongs_to :treatment
|
||||
|
||||
#
|
||||
# Validations
|
||||
#
|
||||
validates :user, :treatment, presence: true
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
class Weather < ActiveRecord::Base
|
||||
validates :date, uniqueness: {scope: :position_id}
|
||||
# NOTE: if we want to enforce presence, modify spec factory instead
|
||||
belongs_to :position, optional: true
|
||||
end
|
||||
Reference in New Issue
Block a user