diff --git a/app/controllers/body_trackers_controller.rb b/app/controllers/body_trackers_controller.rb index 51c8c32..3ea751b 100644 --- a/app/controllers/body_trackers_controller.rb +++ b/app/controllers/body_trackers_controller.rb @@ -13,7 +13,7 @@ class BodyTrackersController < ApplicationController # Units available_units = @project.units.pluck(:shortname, :id).to_h - defaults = Unit.where(project: nil).map do |u| + defaults = Unit.defaults.map do |u| u.attributes.except('id', 'project_id', 'created_at', 'updated_at') end defaults.delete_if { |u| available_units.has_key?(u['shortname']) } @@ -26,7 +26,7 @@ class BodyTrackersController < ApplicationController # Quantities available_quantities = Quantity.each_with_path(@project.quantities).map(&:rotate).to_h quantities_count = available_quantities.length - defaults = Quantity.where(project: nil) + defaults = Quantity.defaults Quantity.each_with_path(defaults) do |q, path| unless available_quantities.has_key?(path) attrs = q.attributes.except('id', 'project_id', 'parent_id', 'lft', 'rgt', @@ -56,7 +56,7 @@ class BodyTrackersController < ApplicationController # Sources available_sources = @project.sources.pluck(:name, :id).to_h - defaults = Source.where(project: nil).map do |s| + defaults = Source.defaults.map do |s| s.attributes.except('id', 'project_id', 'created_at', 'updated_at') end defaults.delete_if { |s| available_sources.has_key?(s['name']) } diff --git a/app/models/formula.rb b/app/models/formula.rb index 179d7cd..edce86b 100644 --- a/app/models/formula.rb +++ b/app/models/formula.rb @@ -6,6 +6,8 @@ class Formula < ActiveRecord::Base belongs_to :quantity, inverse_of: :formula, required: true belongs_to :unit + scope :defaults, -> { includes(:quantity).where(quantities: {project: nil}) } + validates :code, presence: true validate do messages = parse.each { |message, params| errors.add(:code, message, params) } @@ -92,7 +94,7 @@ class Formula < ActiveRecord::Base # e.g. during import of defaults (so impossible to use recursive sql instead) q_names = identifiers.map { |i| i.split('::').last } q_paths = {} - (quantity.project.try(&:quantities) || Quantity.where(project: nil)) + (quantity.project.try(&:quantities) || Quantity.defaults) .select { |q| q_names.include?(q.name) }.each do |q| # NOTE: after upgrade to Ruby 2.7 replace with Enumerator#produce diff --git a/app/models/quantity.rb b/app/models/quantity.rb index 4d5fb66..fa306d7 100644 --- a/app/models/quantity.rb +++ b/app/models/quantity.rb @@ -13,6 +13,8 @@ class Quantity < ActiveRecord::Base has_many :values, class_name: 'QuantityValue', dependent: :restrict_with_error has_many :exposures, dependent: :destroy + scope :defaults, -> { where(project: nil) } + has_one :formula, inverse_of: :quantity, dependent: :destroy, validate: true accepts_nested_attributes_for :formula, allow_destroy: true, reject_if: proc { |attrs| attrs['id'].blank? && attrs['code'].blank? } diff --git a/app/models/source.rb b/app/models/source.rb index daa45ee..9a48143 100644 --- a/app/models/source.rb +++ b/app/models/source.rb @@ -1,6 +1,8 @@ class Source < ActiveRecord::Base belongs_to :project, required: false + scope :defaults, -> { where(project: nil) } + validates :name, presence: true, uniqueness: {scope: :project_id} # Has to go before any 'dependent:' association diff --git a/app/models/unit.rb b/app/models/unit.rb index 93a39e5..4f1467c 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -1,6 +1,8 @@ class Unit < ActiveRecord::Base belongs_to :project, required: false + scope :defaults, -> { where(project: nil) } + validates :shortname, presence: true, uniqueness: {scope: :project_id} # Has to go before any 'dependent:' association diff --git a/db/seeds.rb b/db/seeds.rb new file mode 100644 index 0000000..81a67d5 --- /dev/null +++ b/db/seeds.rb @@ -0,0 +1,194 @@ +# Formulas will be deleted as dependent on Quantities +[Source, Quantity, Unit].each { |model| model.defaults.delete_all } + +# Units +u_a = Unit.create shortname: "g", name: "gram" +u_aa = Unit.create shortname: "ug", name: "microgram" +u_ab = Unit.create shortname: "mg", name: "milligram" +u_ac = Unit.create shortname: "kg", name: "kilogram" +u_b = Unit.create shortname: "kcal", name: "kilocalorie" +u_c = Unit.create shortname: "%", name: "percent" + +# Quantities +# https://www.fsai.ie/uploadedFiles/Consol_Reg1169_2011.pdf +# https://www.fsai.ie/legislation/food_legislation/food_information_fic/nutrition_labelling.html +# -> Energy +e_a = Quantity.create name: "Energy", domain: :diet, parent: nil, + description: "Total energy" +e_aa = Quantity.create name: "calculated", domain: :diet, parent: e_a, + description: "Total energy calculated from macronutrients" +e_ab = Quantity.create name: "as %RM", domain: :diet, parent: e_a, + description: "Total energy percent value relative to current" \ + " resting metabolism" +e_ac = Quantity.create name: "proteins", domain: :diet, parent: e_a, + description: "Calculated proteins energy" +e_aca = Quantity.create name: "as %RM", domain: :diet, parent: e_ac, + description: "" +e_ad = Quantity.create name: "fats", domain: :diet, parent: e_a, + description: "Calculated fats energy" +e_ada = Quantity.create name: "as %RM", domain: :diet, parent: e_ad, + description: "" +e_ae = Quantity.create name: "carbs", domain: :diet, parent: e_a, + description: "Calculated carbs energy" +e_aea = Quantity.create name: "as %RM", domain: :diet, parent: e_ae, + description: "" + +# -> Proteins +p_a = Quantity.create name: "Proteins", domain: :diet, parent: nil, + description: "Total amount of proteins" + +# -> Fats +f_a = Quantity.create name: "Fats", domain: :diet, parent: nil, + description: "Total lipids, including phospholipids" +f_aa = Quantity.create name: "Fatty acids", domain: :diet, parent: f_a, + description: "" +f_aaa = Quantity.create name: "Saturated", domain: :diet, parent: f_aa, + description: "Fatty acids without double bond" +f_aab = Quantity.create name: "Unsaturated", domain: :diet, parent: f_aa, + description: "" +f_aaba = Quantity.create name: "Monounsaturated", domain: :diet, parent: f_aab, + description: "Fatty acids with one cis double bond" +f_aabb = Quantity.create name: "Polyunsaturated", domain: :diet, parent: f_aab, + description: "Fatty acids with two or more cis, cis-methylene" \ + " interrupted double bonds; PUFA" +f_aabba = Quantity.create name: "Omega-3 (n-3)", domain: :diet, parent: f_aabb, + description: "" +f_aabbaa = Quantity.create name: "ALA 18:3(n-3)", domain: :diet, parent: f_aabba, + description: "alpha-Linolenic acid" +f_aabbab = Quantity.create name: "EPA 20:5(n-3)", domain: :diet, parent: f_aabba, + description: "Eicosapentaenoic acid; also icosapentaenoic acid" +f_aabbac = Quantity.create name: "DHA 22:6(n-3)", domain: :diet, parent: f_aabba, + description: "Docosahexaenoic acid" +f_aabbb = Quantity.create name: "Omega-6 (n-6)", domain: :diet, parent: f_aabb, + description: "" +f_aabc = Quantity.create name: "Trans", domain: :diet, parent: f_aab, + description: "Fatty acids with at least one non-conjugated C-C" \ + " double bond in the trans configuration" + +# -> Carbs +c_a = Quantity.create name: "Carbs", domain: :diet, parent: nil, + description: "Total amount of carbohydrates" +c_aa = Quantity.create name: "Digestible", domain: :diet, parent: c_a, + description: "" +c_aaa = Quantity.create name: "Sugars", domain: :diet, parent: c_aa, + description: "Monosaccharides and disaccharides, excluding" \ + " polyols" +c_aaaa = Quantity.create name: "Monosaccharides", domain: :diet, parent: c_aaa, + description: "" +c_aaaaa = Quantity.create name: "Glucose", domain: :diet, parent: c_aaaa, + description: "" +c_aaaab = Quantity.create name: "Fructose", domain: :diet, parent: c_aaaa, + description: "" +c_aaab = Quantity.create name: "Disaccharides", domain: :diet, parent: c_aaa, + description: "" +c_aaaba = Quantity.create name: "Sucrose", domain: :diet, parent: c_aaab, + description: "" +c_aaabb = Quantity.create name: "Lactose", domain: :diet, parent: c_aaab, + description: "" +c_aab = Quantity.create name: "Polyols", domain: :diet, parent: c_aa, + description: "Alcohols containing more than 2 hydroxyl groups" +c_aac = Quantity.create name: "Polysaccharides", domain: :diet, parent: c_aa, + description: "" +c_aaca = Quantity.create name: "Starch", domain: :diet, parent: c_aac, + description: "" +c_ab = Quantity.create name: "Indigestible", domain: :diet, parent: c_a, + description: "" +c_aba = Quantity.create name: "Fibre", domain: :diet, parent: c_ab, + description: "Carbohydrate polymers with 3 or more monomeric" \ + " units, which are neither digested nor absorbed" \ + " in the human small intestine" + +# -> Minerals +m_a = Quantity.create name: "Minerals", domain: :diet, parent: nil, + description: "" +m_aa = Quantity.create name: "Salt", domain: :diet, parent: m_a, + description: "Sodium chloride" + +# -> Vitamins +v_a = Quantity.create name: "Vitamins", domain: :diet, parent: nil, + description: "" +v_aa = Quantity.create name: "Vitamin A", domain: :diet, parent: v_a, + description: "" +v_aaa = Quantity.create name: "Retinol (A1)", domain: :diet, parent: v_aa, + description: "" +v_ab = Quantity.create name: "Provitamin A", domain: :diet, parent: v_a, + description: "" +v_aba = Quantity.create name: "beta-Carotene", domain: :diet, parent: v_ab, + description: "" +v_ac = Quantity.create name: "Vitamin B", domain: :diet, parent: v_a, + description: "" +v_aca = Quantity.create name: "Thiamine (B1)", domain: :diet, parent: v_ac, + description: "" +v_acb = Quantity.create name: "Riboflavin (B2)", domain: :diet, parent: v_ac, + description: "Vitamin G" +v_acc = Quantity.create name: "Vitamin B3", domain: :diet, parent: v_ac, + description: "Vitamin PP" +v_acca = Quantity.create name: "Niacin", domain: :diet, parent: v_acc, + description: "Nicotinic acid" +v_acd = Quantity.create name: "Vitamin B5", domain: :diet, parent: v_ac, + description: "Pantothenic acid" +v_ace = Quantity.create name: "Vitamin B6", domain: :diet, parent: v_ac, + description: "" +v_acf = Quantity.create name: "Biotin (B7)", domain: :diet, parent: v_ac, + description: "Vitamin H, also coenzyme R" +v_acg = Quantity.create name: "Folate", domain: :diet, parent: v_ac, + description: "Includes: folic acid, folacin and vitamin B9" +v_acga = Quantity.create name: "Vitamin B9", domain: :diet, parent: v_acg, + description: "" +v_ach = Quantity.create name: "Cobalamin (B12)", domain: :diet, parent: v_ac, + description: "" +v_ad = Quantity.create name: "Vitamin C", domain: :diet, parent: v_a, + description: "" +v_ae = Quantity.create name: "Vitamin D", domain: :diet, parent: v_a, + description: "Calciferol" +v_aea = Quantity.create name: "Cholecalciferol (D3)", domain: :diet, parent: v_ae, + description: "" +v_af = Quantity.create name: "Vitamin E", domain: :diet, parent: v_a, + description: "" +v_ag = Quantity.create name: "Vitamin K", domain: :diet, parent: v_a, + description: "" + +# -> Body composition +b_a = Quantity.create name: "Body composition", domain: :measurement, parent: nil, + description: "" +b_aa = Quantity.create name: "Weight", domain: :measurement, parent: b_a, + description: "Total weight" +b_aaa = Quantity.create name: "Fat", domain: :measurement, parent: b_aa, + description: "Fat weight" +b_aab = Quantity.create name: "Muscle", domain: :measurement, parent: b_aa, + description: "Muscle weight" +b_ab = Quantity.create name: "Composition", domain: :measurement, parent: b_a, + description: "" +b_aba = Quantity.create name: "% fat", domain: :measurement, parent: b_ab, + description: "Fat as a % of total body weight" +b_abb = Quantity.create name: "% muscle", domain: :measurement, parent: b_ab, + description: "Muscle as a % of total body weight" +b_ac = Quantity.create name: "RM", domain: :measurement, parent: b_a, + description: "Resting metabolism" +b_ad = Quantity.create name: "VF", domain: :measurement, parent: b_a, + description: "Visceral fat" + +# Formulas go at the and to make sure dependencies exist +e_aa.create_formula zero_nil: true, unit: u_b, + code: "4*Proteins + 9*Fats + 4*Carbs + 2*Fibre" +e_ab.create_formula zero_nil: true, unit: u_c, + code: "100*Energy/RM.lastBefore(Meal.eaten_at||Meal.created_at)" +e_ac.create_formula zero_nil: true, unit: u_b, + code: "4*Proteins" +e_aca.create_formula zero_nil: true, unit: u_c, + code: "100*proteins/RM.lastBefore(Meal.eaten_at||Meal.created_at)" +e_ad.create_formula zero_nil: true, unit: u_b, + code: "4*Fats" +e_ada.create_formula zero_nil: true, unit: u_c, + code: "100*fats/RM.lastBefore(Meal.eaten_at||Meal.created_at)" +e_ae.create_formula zero_nil: true, unit: u_b, + code: "4*Carbs" +e_aea.create_formula zero_nil: true, unit: u_c, + code: "100*carbs/RM.lastBefore(Meal.eaten_at||Meal.created_at)" + +b_aaa.create_formula zero_nil: true, unit: u_ac, + code: "'% fat' * Weight" + +# Sources +s_a = Source.create name: "nutrition label", + description: "nutrition facts taken from package nutrition label" diff --git a/lib/tasks/body_tracking.rake b/lib/tasks/body_tracking.rake new file mode 100644 index 0000000..198333e --- /dev/null +++ b/lib/tasks/body_tracking.rake @@ -0,0 +1,20 @@ + +require Rails.root.join('config', 'environment') + +namespace :redmine do + namespace :body_tracking do + desc "Loads body_tracking plugin seed data from db/seeds.rb. Requires pending" \ + " migrations to be applied before running. Purges and reloads all seed data." + task seed: [:environment, 'db:abort_if_pending_migrations'] do + seed_fn = Rails.root.join('plugins', 'body_tracking', 'db', 'seeds.rb') + if seed_fn.exist? + print "Loading seed data from #{seed_fn}..." + load(seed_fn) + puts "done" + else + puts "Seed data file #{seed_fn} is missing :/" + end + end + end +end + diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index efb64b6..0e9fd48 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -25,7 +25,7 @@ class BodyTrackingSystemTestCase < ApplicationSystemTestCase # Redmine fixtures use explicit IDs, so it's impossible to access them by name. # Use: 'project_id: 1' and NOT 'project: projects_001' plugin_fixtures :enabled_modules, :roles, :member_roles, - :quantities, :units, :goals, :exposures, :targets, :quantity_values + :sources, :quantities, :units, :formulas, :goals, :exposures, :targets, :quantity_values include AbstractController::Translation diff --git a/test/fixtures/formulas.yml b/test/fixtures/formulas.yml new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/sources.yml b/test/fixtures/sources.yml new file mode 100644 index 0000000..e69de29 diff --git a/test/system/body_trackers_test.rb b/test/system/body_trackers_test.rb index f620aa3..5b30fb4 100644 --- a/test/system/body_trackers_test.rb +++ b/test/system/body_trackers_test.rb @@ -1,4 +1,5 @@ require File.expand_path('../../application_system_test_case', __FILE__) +require 'rake' class BodyTrackersTest < BodyTrackingSystemTestCase def setup @@ -7,12 +8,24 @@ class BodyTrackersTest < BodyTrackingSystemTestCase log_user 'jsmith', 'jsmith' end - def test_defaults_load + def test_defaults_seed_and_load_into_empty_project + Rails.application.load_tasks + Rake::Task['redmine:body_tracking:seed'].invoke + counts = [Source, Quantity, Formula, Unit].map do |model| + assoc = model.to_s.downcase.pluralize + @project1.send(assoc).delete_all unless assoc == 'formulas' + ["@project1.#{assoc}.reload.count", model.defaults.count] + end.to_h + visit project_body_trackers_path(@project1) - accept_alert t('layouts.sidebar.confirm_defaults') do - click_link t('layouts.sidebar.link_defaults') + assert_difference counts do + accept_alert t('layouts.sidebar.confirm_defaults') do + click_link t('layouts.sidebar.link_defaults') + end + # click_link is asynchronuous, need to wait for page reload before + # checking differences + assert_selector 'div#flash_notice' + assert_no_selector 'div#flash_error' end - assert_selector 'div#flash_notice' - assert_no_selector 'div#flash_error' end end