diff --git a/app/controllers/measurements_controller.rb b/app/controllers/measurements_controller.rb index 3b40b71..8fdb316 100644 --- a/app/controllers/measurements_controller.rb +++ b/app/controllers/measurements_controller.rb @@ -31,7 +31,7 @@ class MeasurementsController < ApplicationController # Nested attributes cannot create outer object (Measurement) and at the same time edit # existing nested object (MeasurementRoutine) if it's not associated with outer object # https://stackoverflow.com/questions/6346134/ - # That's why routine needs to be found and associated before measurement initialization + # That's why Routine needs to be found and associated before Measurement initialization @measurement = @project.measurements.new update_routine_from_params @measurement.attributes = measurement_params @@ -127,7 +127,8 @@ class MeasurementsController < ApplicationController def update_routine_from_params routine_id = params[:measurement][:routine_attributes][:id] - @measurement.routine = @project.measurement_routines.find_by(id: routine_id) if routine_id + return unless routine_id + @measurement.routine = @project.measurement_routines.find_by(id: routine_id) end def prepare_items diff --git a/app/controllers/targets_controller.rb b/app/controllers/targets_controller.rb index d5804f4..154ed8a 100644 --- a/app/controllers/targets_controller.rb +++ b/app/controllers/targets_controller.rb @@ -5,19 +5,59 @@ class TargetsController < ApplicationController include Concerns::Finders - before_action :find_project_by_project_id, only: [:index, :new] + before_action :find_project_by_project_id, only: [:index, :new, :create] def index prepare_targets end def new - @target = (@goal || @project.goals.binding).targets.new - @target.arity.times { @target.thresholds.new } + target = (@goal || @project.goals.binding).targets.new + target.arity.times { target.thresholds.new } + @targets = [target] + end + + def create + goal = @project.goals.find_by(id: params[:goal][:id]) || @project.goals.build(goal_params) + @targets = goal.targets.build(targets_params[:targets]) do |target| + target.effective_from = params[:target][:effective_from] + end + + # :save only after build, to re-display values in case records are invalid + if goal.save && Target.transaction { @targets.all?(&:save) } + flash[:notice] = 'Created new target(s)' + prepare_targets + else + @targets.each do |target| + (target.thresholds.length...target.arity).each { target.thresholds.new } + target.thresholds[target.arity..-1].map(&:destroy) + end + render :new + end end private + def goal_params + params.require(:goal).permit(:id, :name, :description) + end + + def targets_params + params.require(:target).permit( + targets: [ + :id, + :condition, + :scope, + thresholds_attributes: [ + :id, + :quantity_id, + :value, + :unit_id + ] + ] + ) + end + def prepare_targets @targets = @project.targets.includes(:item, :thresholds) end diff --git a/app/helpers/body_trackers_helper.rb b/app/helpers/body_trackers_helper.rb index ef383ab..a9aa842 100644 --- a/app/helpers/body_trackers_helper.rb +++ b/app/helpers/body_trackers_helper.rb @@ -33,8 +33,13 @@ module BodyTrackersHelper end def quantity_options(domain = :all) - nested_set_options(@project.quantities.send(domain)) do |q| - raw("#{' ' * q.level}#{q.name}") + Quantity.each_with_ancestors(@project.quantities.send(domain)).map do |ancestors| + quantity = ancestors.last + [ + raw("#{' ' * (ancestors.length-2)}#{quantity.name}"), + quantity.id, + {'data-path' => ancestors[1..-2].reduce('::') { |m, q| "#{m}#{q.try(:name)}::" }} + ] end end @@ -44,6 +49,7 @@ module BodyTrackersHelper end end + # TODO: rename to quantities_table_header def table_header_spec(quantities) # spec: table of rows (tr), where each row is a hash of cells (td) (hash keeps items # ordered the way they were added). Hash values determine cell property: diff --git a/app/helpers/targets_helper.rb b/app/helpers/targets_helper.rb index e695f23..9365156 100644 --- a/app/helpers/targets_helper.rb +++ b/app/helpers/targets_helper.rb @@ -1,6 +1,6 @@ module TargetsHelper def condition_options - Target::CONDITIONS.each_with_index.to_a + Target::CONDITIONS end def action_links(m) diff --git a/app/models/target.rb b/app/models/target.rb index 7569e1a..8a639ca 100644 --- a/app/models/target.rb +++ b/app/models/target.rb @@ -1,5 +1,5 @@ class Target < ActiveRecord::Base - CONDITIONS = [:<, :<=, :>, :>=, :==] + CONDITIONS = ['<', '<=', '>', '>=', '=='] belongs_to :goal, inverse_of: :targets, required: true belongs_to :item, polymorphic: true, inverse_of: :targets @@ -10,8 +10,8 @@ class Target < ActiveRecord::Base accepts_nested_attributes_for :thresholds, allow_destroy: true, reject_if: proc { |attrs| attrs['quantity_id'].blank? && attrs['value'].blank? } validate do - errors.add(:thresholds, :count_mismatch) unless thresholds.count == arity - errors.add(:thresholds, :quantity_mismatch) if thresholds.to_a.uniq(&:quantity) != 1 + errors.add(:thresholds, :count_mismatch) unless thresholds.length == arity + errors.add(:thresholds, :quantity_mismatch) if thresholds.to_a.uniq(&:quantity).length != 1 end validates :condition, inclusion: {in: CONDITIONS} validates :scope, inclusion: {in: [:ingredient, :meal, :day], @@ -20,8 +20,9 @@ class Target < ActiveRecord::Base after_initialize do if new_record? - self.condition = CONDITIONS.first - self.effective_from = Date.current if is_binding? + self.condition ||= CONDITIONS.first + # Target should be only instantiated through Goal, so :is_binding? will be available + self.effective_from ||= Date.current if is_binding? end end diff --git a/app/views/goals/_show_form.html.erb b/app/views/goals/_show_form.html.erb index 121eb9d..4ade2c8 100644 --- a/app/views/goals/_show_form.html.erb +++ b/app/views/goals/_show_form.html.erb @@ -1,4 +1,4 @@ -<%= fields_for 'target[goal_attributes]', goal do |ff| %> +<%= fields_for 'goal', goal do |ff| %> <%= ff.select :id, options_from_collection_for_select(@project.goals, :id, :name, goal.id), diff --git a/app/views/goals/show.js.erb b/app/views/goals/show.js.erb new file mode 100644 index 0000000..fef1988 --- /dev/null +++ b/app/views/goals/show.js.erb @@ -0,0 +1,2 @@ +$('#goal-form').html('<%= j render partial: 'goals/show_form', locals: {goal: @goal} %>'); +$('input#target_effective_from').prop('disabled', <%= @goal.is_binding? %>) diff --git a/app/views/targets/_form.html.erb b/app/views/targets/_form.html.erb index 2fe4acd..23d9e9d 100644 --- a/app/views/targets/_form.html.erb +++ b/app/views/targets/_form.html.erb @@ -1,66 +1,86 @@ -<%= error_messages_for @target %> +<%= error_messages_for *@targets %> -
-
- <% if @target.goal.persisted? %> -

