From 0df2c6ec4f4e7c07a5ef5317407a0daef937f601 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sat, 27 Jun 2020 21:52:32 +0200 Subject: [PATCH] Preliminary Target #new support --- app/controllers/targets_controller.rb | 4 +- app/helpers/body_trackers_helper.rb | 6 ++ app/helpers/foods_helper.rb | 6 -- app/helpers/measurements_helper.rb | 6 -- app/helpers/targets_helper.rb | 13 ++++ app/models/target.rb | 20 ++++-- app/views/foods/_form.html.erb | 2 +- app/views/goals/_form.html.erb | 13 ++++ app/views/goals/_show_form.html.erb | 27 ++++++++ .../measurement_routines/_show_form.html.erb | 2 +- app/views/measurements/_form.html.erb | 2 +- app/views/targets/_form.html.erb | 65 +++++++++++++++++++ app/views/targets/_new_form.html.erb | 18 +++++ app/views/targets/new.js.erb | 1 + config/locales/en.yml | 7 +- config/routes.rb | 1 + init.rb | 2 + lib/body_tracking/project_patch.rb | 8 ++- 18 files changed, 177 insertions(+), 26 deletions(-) create mode 100644 app/helpers/targets_helper.rb create mode 100644 app/views/goals/_form.html.erb create mode 100644 app/views/goals/_show_form.html.erb create mode 100644 app/views/targets/_form.html.erb create mode 100644 app/views/targets/_new_form.html.erb create mode 100644 app/views/targets/new.js.erb diff --git a/app/controllers/targets_controller.rb b/app/controllers/targets_controller.rb index 0a4077b..d5804f4 100644 --- a/app/controllers/targets_controller.rb +++ b/app/controllers/targets_controller.rb @@ -5,14 +5,14 @@ class TargetsController < ApplicationController include Concerns::Finders - before_action :find_project_by_project_id, only: [:index] + before_action :find_project_by_project_id, only: [:index, :new] def index prepare_targets end def new - @target = @project.targets.new + @target = (@goal || @project.goals.binding).targets.new @target.arity.times { @target.thresholds.new } end diff --git a/app/helpers/body_trackers_helper.rb b/app/helpers/body_trackers_helper.rb index 16964c3..ef383ab 100644 --- a/app/helpers/body_trackers_helper.rb +++ b/app/helpers/body_trackers_helper.rb @@ -32,6 +32,12 @@ module BodyTrackersHelper options_for_select(options, disabled: 0) end + def quantity_options(domain = :all) + nested_set_options(@project.quantities.send(domain)) do |q| + raw("#{' ' * q.level}#{q.name}") + end + end + def unit_options @project.units.map do |u| [u.shortname, u.id] diff --git a/app/helpers/foods_helper.rb b/app/helpers/foods_helper.rb index 8940039..156fd28 100644 --- a/app/helpers/foods_helper.rb +++ b/app/helpers/foods_helper.rb @@ -1,10 +1,4 @@ module FoodsHelper - def quantity_options - nested_set_options(@project.quantities.diet) do |q| - raw("#{' ' * q.level}#{q.name}") - end - end - def visibility_options(selected) options = [["visible", 1], ["hidden", 0]] options_for_select(options, selected) diff --git a/app/helpers/measurements_helper.rb b/app/helpers/measurements_helper.rb index 77d646d..5bff5b0 100644 --- a/app/helpers/measurements_helper.rb +++ b/app/helpers/measurements_helper.rb @@ -5,12 +5,6 @@ module MeasurementsHelper .html_safe end - def quantity_options - nested_set_options(@project.quantities.measurement) do |q| - raw("#{' ' * q.level}#{q.name}") - end - end - def source_options @project.sources.map do |s| [s.name, s.id] diff --git a/app/helpers/targets_helper.rb b/app/helpers/targets_helper.rb new file mode 100644 index 0000000..e695f23 --- /dev/null +++ b/app/helpers/targets_helper.rb @@ -0,0 +1,13 @@ +module TargetsHelper + def condition_options + Target::CONDITIONS.each_with_index.to_a + end + + def action_links(m) + link_to(l(:button_retake), retake_measurement_path(m, @view_params), + {remote: true, class: "icon icon-reload"}) + + link_to(l(:button_edit), edit_measurement_path(m, @view_params), + {remote: true, class: "icon icon-edit"}) + + delete_link(measurement_path(m), {remote: true, data: {}}) + end +end diff --git a/app/models/target.rb b/app/models/target.rb index 402271e..ac9a008 100644 --- a/app/models/target.rb +++ b/app/models/target.rb @@ -1,5 +1,7 @@ class Target < ActiveRecord::Base - belongs_to :goal, inverse_of: :targets + CONDITIONS = [:<, :<=, :>, :>=, :==] + + belongs_to :goal, inverse_of: :targets, required: true belongs_to :item, polymorphic: true, inverse_of: :targets has_many :thresholds, as: :registry, inverse_of: :target, dependent: :destroy, validate: true @@ -7,19 +9,25 @@ class Target < ActiveRecord::Base validates :thresholds, presence: true accepts_nested_attributes_for :thresholds, allow_destroy: true, reject_if: proc { |attrs| attrs['quantity_id'].blank? && attrs['value'].blank? } - # TODO: validate thresholds count according to condition type - validates :condition, inclusion: {in: [:<, :<=, :>, :>=, :==]} + validate do + errors.add(:thresholds, :count_mismatch) unless thresholds.count == arity + errors.add(:thresholds, :quantity_mismatch) if thresholds.to_a.uniq(&:quantity) != 1 + end + validates :condition, inclusion: {in: CONDITIONS } validates :scope, inclusion: {in: [:day], if: -> { thresholds.first.domain == :diet }} - validates :effective_from, presence: {unless: -> { goal.present? }}, - absence: {if: -> { goal.present? }} + validates :effective_from, presence: {unless: :is_binding?}, absence: {if: :is_binding?} after_initialize do if new_record? - self.condition = :< + self.condition = CONDITIONS.first end end def arity BigDecimal.method(condition).arity end + + def is_binding? + goal == goal.project.goals.binding + end end diff --git a/app/views/foods/_form.html.erb b/app/views/foods/_form.html.erb index 58caaf5..55af5ba 100644 --- a/app/views/foods/_form.html.erb +++ b/app/views/foods/_form.html.erb @@ -25,7 +25,7 @@ <%= f.fields_for 'nutrients_attributes', n, index: '' do |ff| %>

