diff --git a/app/controllers/targets_controller.rb b/app/controllers/targets_controller.rb new file mode 100644 index 0000000..0a4077b --- /dev/null +++ b/app/controllers/targets_controller.rb @@ -0,0 +1,24 @@ +class TargetsController < ApplicationController + layout 'body_tracking' + menu_item :body_trackers + helper :body_trackers + + include Concerns::Finders + + before_action :find_project_by_project_id, only: [:index] + + def index + prepare_targets + end + + def new + @target = @project.targets.new + @target.arity.times { @target.thresholds.new } + end + + private + + def prepare_targets + @targets = @project.targets.includes(:item, :thresholds) + end +end diff --git a/app/models/goal.rb b/app/models/goal.rb new file mode 100644 index 0000000..051b921 --- /dev/null +++ b/app/models/goal.rb @@ -0,0 +1,6 @@ +class Goal < ActiveRecord::Base + belongs_to :project, required: true + has_many :targets, inverse_of: :goal, dependent: :destroy + + validates :name, presence: true, uniqueness: {scope: :project_id} +end diff --git a/app/models/ingredient.rb b/app/models/ingredient.rb index 81c78ea..3d48dc0 100644 --- a/app/models/ingredient.rb +++ b/app/models/ingredient.rb @@ -2,7 +2,7 @@ class Ingredient < ActiveRecord::Base belongs_to :composition, inverse_of: :ingredients, polymorphic: true, required: true belongs_to :food, required: true belongs_to :part_of, required: false - has_many :nutrients, through: :food, source: :nutrients + has_many :nutrients, through: :food DOMAIN = :diet alias_attribute :subitems, :nutrients diff --git a/app/models/nutrient.rb b/app/models/nutrient.rb index 1d75aed..1fdbbba 100644 --- a/app/models/nutrient.rb +++ b/app/models/nutrient.rb @@ -1,6 +1,8 @@ class Nutrient < QuantityValue - belongs_to :food, foreign_key: 'registry_id', foreign_type: 'registry_type', - inverse_of: :nutrients, polymorphic: true, required: true + # Need to specify polymorphic association so :registry_type gets written (see + # QuantityValue for explanation why it's needed) + belongs_to :food, inverse_of: :nutrients, polymorphic: true, required: true, + foreign_key: 'registry_id', foreign_type: 'registry_type' # Uniqueness NOT validated here, see Value for explanation #validates :quantity, uniqueness: {scope: :food_id} diff --git a/app/models/quantity.rb b/app/models/quantity.rb index 74f5118..4d5fb66 100644 --- a/app/models/quantity.rb +++ b/app/models/quantity.rb @@ -9,6 +9,7 @@ class Quantity < ActiveRecord::Base belongs_to :project, inverse_of: :quantities, required: false has_many :nutrients, dependent: :restrict_with_error has_many :readouts, dependent: :restrict_with_error + has_many :thresholds, dependent: :restrict_with_error has_many :values, class_name: 'QuantityValue', dependent: :restrict_with_error has_many :exposures, dependent: :destroy diff --git a/app/models/quantity_value.rb b/app/models/quantity_value.rb index 9dd04b1..ca1c740 100644 --- a/app/models/quantity_value.rb +++ b/app/models/quantity_value.rb @@ -1,5 +1,9 @@ class QuantityValue < ActiveRecord::Base # Requirement validation for :registry left to subclasses + # Polymorphic registry (including :registry_type) is required - despite 1:1 + # mapping between Nutrient:Food, Readout:Measurement, Threshold:Target ... - + # to allow for accessing registry item without knowing QuantityValue (subitem) + # type, e.g. qv.registry.completed_at belongs_to :registry, polymorphic: true belongs_to :quantity, required: true belongs_to :unit, required: true diff --git a/app/models/readout.rb b/app/models/readout.rb index 6c7834a..09789d0 100644 --- a/app/models/readout.rb +++ b/app/models/readout.rb @@ -1,6 +1,8 @@ class Readout < QuantityValue - belongs_to :measurement, foreign_key: 'registry_id', foreign_type: 'registry_type', - inverse_of: :readouts, polymorphic: true, required: true + # Need to specify polymorphic association so :registry_type gets written (see + # QuantityValue for explanation why it's needed) + belongs_to :measurement, inverse_of: :readouts, polymorphic: true, required: true, + foreign_key: 'registry_id', foreign_type: 'registry_type' # Uniqueness NOT validated here, see Value for explanation #validates :quantity, uniqueness: {scope: [:measurement_id, :unit_id]} diff --git a/app/models/target.rb b/app/models/target.rb index 96d273f..402271e 100644 --- a/app/models/target.rb +++ b/app/models/target.rb @@ -10,5 +10,16 @@ class Target < ActiveRecord::Base # TODO: validate thresholds count according to condition type validates :condition, inclusion: {in: [:<, :<=, :>, :>=, :==]} validates :scope, inclusion: {in: [:day], if: -> { thresholds.first.domain == :diet }} - validates :effective_from, presence: {unless: goal?}, absence: {if: goal?} + validates :effective_from, presence: {unless: -> { goal.present? }}, + absence: {if: -> { goal.present? }} + + after_initialize do + if new_record? + self.condition = :< + end + end + + def arity + BigDecimal.method(condition).arity + end end diff --git a/app/models/threshold.rb b/app/models/threshold.rb index 54f6ad1..742da1e 100644 --- a/app/models/threshold.rb +++ b/app/models/threshold.rb @@ -1,6 +1,8 @@ class Threshold < QuantityValue - belongs_to :target, foreign_key: 'registry_id', foreign_type: 'foreign_type', - inverse_of: :thresholds, polymorphic: true, required: true + # Need to specify polymorphic association so :registry_type gets written (see + # QuantityValue for explanation why it's needed) + belongs_to :target, inverse_of: :thresholds, polymorphic: true, required: true, + foreign_key: 'registry_id', foreign_type: 'registry_type' validates :value, numericality: true end diff --git a/app/views/layouts/_sidebar.html.erb b/app/views/layouts/_sidebar.html.erb index 7ea4167..7d083c9 100644 --- a/app/views/layouts/_sidebar.html.erb +++ b/app/views/layouts/_sidebar.html.erb @@ -1,6 +1,12 @@