<%= render partial: 'goals/show_form', locals: {goal: @target.goal} %>

- <%#= t '.effective_from' %> -

<%= f.date_field :effective_from, disabled: !@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, label: '' %> - <% 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 %> +<% @targets.group_by(&:goal).each do |goal, targets| %> +

+
+ <% if goal.persisted? %> +

<%= render partial: 'goals/show_form', locals: {goal: goal} %>

+ <%#= t '.effective_from' %> +

<%= f.date_field :effective_from, disabled: goal.is_binding? %>

+ <% else %> + <%= render partial: 'goals/form', locals: {goal: goal} %> <% 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;' %> -

+
+ +
+ +
+ <% targets.each do |target| %> +

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

+ <% end %> +

+ <%= link_to t(".button_new_target"), '#', class: 'icon icon-add', + onclick: 'newTarget(); return false;' %> +

+
-
+<% end %> <%= javascript_tag do %> - function newReadout() { + function showQuantityPath(event) { + $(event.target).prevAll('em').text($('option:selected', event.target).attr('data-path')); + } + $(document).ajaxComplete(function() { + $('select[id$=__quantity_id]').trigger(jQuery.Event('change')); + }) + + function newTarget() { var form = $(event.target).closest('form'); - var row = form.find('p.readout:visible:last'); + var row = form.find('p.target: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('em').text('<%= t ".choose_quantity" %>'); + new_row.find('input, select').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(); + form.find('p.target:visible a.icon-del').show(); } - function deleteReadout() { + function deleteTarget() { var form = $(event.target).closest('form'); - var row = $(event.target).closest('p.readout'); + var row = $(event.target).closest('p.target'); 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(); + form.find('p.target:visible:first label:first').show(); + if (form.find('p.target:visible').length <= 1) { + form.find('p.target:visible a.icon-del').hide(); } } <% end %> diff --git a/app/views/targets/_new_form.html.erb b/app/views/targets/_new_form.html.erb index 11c3f5c..351b872 100644 --- a/app/views/targets/_new_form.html.erb +++ b/app/views/targets/_new_form.html.erb @@ -1,6 +1,6 @@

<%= t ".heading_new_target" %>

-<%= labelled_form_for @target, +<%= labelled_form_for @targets, url: project_targets_path(@project, @view_params), remote: true, html: {id: 'new-target-form', name: 'new-target-form'} do |f| %> diff --git a/app/views/targets/create.js.erb b/app/views/targets/create.js.erb new file mode 100644 index 0000000..4ad3eae --- /dev/null +++ b/app/views/targets/create.js.erb @@ -0,0 +1,7 @@ +$('#new-targets').empty(); +<% case @view_params[:view] %> + <% when :by_date %> + $('#targets').html('<%= j render partial: 'targets/by_date' %>'); + <% else %> + $('#targets').html('<%= j render partial: 'targets/index' %>'); +<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index d7e4cbc..b1168c9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -93,6 +93,7 @@ en: contextual: link_new_target: 'New target' form: + choose_quantity: "Choose quantity" button_new_target: 'Add target' button_delete_target: 'Delete' effective_from: ', effective from:' diff --git a/lib/body_tracking/awesome_nested_set_patch.rb b/lib/body_tracking/awesome_nested_set_patch.rb index 1e3a772..59c95c3 100644 --- a/lib/body_tracking/awesome_nested_set_patch.rb +++ b/lib/body_tracking/awesome_nested_set_patch.rb @@ -2,21 +2,31 @@ module BodyTracking::AwesomeNestedSetPatch CollectiveIdea::Acts::NestedSet.class_eval do module CollectiveIdea::Acts::NestedSet class Iterator - def each_with_path + def each_with_ancestors return to_enum(__method__) { objects.length } unless block_given? - path = [nil] + ancestors = [nil] objects.each do |o| - path[path.rindex(o.parent)+1..-1] = o - yield [o, path.map { |q| q.try(:name) }.join('::')] + ancestors[ancestors.rindex(o.parent)+1..-1] = o + yield ancestors end end end module Model module ClassMethods - def each_with_path(objects, &block) - Iterator.new(objects).each_with_path(&block) + def each_with_path(objects) + return to_enum(__method__, objects) { objects.length } unless block_given? + + Iterator.new(objects).each_with_ancestors do |ancestors| + yield [ancestors.last, ancestors.map { |q| q.try(:name) }.join('::')] + end + end + + def each_with_ancestors(objects) + return to_enum(__method__, objects) { objects.length } unless block_given? + + Iterator.new(objects).each_with_ancestors { |ancestors| yield ancestors.dup } end end end