<%= ff.hidden_field :id %> - <%= ff.select :quantity_id, quantity_options, + <%= ff.select :quantity_id, quantity_options(:diet), {include_blank: true, required: true, label: (index > 0 ? '' : :field_nutrients)} %> <%= ff.number_field :amount, {size: 8, min: 0, step: :any, label: ''} %> <%= ff.select :unit_id, unit_options, {label: ''} %> diff --git a/app/views/goals/_form.html.erb b/app/views/goals/_form.html.erb new file mode 100644 index 0000000..ce32e44 --- /dev/null +++ b/app/views/goals/_form.html.erb @@ -0,0 +1,13 @@ +

+ <%= fields_for 'measurement[routine_attributes]', routine do |ff| %> + <%= ff.hidden_field :id %> +

+ + <%= ff.text_field :name, required: true, style: "width: 95%;" %> +

+

+ + <%= ff.text_area :description, cols: 40, rows: 3, style: "width: 95%;" %> +

+ <% end %> +
diff --git a/app/views/goals/_show_form.html.erb b/app/views/goals/_show_form.html.erb new file mode 100644 index 0000000..8d3fc67 --- /dev/null +++ b/app/views/goals/_show_form.html.erb @@ -0,0 +1,27 @@ +
+<%= fields_for 'target[goal_attributes]', goal do |ff| %> +

