You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

188 lines
5.8 KiB

class Node < ApplicationRecord
MENUS = %i""
COOKIE_POLICY = :cookie_policy
SETTINGS = MENUS << COOKIE_POLICY
include AncestryWithSortedUrl
include HasAttachments
include HasTags
has_many :answers, dependent: :destroy
extend Mobility
translates :slug,
:tags,
locale_accessors: I18n.available_locales,
fallbacks: {zh: :en, hr: :en, cs: :en, da: :en, nl: :en, fi: :en, fr: :en, fr_ca: :en, de: :en, hu: :en, it: :en, ja: :en, ko: :en, nb: :en, pl: :en, pt: :en, ro: :en, sr: :en, sk: :en, sl: :en, es: :en, sv: :en, uk: :en}
translates :title,
:url,
:href,
:page_title,
:page_description,
locale_accessors: I18n.available_locales,
fallbacks: {zh: :en, hr: :en, cs: :en, da: :en, nl: :en, fi: :en, fr: :en, fr_ca: :en, de: :en, hu: :en, it: :en, ja: :en, ko: :en, nb: :en, pl: :en, pt: :en, ro: :en, sr: :en, sk: :en, sl: :en, es: :en, sv: :en, uk: :en}
NODE_TEMPLATES = %w"tmpl_article tmpl_index tmpl_list"
enum :status, { status_published: 0, status_draft: 1, status_archived: 2 }
enum :template, { tmpl_index: 0,
tmpl_article: 1,
tmpl_list: 2
}
include PgSearch::Model
pg_search_scope :pg_search,
against: {title: 'A', url: 'B', page_title: 'A', page_description: 'B', href: 'B', slug: 'B' },
associated_against: {
attachments: [:body, :url]
}
before_validation :remove_empty_tags
validates_presence_of :title
validates :expires_at,
date: { after: :published_at },
allow_nil: true
scope :ordered, -> { order(position: :asc) }
scope :by_title, ->(rev) { order(Arel.sql(rev ? "title->>'#{I18n.locale.to_s}' DESC, id DESC": "title ->>'#{I18n.locale.to_s}' ASC, id ASC")) }
scope :by_status, ->(rev) { order(status: rev ? :desc : :asc, id: rev ? :desc : :asc) }
scope :by_slug, ->(rev) { order(ancestry: rev ? :desc : :asc, position: rev ? :desc : :asc, id: rev ? :desc : :asc) }
scope :by_last_modified, ->(rev) { order(updated_at: rev ? :asc : :desc, id: rev ? :desc : :asc) }
# scope :not_excluded, -> { where "NOT(? = ANY (excluded_locales))", I18n.locale.to_s }
scope :for_current_locale, -> { where("(cardinality(excluded_locales) = 0 AND is_allowlist = false) OR
(is_allowlist = true AND ? = ANY(excluded_locales)) OR
(is_allowlist = false AND NOT(? = ANY(excluded_locales)))",
I18n.locale.to_s, I18n.locale.to_s ) }
scope :viewable, -> { status_published.for_current_locale.where('published_at <= ? AND (expires_at IS NULL OR expires_at > ?)', Time.current, Time.current) }
scope :of_template, ->(tmpl) { where(template: Node.templates[tmpl.to_s]) }
scope :with_setting, ->(setting) { setting.kind_of?(Array) ? where("settings && ?", "{#{setting.join(',')}}") : where("settings @> ?", "{#{setting}}") }
scope :simple_search, ->(q) { pg_search(q) unless q.blank? }
scope :tiles, -> { where("ancestry LIKE '/?/%'", Node.where(position: 2, ancestry_depth: 0).first) }
def self.tags
result = {}
I18n.available_locales.each do |locale|
key = locale.to_s.downcase.sub('-','_')
result[key] = pluck(Arel.sql("tags->'#{locale}'")).flatten.compact.uniq.sort.reject{|v| v.blank?}
end
result
end
def self.categories
['document']
end
def category
self.children.any? ? 'folder' : 'document'
end
def document?
self.category == 'document'
end
def href_or_url
href.present?? href : url
end
def viewable?
Node.viewable.where(id: self.path_ids).count == self.path_ids.length
end
def index?
self.attachments.blank?
end
def site?
(self.root || self).position == 1
end
def tile?
(self.root || self).position == 2
end
def node_type
return "site" if self.site?
'tile'
end
def answer_percentages
# Get all valid answers (non-nil values)
valid_answers = answers.where.not(value: nil)
# Count answers by value
value_counts = valid_answers.group(:value).count
# Calculate total number of answers
total_answers = valid_answers.count
# Initialize with 0 for both expected values
percentages = {0 => 0, 1 => 0}
# Return initial percentages if no answers
return percentages if total_answers.zero?
# Calculate exact percentages first (not rounded)
exact_percentages = {}
value_counts.each do |value, count|
# Only consider values 0 and 1
next unless [0, 1].include?(value)
exact_percentages[value] = count.to_f / total_answers * 100
end
# First round down all percentages
[0, 1].each do |value|
percentages[value] = (exact_percentages[value] || 0).floor
end
# Calculate how much we're off by due to rounding
remaining = 100 - percentages.values.sum
# Distribute the remaining percentage points to values with the largest fractional parts
if remaining > 0
# Filter exact percentages to only include 0 and 1
sorted_by_fraction = exact_percentages
.select { |k, _| [0, 1].include?(k) }
.sort_by { |_, p| p - p.floor }
.reverse
# Add 1 to the values with the largest fractional parts
sorted_by_fraction.take(remaining).each do |value, _|
percentages[value] += 1
end
end
# Return the percentages hash with keys 0 and 1
percentages
end
private
def remove_empty_tags
%w"settings excluded_locales tags_en tags_zh tags_hr tags_cs tags_da tags_nl tags_fi tags_fr tags_fr_ca tags_de tags_hu tags_it tags_ja tags_ko tags_nb tags_pl tags_pt tags_ro tags_sr tags_sk tags_sl tags_es tags_sv tags_uk ".map do |k|
self.send "#{k}=", self.send(k).reject { |v| v.blank? } unless self.send(k).blank?
end
end
end