summaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/concerns/nutrient_vector.rb55
-rw-r--r--app/models/nutrient.rb4
-rw-r--r--app/models/nutrient_measurement.rb12
-rw-r--r--app/models/target.rb67
-rw-r--r--app/models/target_allocation.rb3
5 files changed, 106 insertions, 35 deletions
diff --git a/app/models/concerns/nutrient_vector.rb b/app/models/concerns/nutrient_vector.rb
new file mode 100644
index 0000000..ba4f28a
--- /dev/null
+++ b/app/models/concerns/nutrient_vector.rb
@@ -0,0 +1,55 @@
+module NutrientVector
+ extend ActiveSupport::Concern
+
+ NUTRIENT_KEYS = %i[
+ nno3 p k ca mg s na cl si fe zn b mn cu mo nnh4
+ ].freeze
+
+ included do
+ # Define simple readers (nno3, p, k, ...) that read from #nutrient_values
+ NUTRIENT_KEYS.each do |k|
+ define_method(k) { nutrient_values[k] }
+ end
+ end
+
+ # Hash-like access
+ def [](key)
+ key = key.to_sym
+ return nil unless NUTRIENT_KEYS.include?(key)
+ public_send(key)
+ end
+
+ def keys = NUTRIENT_KEYS
+
+ # Returns a copy to avoid accidental mutation
+ def to_h
+ NUTRIENT_KEYS.index_with { |k| public_send(k) }
+ end
+
+ # Iterate over pairs
+ def each_pair
+ return enum_for(:each_pair) unless block_given?
+ NUTRIENT_KEYS.each { |k| yield k, public_send(k) }
+ end
+
+ # Simple difference (self - other), useful for “how far from target?”
+ def delta_against(other)
+ NUTRIENT_KEYS.index_with { |k| (public_send(k).to_f) - (other.public_send(k).to_f) }
+ end
+
+ # Percent difference relative to other (e.g., measurement vs target)
+ # Returns 0 when both are 0, and nil when target is 0 but measurement isn’t.
+ def percent_diff_against(other)
+ NUTRIENT_KEYS.index_with do |k|
+ a = public_send(k).to_f
+ b = other.public_send(k).to_f
+ if b.zero? && a.zero?
+ 0.0
+ elsif b.zero?
+ nil
+ else
+ ((a - b) / b) * 100.0
+ end
+ end
+ end
+end
diff --git a/app/models/nutrient.rb b/app/models/nutrient.rb
new file mode 100644
index 0000000..0840086
--- /dev/null
+++ b/app/models/nutrient.rb
@@ -0,0 +1,4 @@
+class Nutrient < ApplicationRecord
+ validates :formula, presence: true, uniqueness: true
+ validates :name, presence: true
+end
diff --git a/app/models/nutrient_measurement.rb b/app/models/nutrient_measurement.rb
index 1139af7..b56c5ed 100644
--- a/app/models/nutrient_measurement.rb
+++ b/app/models/nutrient_measurement.rb
@@ -1,7 +1,5 @@
class NutrientMeasurement < ApplicationRecord
- NUTRIENT_FIELDS = %i[
- nno3 p k ca mg s na cl si fe zn b mn cu mo nnh4
- ].freeze
+ include NutrientVector
validates :measured_on, presence: true
validates :measured_on, uniqueness: true
@@ -12,7 +10,11 @@ class NutrientMeasurement < ApplicationRecord
end
end
- def self.nutrient_fields
- NUTRIENT_FIELDS
+ private
+
+ def nutrient_values
+ @nutrient_values ||= NutrientVector::NUTRIENT_KEYS.index_with do |k|
+ read_attribute(k).to_f
+ end
end
end
diff --git a/app/models/target.rb b/app/models/target.rb
index 9211e74..2efd3b2 100644
--- a/app/models/target.rb
+++ b/app/models/target.rb
@@ -1,42 +1,53 @@
-# app/models/target.rb
class Target < ApplicationRecord
- has_many :target_allocations, dependent: :destroy
- has_many :nutrient_profiles, through: :target_allocations
+ include NutrientVector
- accepts_nested_attributes_for :target_allocations, allow_destroy: true
- # validate :percentages_sum_to_100
+ has_many :target_allocations, inverse_of: :target, dependent: :destroy
- def weighted_requirements
- totals = Hash.new(0.0)
- denom = 100.0
+ after_initialize :set_defaults, if: :new_record?
- target_allocations.includes(:nutrient_profile).each do |alloc|
- profile = alloc.nutrient_profile
- next unless profile
+ validate :allocations_sum_to_100
- weight = (alloc.percentage || 0).to_f / denom
- next if weight <= 0
+ # Recompute when allocations change; keep it simple (memoized per instance)
+ def recompute_nutrients!
+ @nutrient_values = nil
+ nutrient_values
+ end
- # Prefer the helper, but gracefully fall back to slicing attributes.
- reqs = if profile.respond_to?(:requirements_hash)
- profile.requirements_hash
- else
- profile.attributes.slice(*NutrientProfile::NUTRIENT_KEYS.map(&:to_s))
- end
+ private
+
+ def nutrient_values
+ @nutrient_values ||= get_nutrient_values
+ end
+
+ def get_nutrient_values
+ sums = Hash.new(0.0)
+ allocs = target_allocations.includes(:nutrient_profile)
- reqs.each do |nutrient_key, value|
- next if value.nil?
- totals[nutrient_key.to_s] += value.to_f * weight
+ allocs.each do |alloc|
+ weight = alloc.percentage.to_f / 100.0
+ profile = alloc.nutrient_profile
+ NutrientVector::NUTRIENT_KEYS.each do |k|
+ sums[k] += profile.public_send(k).to_f * weight
end
end
- totals
+ # Ensure all keys exist, even when there are no allocations
+ NutrientVector::NUTRIENT_KEYS.each { |k| sums[k] ||= 0.0 }
+ sums.freeze
end
- private
+ def set_defaults
+ self.name ||= "Cible #{Date.today + 1.month}"
+ if target_allocations.empty?
+ nutrient_profiles = NutrientProfile.limit(3)
+ nutrient_profiles.each do |profile|
+ target_allocations.build(nutrient_profile: profile, percentage: 0)
+ end
+ end
+ end
- # def percentages_sum_to_100
- # sum = target_allocations.sum { |a| a.percentage.to_f }
- # errors.add(:base, "La somme des pourcentages doit être égale à 100%") unless (sum - 100.0).abs <= 0.1
- # end
+ def allocations_sum_to_100
+ sum = target_allocations.reject(&:marked_for_destruction?).sum { |a| a.percentage.to_f }
+ errors.add(:base, "La somme des pourcentages doit être 100%") unless (sum - 100).abs < 0.01
+ end
end
diff --git a/app/models/target_allocation.rb b/app/models/target_allocation.rb
index 6aa1dcb..5e971a0 100644
--- a/app/models/target_allocation.rb
+++ b/app/models/target_allocation.rb
@@ -1,7 +1,6 @@
class TargetAllocation < ApplicationRecord
- belongs_to :target
+ belongs_to :target, inverse_of: :target_allocations, touch: true
belongs_to :nutrient_profile
validates :percentage, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }
- validates :nutrient_profile_id, uniqueness: { scope: :target_id }
end
Copyright 2019--2026 Marius PETER