+ + <%= ff.select :id, + options_from_collection_for_select(@project.goals, :id, :name, goal.id), + {required: true}, autocomplete: 'off', + onchange: "var goal_id = $('#target_goal_attributes_id').val(); + $.ajax({ + url: '#{goal_path(id: :goal_id)}'.replace('goal_id', goal_id), + dataType: 'script' + }); + return false;" %> + <%= link_to l(:button_edit), '#', + onclick: "var goal_id = $('#target_goal_attributes_id').val(); + $.ajax({ + url: '#{edit_goal_path(id: :goal_id)}'.replace('goal_id', goal_id), + dataType: 'script' + }); + return false;", + class: 'icon icon-edit' %> +

+<% end %> +<% if goal.description? %> +

<%= goal.description %>

+<% end %> +
diff --git a/app/views/measurement_routines/_show_form.html.erb b/app/views/measurement_routines/_show_form.html.erb index 29946f7..eea8be5 100644 --- a/app/views/measurement_routines/_show_form.html.erb +++ b/app/views/measurement_routines/_show_form.html.erb @@ -21,7 +21,7 @@ class: 'icon icon-edit' %>

<% end %> -<% unless routine.description.empty? %> +<% if routine.description? %>

<%= routine.description %>

<% end %> diff --git a/app/views/measurements/_form.html.erb b/app/views/measurements/_form.html.erb index 58e32d0..c76c209 100644 --- a/app/views/measurements/_form.html.erb +++ b/app/views/measurements/_form.html.erb @@ -30,7 +30,7 @@ <%= f.fields_for 'readouts_attributes', r, index: '' do |ff| %>

<%= ff.hidden_field :id %> - <%= ff.select :quantity_id, quantity_options, + <%= ff.select :quantity_id, quantity_options(:measurement), {include_blank: true, required: true, label: (index > 0 ? '' : :field_readouts)} %> <%= ff.number_field :value, {size: 8, step: :any, label: ''} %> <%= ff.select :unit_id, unit_options, {label: ''} %> diff --git a/app/views/targets/_form.html.erb b/app/views/targets/_form.html.erb new file mode 100644 index 0000000..250a6b1 --- /dev/null +++ b/app/views/targets/_form.html.erb @@ -0,0 +1,65 @@ +<%= error_messages_for @target %> + +

+
+ <% if @target.goal.persisted? %> + <%= render partial: 'goals/show_form', locals: {goal: @target.goal} %> + <%= f.date_field(:effective_from, required: true) if @target.is_binding? %> + <% else %> + <%= render partial: 'goals/form', locals: {goal: @target.goal} %> + <% end %> +
+
+
+

+ <% @target.thresholds.each_with_index do |t, index| %> + <%= f.fields_for 'thresholds_attributes', t, index: '' do |ff| %> + <% if index == 0 %> + <%= ff.select :quantity_id, quantity_options, + {include_blank: true, required: true, label: :field_target} %> + <%= f.select :condition, condition_options, required: true %> + <% end %> + <%= ff.hidden_field :id %> + <%= ff.number_field :value, {size: 8, step: :any, label: ''} %> + <% if index == 0 %> + <%= ff.select :unit_id, unit_options, {label: ''} %> + <% end %> + <% end %> + <% end %> + <%= link_to t(".button_delete_target"), '#', + class: 'icon icon-del', + onclick: "deleteTarget(); return false;" %> +

