1
0

WIP: Targets configurable with Quantities

This commit is contained in:
cryptogopher
2021-02-07 11:02:41 +01:00
parent 316005bf1f
commit 8b17b33603
23 changed files with 218 additions and 108 deletions

View File

@@ -28,19 +28,19 @@ class BodyTrackersController < ApplicationController
quantities_count = available_quantities.length
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',
'created_at', 'updated_at')
if q.parent
attrs['parent'] = available_quantities[path.rpartition('::').first]
end
if q.formula
attrs['formula_attributes'] = q.formula.attributes
.except('id', 'quantity_id', 'unit_id', 'created_at', 'updated_at')
attrs['formula_attributes']['unit_id'] = available_units[q.formula.unit.shortname]
end
available_quantities[path] = @project.quantities.build(attrs)
next if available_quantities.has_key?(path)
attrs = q.attributes.except('id', 'project_id', 'parent_id', 'lft', 'rgt', 'depth',
'created_at', 'updated_at')
if q.parent
attrs['parent'] = available_quantities[path.rpartition('::').first]
end
if q.formula
attrs['formula_attributes'] = q.formula.attributes
.except('id', 'quantity_id', 'unit_id', 'created_at', 'updated_at')
attrs['formula_attributes']['unit_id'] = available_units[q.formula.unit&.shortname]
end
available_quantities[path] = @project.quantities.build(attrs)
end
Quantity.transaction do
failed_objects += available_quantities.values.reject { |o| o.persisted? || o.save }

View File

@@ -6,6 +6,7 @@ class TargetsController < ApplicationController
include Concerns::Finders
before_action :find_binding_goal_by_project_id, only: [:index, :new, :edit]
before_action :find_project, only: [:subthresholds]
before_action :find_project_by_project_id, only: [:create]
before_action :find_quantity_by_quantity_id, only: [:toggle_exposure]
#, if: ->{ params[:project_id].present? }
@@ -21,8 +22,9 @@ class TargetsController < ApplicationController
def new
target = @goal.targets.new
target.arity.times { target.thresholds.new }
target.thresholds.new(quantity: Quantity.target.roots.last)
@targets = [target]
@effective_from = target.effective_from
end
def create
@@ -75,6 +77,15 @@ class TargetsController < ApplicationController
prepare_targets
end
def subthresholds
quantity_id = params[:goal][:targets_attributes]
.last[:thresholds_attributes][:quantity_id]
return if quantity_id.blank?
quantity = @project.quantities.find(quantity_id)
@threshold = Threshold.new(quantity: quantity)
end
private
def goal_params

View File

@@ -36,17 +36,18 @@ module BodyTrackersHelper
options_for_select(options, disabled: 0)
end
def quantity_options(domain = :all)
def quantity_options(domain = :except_targets)
Quantity.each_with_ancestors(@project.quantities.send(domain)).map do |ancestors|
quantity = ancestors.last
[
raw("#{'&ensp;' * (ancestors.length-2)}#{quantity.name}"),
raw('&ensp;'*(ancestors.length-2) + quantity.name),
quantity.id,
{'data-path' => ancestors[1..-2].reduce('::') { |m, q| "#{m}#{q.try(:name)}::" }}
]
end
end
# TODO: replace with collection_select and remove
def unit_options
@project.units.map do |u|
[u.shortname, u.id]

View File

@@ -1,8 +1,4 @@
module TargetsHelper
def condition_options
Target::CONDITIONS
end
def action_links(d)
link_to(l(:button_reapply), reapply_project_targets_path(@project, d, @view_params),
{remote: true, class: "icon icon-reload"}) +

View File

@@ -1,6 +1,7 @@
class Formula < ActiveRecord::Base
include BodyTracking::FormulaBuilder
# NOTE: check if model_deps used and merge with quantity_deps if not
attr_reader :parts, :quantity_deps, :model_deps
belongs_to :quantity, inverse_of: :formula, required: true
@@ -107,18 +108,23 @@ class Formula < ActiveRecord::Base
end
quantities = []
identifiers.reject! do |i|
if q_paths.has_key?(i)
q = q_paths[i]
q.nil? ? errors << [:ambiguous_dependency, {identifier: i}] : quantities << q
models = []
identifiers.each do |i|
case
when q_paths.has_key?(i)
if q_paths[i].nil?
errors << [:ambiguous_dependency, {identifier: i}]
else
quantities << q_paths[i]
end
when quantity.target? && (i.casecmp('Value') == 0)
when model = i.safe_constantize
models << model
else
errors << [:unknown_dependency, {identifier: i}]
end
end
models = identifiers.map(&:safe_constantize).compact || []
(identifiers - models.map(&:class_name)).each do |i|
errors << [:unknown_dependency, {identifier: i}]
end
@parts, @quantity_deps, @model_deps = parts, quantities, models if errors.empty?
errors
end