<%= t ".heading_body_trackers" %>

<%= t ".heading_measurements" %>

diff --git a/app/views/targets/_contextual.html.erb b/app/views/targets/_contextual.html.erb new file mode 100644 index 0000000..a6bfd4e --- /dev/null +++ b/app/views/targets/_contextual.html.erb @@ -0,0 +1,5 @@ +<% if User.current.allowed_to?(:manage_common, @project) %> + <%= link_to t(".link_new_target"), + new_project_target_path(@project, @view_params), + {remote: true, class: 'icon icon-add'} %> +<% end %> diff --git a/app/views/targets/_index.html.erb b/app/views/targets/_index.html.erb new file mode 100644 index 0000000..b08d70c --- /dev/null +++ b/app/views/targets/_index.html.erb @@ -0,0 +1,34 @@ +<% if @targets.any? { |t| t.persisted? } %> + + + + + + + + + + + + <% @measurements.each do |m| %> + <% next if m.new_record? %> + + + + + + + + <% end %> + +
<%= l(:field_taken_at_date) %><%= l(:field_name) %><%= l(:field_notes) %><%= l(:field_source) %><%= l(:field_action) %>
<%= format_datetime(m) %> +
+ <%= link_to m.routine.name, readouts_measurement_routine_path(m.routine) %> +
+
+ <%= " (#{pluralize(m.readouts.size, 'readout')})" %> +
+
<%= m.notes %><%= m.source.name if m.source.present? %><%= action_links(m) %>
+<% else %> +

<%= l(:label_no_data) %>

+<% end %> diff --git a/app/views/targets/index.html.erb b/app/views/targets/index.html.erb new file mode 100644 index 0000000..b7ff56f --- /dev/null +++ b/app/views/targets/index.html.erb @@ -0,0 +1,11 @@ +
+ <%= render partial: 'targets/contextual' %> +
+ +
+
+ +

<%= t ".heading" %>