+ <%= link_to t(".button_new_target"), '#', + class: 'icon icon-add', + onclick: 'newTarget(); return false;' %> +
+
+ +<%= javascript_tag do %> + function newReadout() { + var form = $(event.target).closest('form'); + var row = form.find('p.readout:visible:last'); + var new_row = row.clone().insertAfter(row); + new_row.find('input[id$=__id], input[id$=__value], select[id$=_quantity__id]').val(''); + new_row.find('select[id$=__unit_id]').val(row.find('select[id$=__unit_id]').val()); + new_row.find('input[id$=__destroy]').val(''); + new_row.find('label:first').hide(); + form.find('p.readout:visible a.icon-del').show(); + } + + function deleteReadout() { + var form = $(event.target).closest('form'); + var row = $(event.target).closest('p.readout'); + if (row.find('input[id$=__id]').val()) { + row.hide(); + row.find('input[id$=__destroy]').val('1'); + } else { + row.remove(); + } + form.find('p.readout:visible:first label:first').show(); + if (form.find('p.readout:visible').length <= 1) { + form.find('p.readout:visible a.icon-del').hide(); + } + } +<% end %> diff --git a/app/views/targets/_new_form.html.erb b/app/views/targets/_new_form.html.erb new file mode 100644 index 0000000..11c3f5c --- /dev/null +++ b/app/views/targets/_new_form.html.erb @@ -0,0 +1,18 @@ +

<%= t ".heading_new_target" %>

+ +<%= labelled_form_for @target, + url: project_targets_path(@project, @view_params), + remote: true, + html: {id: 'new-target-form', name: 'new-target-form'} do |f| %> + + <%= render partial: 'targets/form', locals: {f: f} %> + +
+

+ <%= submit_tag l(:button_create) %> + <%= link_to l(:button_cancel), "#", + onclick: '$("#new-target").empty(); return false;' %> +

+
+<% end %> +
diff --git a/app/views/targets/new.js.erb b/app/views/targets/new.js.erb new file mode 100644 index 0000000..db5d1ba --- /dev/null +++ b/app/views/targets/new.js.erb @@ -0,0 +1 @@ +$('#new-target').html('<%= j render partial: 'targets/new_form' %>'); diff --git a/config/locales/en.yml b/config/locales/en.yml index 1ba78d5..65358d9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -27,6 +27,11 @@ en: activerecord: errors: models: + target: + attributes: + thresholds: + count_mismatch: 'count invalid for given condition' + quantity_mismatch: 'should refer to the same quantity' meal: attributes: ingredients: @@ -34,7 +39,7 @@ en: measurement: attributes: readouts: - duplicated_quantity_unit_pair: 'each quantity+unit pair can only be specified + duplicated_quantity_unit_pair: 'each (quantity, unit) pair can only be specified once per measurement' food: attributes: diff --git a/config/routes.rb b/config/routes.rb index 11e81d4..7b2fabf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,6 +7,7 @@ resources :projects, shallow: true do post 'defaults' end end + resources :goals, only: [:show, :edit] resources :targets, except: [:show] resources :ingredients, only: [] do post 'adjust/:adjustment', to: 'meals#adjust', as: :adjust, on: :member diff --git a/init.rb b/init.rb index b70726e..ee80c54 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], + goals: [:show], targets: [:index], meals: [:index], measurement_routines: [:show], @@ -25,6 +26,7 @@ Redmine::Plugin.register :body_tracking do }, read: true permission :manage_common, { body_trackers: [:defaults], + goals: [:edit], targets: [:new, :create, :edit, :update, :destroy], meals: [:new, :create, :edit, :update, :destroy, :edit_notes, :update_notes, :toggle_eaten, :toggle_exposure, :adjust], diff --git a/lib/body_tracking/project_patch.rb b/lib/body_tracking/project_patch.rb index acb669e..4b20c8b 100644 --- a/lib/body_tracking/project_patch.rb +++ b/lib/body_tracking/project_patch.rb @@ -29,7 +29,11 @@ module BodyTracking::ProjectPatch has_many :meal_quantities, -> { order "lft" }, through: :meal_exposures, source: 'quantity' - has_many :thresholds, through: :quantities - has_many :targets, through: :thresholds, source_type: 'Target' + has_many :goals, dependent: :destroy do + def binding + find_or_create_by(name: "binding") + end + end + has_many :targets, through: :goals end end