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