+
+ <%= render partial: 'targets/index' %> +
diff --git a/config/locales/en.yml b/config/locales/en.yml index 050a5f2..1ba78d5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -70,6 +70,8 @@ en: heading_diet: 'Diet' heading_common: 'Common' link_summary: 'Summary' + link_targets: 'Targets' + link_goals: 'Goals' link_measurements: 'Measurements' link_meals: 'Meals' link_foods: 'Foods' @@ -79,6 +81,18 @@ en: link_units: 'Units' link_defaults: 'Load defaults' confirm_defaults: 'This will load default data sources, quantities and units. Continue?' + targets: + contextual: + link_new_target: 'New target' + form: + button_new_target: 'Add target' + button_delete_target: 'Delete' + new_form: + heading_new_target: 'New target' + index: + heading: 'Targets' + show: + label_target: 'Target' meals: contextual: link_new_meal: 'New meal' diff --git a/config/routes.rb b/config/routes.rb index 6484538..11e81d4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,10 +7,11 @@ resources :projects, shallow: true do post 'defaults' end end + resources :targets, except: [:show] resources :ingredients, only: [] do post 'adjust/:adjustment', to: 'meals#adjust', as: :adjust, on: :member end - resources :meals, only: [:index, :new, :create, :edit, :update, :destroy] do + resources :meals, except: [:show] do member do get 'edit_notes' patch 'update_notes' @@ -26,7 +27,7 @@ resources :projects, shallow: true do post 'toggle_exposure', to: 'measurements#toggle_exposure' end end - resources :measurements, only: [:index, :new, :create, :edit, :update, :destroy] do + resources :measurements, except: [:show] do member do get 'retake' end @@ -34,7 +35,7 @@ resources :projects, shallow: true do get 'filter' end end - resources :foods, only: [:index, :new, :create, :edit, :update, :destroy] do + resources :foods, except: [:show] do post 'toggle', on: :member collection do get 'nutrients' @@ -45,7 +46,7 @@ resources :projects, shallow: true do end end resources :sources, only: [:index, :create, :destroy] - resources :quantities, only: [:index, :new, :create, :edit, :update, :destroy] do + resources :quantities, except: [:show] do member do get 'new_child' post 'create_child' diff --git a/db/migrate/001_create_schema.rb b/db/migrate/001_create_schema.rb index 170492f..9b65382 100644 --- a/db/migrate/001_create_schema.rb +++ b/db/migrate/001_create_schema.rb @@ -93,6 +93,13 @@ class CreateSchema < ActiveRecord::Migration t.timestamps null: false end + create_table :goals do |t| + t.references :project + t.string :name + t.text :description + t.timestamps null: false + end + create_table :targets do |t| t.references :goal t.references :item, polymorphic: true diff --git a/init.rb b/init.rb index e82d133..b70726e 100644 --- a/init.rb +++ b/init.rb @@ -14,6 +14,7 @@ Redmine::Plugin.register :body_tracking do project_module :body_tracking do permission :view_body_trackers, { body_trackers: [:index], + targets: [:index], meals: [:index], measurement_routines: [:show], measurements: [:index, :readouts, :filter], @@ -24,6 +25,7 @@ Redmine::Plugin.register :body_tracking do }, read: true permission :manage_common, { body_trackers: [:defaults], + targets: [:new, :create, :edit, :update, :destroy], meals: [:new, :create, :edit, :update, :destroy, :edit_notes, :update_notes, :toggle_eaten, :toggle_exposure, :adjust], measurement_routines: [:edit], diff --git a/lib/body_tracking/project_patch.rb b/lib/body_tracking/project_patch.rb index b59f240..acb669e 100644 --- a/lib/body_tracking/project_patch.rb +++ b/lib/body_tracking/project_patch.rb @@ -15,8 +15,8 @@ module BodyTracking::ProjectPatch source: 'quantity' has_many :measurement_routines, dependent: :destroy - has_many :measurements, -> { order "taken_at DESC" }, dependent: :destroy, - extend: BodyTracking::ItemsWithQuantities, through: :measurement_routines + has_many :measurements, -> { order "taken_at DESC" }, through: :measurement_routines, + extend: BodyTracking::ItemsWithQuantities has_many :meals, -> { order "eaten_at DESC" }, dependent: :destroy has_many :meal_ingredients, through: :meals, source: 'ingredients', @@ -28,5 +28,8 @@ module BodyTracking::ProjectPatch class_name: 'Exposure', extend: BodyTracking::TogglableExposures has_many :meal_quantities, -> { order "lft" }, through: :meal_exposures, source: 'quantity' + + has_many :thresholds, through: :quantities + has_many :targets, through: :thresholds, source_type: 'Target' end end