View File

@@ -1,11 +1,13 @@
class Goal < ActiveRecord::Base
belongs_to :project, required: true
has_many :targets, -> { order "effective_from DESC" }, inverse_of: :goal,
has_many :targets, -> { order effective_from: :desc }, inverse_of: :goal,
dependent: :destroy, extend: BodyTracking::ItemsWithQuantities
has_many :target_exposures, as: :view, dependent: :destroy,
class_name: 'Exposure', extend: BodyTracking::TogglableExposures
has_many :quantities, -> { order "lft" }, through: :target_exposures
accepts_nested_attributes_for :targets, allow_destroy: true,
reject_if: proc { |attrs| attrs['quantity_id'].blank? }
validates :target_exposures, presence: true, unless: :is_binding?
validates :is_binding, uniqueness: {scope: :project_id}, if: :is_binding?
validates :name, presence: true, uniqueness: {scope: :project_id},

View File

@@ -2,18 +2,21 @@ class Quantity < ActiveRecord::Base
enum domain: {
diet: 0,
measurement: 1,
exercise: 2
exercise: 2,
target: 3
}
acts_as_nested_set dependent: :destroy, scope: :project
belongs_to :project, inverse_of: :quantities, required: false
has_many :nutrients, dependent: :restrict_with_error
has_many :readouts, dependent: :restrict_with_error
has_many :targets, 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
scope :defaults, -> { where(project: nil) }
scope :except_targets, -> { where.not(domain: :target) }
has_one :formula, inverse_of: :quantity, dependent: :destroy, validate: true
accepts_nested_attributes_for :formula, allow_destroy: true,

View File

@@ -1,37 +1,37 @@
class Target < ActiveRecord::Base
CONDITIONS = ['<', '<=', '>', '>=', '==']
belongs_to :goal, inverse_of: :targets, required: true
belongs_to :quantity, 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
has_many :thresholds, -> { joins(:quantity).order(:lft) },
as: :registry, inverse_of: :target, dependent: :destroy, validate: true
validates :thresholds, presence: true
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.length == arity
errors.add(:thresholds, :quantity_mismatch) if thresholds.to_a.uniq(&:quantity).length != 1
quantities = thresholds.map(&:quantity)
ancestors = quantities.max_by(:lft).self_and_ancestors
errors.add(:thresholds, :count_mismatch) unless quantities.length == ancestors.length
errors.add(:thresholds, :quantity_mismatch) unless quantities == ancestors
end
validates :condition, inclusion: {in: CONDITIONS}
validates :scope, inclusion: {in: [:ingredient, :meal, :day],
if: -> { thresholds.first.quantity.domain == :diet }}
validates :scope, inclusion: {in: [:ingredient, :meal, :day], if: -> { quantity.diet? }}
validates :effective_from, presence: {if: :is_binding?}, absence: {unless: :is_binding?}
after_initialize do
if new_record?
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
delegate :is_binding?, to: :goal
# NOTE: remove if not used in controller
def arity
BigDecimal.method(condition).arity
thresholds.size
end
def to_s
"#{condition} #{thresholds.first.value} [#{thresholds.first.unit.shortname}]"
thresholds.last.quantity.description %
thresholds.map { |t| [t.quantity.name, "#{t.value} [#{t.unit.shortname}]"] }.to_h
end
end

View File

@@ -1,21 +1,20 @@
<label for="goal_id"><%= l(:field_goal) %><span class="required"> *</span></label>
<%= select_tag :goal_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),
<%= goal_f.collection_select :id, @project.goals, :id, :name,
{include_blank: false, required: true, label: :field_goal},
autocomplete: 'off',
onchange: "$.ajax({
url: '#{goal_path(id: :goal_id)}'
.replace('goal_id', $('#target_goal_attributes_id').val()),
dataType: 'script'
});
return false;" %>
<%= link_to l(:button_add), '#',
onclick: "var goal_id = $('#target_goal_attributes_id').val();
$.ajax({
url: '#{edit_goal_path(id: :goal_id)}'.replace('goal_id', goal_id),
onclick: "$.ajax({
url: '#{edit_goal_path(id: :goal_id)}'
.replace('goal_id', $('#target_goal_attributes_id').val()),
dataType: 'script'
});
return false;",
class: 'icon icon-add' %>
<% if @goal.description? %>
<p style='white-space: pre-wrap;' ><%= @goal.description %></p>
<% if goal_f.object.description? %>
<p style='white-space: pre-wrap;' ><%= goal_f.object.description %></p>
<% end %>

View File

@@ -5,8 +5,7 @@
{autocomplete: 'off',
data: {remote: true,
url: parents_project_quantities_path(@project),
params: "form=#{f.options[:html][:id]}"}}
%></p>
params: "form=#{f.options[:html][:id]}"}} %></p>
<p><%= f.select :parent_id, parent_options(@quantity.domain),
{required: true, label: :field_parent_quantity, include_blank: t('.null_parent')} %></p>
<p><%= f.text_field :name, size: 25, required: true %></p>

View File

@@ -2,9 +2,10 @@
<div class="box">
<div id='goal-form' class="tabular">
<% if @goal.persisted? %>
<p><%= render partial: 'goals/show_form' %></p>
<p><%= f.date_field :effective_from, disabled: !@goal.is_binding? %></p>
<% if goal_f.object.persisted? %>
<p><%= render partial: 'goals/show_form', locals: {goal_f: goal_f} %></p>
<p><%= goal_f.date_field :effective_from, value: @effective_from,
disabled: !goal_f.object.is_binding? %></p>
<% else %>
<%= render partial: 'goals/form' %>
<% end %>
@@ -13,34 +14,30 @@
<hr style="width: 95%;">
<div class="tabular">
<% @targets.each do |target| %>
<p class="target">
<%= f.fields_for 'targets', target, index: '' do |target_f| %>
<%= target_f.hidden_field :id %>
<%= target_f.hidden_field :_destroy %>
<em class="info"><%= t ".choose_quantity" %></em>
<% 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',
style: (@targets.many? ? "" : "display:none"),
onclick: "deleteTarget(); return false;" %>
<p class="target">
<%= goal_f.fields_for :targets, @targets, child_index: '' do |target_f| %>
<%= target_f.hidden_field :_destroy %>
<em class="info"><%= t ".choose_quantity" %></em>
<%= target_f.select :quantity_id, quantity_options,
{include_blank: true, required: true, label: :field_target},
onchange: "showQuantityPath(event);" %>
<%= target_f.fields_for :thresholds do |threshold_f| %>
<%= render partial: 'thresholds/form', locals: {threshold_f: threshold_f} %>
<% end %>
</p>
<% last_quantity = target_f.object.thresholds.last.quantity %>
<% unless last_quantity.leaf? %>
<%= target_f.fields_for 'thresholds_attributes', Threshold.new do |threshold_f| %>
<%= render partial: 'thresholds/form',
locals: {threshold_f: threshold_f, parent: last_quantity} %>
<% end %>
<% end %>
<%= link_to t(".button_delete_target"), '#', class: 'icon icon-del',
style: (@targets.many? ? "" : "display:none"),
onclick: "deleteTarget(); return false;" %>
<% end %>
</p>
<p>
<%= link_to t(".button_new_target"), '#', class: 'icon icon-add',
onclick: 'newTarget(); return false;' %>
@@ -50,10 +47,11 @@
<%= javascript_tag do %>
function showQuantityPath(event) {
$(event.target).prevAll('em').text($('option:selected', event.target).attr('data-path'));
$(event.target).prevAll('em').text($('option:selected', event.target)
.attr('data-path'));
}
$(document).ajaxComplete(function() {
$('select[id$=__quantity_id]').trigger(jQuery.Event('change'));
$('p.target select:first-child[id$=__quantity_id]').trigger(jQuery.Event('change'));
})
function newTarget() {

View File

@@ -1,11 +1,11 @@
<h2><%= t ".heading_new_target" %></h2>
<%= labelled_form_for @targets,
<%= labelled_form_for @goal,
url: project_targets_path(@project, @view_params),
remote: true,
html: {id: 'new-target-form', name: 'new-target-form'} do |f| %>
html: {id: 'new-target-form', name: 'new-target-form'} do |goal_f| %>
<%= render partial: 'targets/form', locals: {f: f} %>
<%= render partial: 'targets/form', locals: {goal_f: goal_f} %>
<div class="tabular">
<p>

View File

@@ -0,0 +1,5 @@
<% if @threshold %>
$('#targets').html('<%= j render partial: 'targets/index' %>');
<% else %>
$(event.target).empty();
<% end %>

View File

@@ -0,0 +1,15 @@
<% threshold_q = threshold_f.object.quantity %>
<% parent_id = defined?(parent) ? parent.id : threshold_q.parent_id %>
<%= threshold_f.collection_select :quantity_id,
@project.quantities.target.children_of(parent_id),
:id, :name,
{prompt: parent_id.nil? ? false : '.', required: true, no_label: true},
{autocomplete: 'off', data: {remote: true,
url: subthresholds_quantity_path(@project)}} %>
<% unless threshold_q.nil? %>
<%= threshold_f.hidden_field :_destroy %>
<%= threshold_f.number_field :value, {size: 8, step: :any, no_label: true} %>
<%= threshold_f.collection_select :unit_id, @project.units, :id, :shortname,
{no_label: true} %>
<% end %>