1
0

Add GoalController: #index, #new, #create

This commit is contained in:
cryptogopher 2021-02-21 18:10:15 +01:00
parent ea308a1e4a
commit 7f87b3bc84
20 changed files with 182 additions and 76 deletions

View File

@ -1,12 +1,60 @@
class GoalsController < ApplicationController
layout 'body_tracking', except: :subthresholds
menu_item :body_trackers
helper :body_trackers
include Concerns::Finders
before_action :find_project_by_project_id, only: [:index, :new, :create]
before_action :find_goal, only: [:show, :edit]
before_action :authorize
def index
@goals = @project.goals
end
def new
@goal = @project.goals.new
@targets = @goal.targets
end
def create
@goal = @project.goals.new(goal_params)
if @goal.save
flash.now[:notice] = 'Created new goal'
@goals = @project.goals
else
@targets = @goal.targets
render :new
end
end
def show
end
def edit
end
private
def goal_params
params.require(:goal).permit(
:name,
:description,
targets_attributes:
[
:id,
:quantity_id,
:scope,
:destroy,
thresholds_attributes: [
:id,
:quantity_id,
:value,
:unit_id,
:_destroy
]
]
)
end
end

View File

@ -33,10 +33,6 @@ class TargetsController < ApplicationController
target.effective_from = params[:target][:effective_from]
end
if @goal.target_exposures.empty?
@goal.quantities << @targets.map(&:quantity)[0..5]
end
# :save only after build, to re-display values in case records are invalid
if @goal.save
flash.now[:notice] = 'Created new target(s)'
@ -85,13 +81,6 @@ class TargetsController < ApplicationController
private
def goal_params
params.require(:goal).permit(
:name,
:description
)
end
def targets_params
params.require(:goal).permit(
targets_attributes:

View File

@ -6,18 +6,22 @@ class Goal < ActiveRecord::Base
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?
accepts_nested_attributes_for :targets, allow_destroy: true
validates :is_binding, uniqueness: {scope: :project_id}, if: :is_binding?
validates :name, presence: true, uniqueness: {scope: :project_id},
exclusion: {in: [I18n.t('targets.form.binding_goal')], unless: :is_binding?}
exclusion: {in: [I18n.t('goals.binding.name')], unless: :is_binding?}
after_initialize do
if new_record?
self.is_binding = false if self.is_binding.nil?
self.targets.new if self.targets.empty?
end
end
before_save do
quantities << targets.map(&:quantity)[0..5] if target_exposures.empty?
end
before_destroy prepend: true do
!is_binding?
end

View File

@ -5,7 +5,7 @@ class QuantityValue < ActiveRecord::Base
# 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, -> { where(domain: DOMAIN) }, required: true
belongs_to :quantity, ->(qv) { where(domain: qv.class::DOMAIN) }, required: true
belongs_to :unit, required: true
# Uniqueness is checked exclusively on the other end of association level.

View File

@ -11,17 +11,20 @@ class Target < ActiveRecord::Base
reject_if: proc { |attrs| attrs['quantity_id'].blank? && attrs['value'].blank? }
validate do
quantities = thresholds.map(&:quantity)
ancestors = quantities.max_by(:lft).self_and_ancestors
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 :scope, inclusion: {in: [:ingredient, :meal, :day], if: -> { quantity.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?
# Target should be only instantiated through Goal, so :is_binding? will be available
self.effective_from ||= Date.current if is_binding?
if self.thresholds.empty?
self.thresholds.new(quantity: self.goal.project.quantities.target.first)
end
end
end

View File

@ -1,13 +1,10 @@
<div class="tabular">
<%= fields_for 'measurement[routine_attributes]', routine do |ff| %>
<%= ff.hidden_field :id %>
<p>
<label><%= l(:field_name) %><span class="required"> *</span></label>
<%= ff.text_field :name, required: true, style: "width: 95%;" %>
</p>
<p>
<label><%= l(:field_description) %></label>
<%= ff.text_area :description, cols: 40, rows: 3, style: "width: 95%;" %>
</p>
<% end %>
<%= error_messages_for @goal %>
<div class="box tabular">
<p><%= goal_f.text_field :name, required: true, style: "width: 95%;" %></p>
<p><%= goal_f.text_area :description, rows: 3, style: "width: 95%;" %></p>
<hr style="width: 95%;">
<%= render partial: 'targets/form', locals: {goal_f: goal_f} %>
</div>

View File

@ -0,0 +1,32 @@
<% if @goals.any? %>
<table id="goals" class="list odd-even">
<thead>
<tr>
<th><%= l(:field_name) %></th>
<th><%= l(:field_description) %></th>
<th style="width:5%"><%= l(:field_action) %></th>
</tr>
</thead>
<tbody>
<% @goals.each do |g| %>
<tr id="goal-<%= g.id %>" class="primary goal">
<td class="name unwrappable">
<div style="float:left;">
<%= checked_image g.is_binding %><%= link_to g.name, g %>
</div>
<div style="float:right;">
<%# TODO: display # of active targets/targeted quantities %>
<small><%#= " (#{pluralize(m.readouts.size, 'readout')})" %></small>
</div>
</td>
<td class="description ellipsible"><%= g.description %></td>
<td class="action unwrappable">
<%= delete_link(g, {remote: true, data: {}}) unless g.is_binding %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p class="nodata"><%= l(:label_no_data) %></p>
<% end %>

View File

@ -0,0 +1,16 @@
<h2><%= t ".heading_new_goal" %></h2>
<%= labelled_form_for [@project, @goal], remote: true,
html: {id: 'new-goal-form', name: 'new-goal-form'} do |goal_f| %>
<%= render partial: 'goals/form', locals: {goal_f: goal_f} %>
<div class="tabular">
<p>
<%= submit_tag l(:button_create) %>
<%= link_to l(:button_cancel), "#",
onclick: "$('#new-goal').empty(); return false;" %>
</p>
</div>
<% end %>
<hr>

View File

@ -0,0 +1,2 @@
$('#new-goal').empty();
$('#goals').html('<%= j render partial: 'goals/index' %>');

View File

@ -0,0 +1,13 @@
<div class="contextual">
<%= link_to_if User.current.allowed_to?(:manage_body_trackers, @project),
t(".link_new_goal"), new_project_goal_path(@project),
{remote: true, class: 'icon icon-add'} %>
</div>
<div id="new-goal">
</div>
<%= title t(:body_trackers_title), t(".heading") %>
<div id='goals'>
<%= render partial: 'goals/index' %>
</div>

View File

@ -0,0 +1 @@
$('#new-goal').html('<%= j render partial: 'goals/new_form' %>');

View File

@ -17,8 +17,7 @@
<li>
<%= link_to t(".link_targets"), project_targets_path(@project) %>
/
<%#= link_to t(".link_goals"), nutrients_project_foods_path(@project) %>
Goals
<%= link_to t(".link_goals"), project_goals_path(@project) %>
</li>
<li><%= link_to t(".link_quantities"), project_quantities_path(@project) %></li>
<li><%= link_to t(".link_units"), project_units_path(@project) %></li>

View File

@ -1,42 +1,30 @@
<%= error_messages_for *@targets %>
<% if goal_f.object.is_binding? %>
<p>
<%= date_field :effective_from, value: @effective_from %>
</p>
<% end %>
<div class="box">
<div id='goal-form' class="tabular">
<% 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 %>
</div>
<%= goal_f.fields_for :targets, @targets, child_index: '' do |target_f| %>
<p class="target">
<%= 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);" %>
<hr style="width: 95%;">
<%= render partial: 'targets/thresholds_form',
locals: {target_f: target_f,
last_quantity: target_f.object.thresholds.last.quantity} %>
<div class="tabular">
<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);" %>
<%= render partial: 'targets/thresholds_form',
locals: {target_f: target_f,
last_quantity: target_f.object.thresholds.last.quantity} %>
<%= 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;' %>
</p>
</div>
</div>
<%= link_to t(".button_delete_target"), '#', class: 'icon icon-del',
style: (@targets.many? ? "" : "display:none"),
onclick: "deleteTarget(); return false;" %>
</p>
<% end %>
<p>
<%= link_to t(".button_new_target"), '#', class: 'icon icon-add',
onclick: 'newTarget(); return false;' %>
</p>
<%= javascript_tag do %>
function showQuantityPath(event) {

View File

@ -4,7 +4,10 @@
<%= labelled_form_for [@project, @goal], remote: true,
html: {id: 'new-target-form', name: 'new-target-form'} do |goal_f| %>
<%= render partial: 'targets/form', locals: {goal_f: goal_f} %>
<%= error_messages_for *@targets %>
<div class="box tabular">
<%= render partial: 'targets/form', locals: {goal_f: goal_f} %>
</div>
<div class="tabular">
<p>

View File

@ -89,11 +89,20 @@ en:
link_units: 'Units'
link_defaults: 'Load defaults'
confirm_defaults: 'This will load default data sources, quantities and units. Continue?'
goals:
binding:
name: 'Binding goal'
description: 'Targets from this goal are applied throughout application'
form:
new_form:
heading_new_goal: 'New goal'
index:
heading: 'Goals'
link_new_goal: 'New goal'
targets:
contextual:
link_new_target: 'New target'
form:
binding_goal: '- binding -'
choose_quantity: 'Choose quantity'
button_new_target: 'Add target'
button_delete_target: 'Delete'

View File

@ -7,7 +7,7 @@ resources :projects, shallow: true do
post 'defaults'
end
end
resources :goals, only: [:show, :edit] do
resources :goals do
member do
post 'toggle_exposure', controller: :targets
end

View File

@ -15,7 +15,7 @@ Redmine::Plugin.register :body_tracking do
project_module :body_tracking do
permission :view_body_trackers, {
body_trackers: [:index],
goals: [:show],
goals: [:index, :show],
targets: [:index],
meals: [:index],
measurement_routines: [:show],
@ -27,7 +27,7 @@ Redmine::Plugin.register :body_tracking do
}, read: true
permission :manage_body_trackers, {
body_trackers: [:defaults],
goals: [:edit],
goals: [:new, :create, :edit],
targets: [:new, :create, :edit, :update, :destroy, :reapply, :toggle_exposure,
:subthresholds],
meals: [:new, :create, :edit, :update, :destroy, :edit_notes, :update_notes,

View File

@ -32,7 +32,8 @@ module BodyTracking::ProjectPatch
has_many :goals, dependent: :destroy do
def binding
find_or_create_by(is_binding: true) do |goal|
goal.name = I18n.t('targets.form.binding_goal')
goal.name = I18n.t('goals.binding.name')
goal.description = I18n.t('goals.binding.description')
end
end
end

View File

@ -1,7 +1,8 @@
goals_binding:
project_id: 1
is_binding: true
name: "<%= I18n.t 'targets.form.binding_goal' %>"
name: "<%= I18n.t 'goals.binding.name' %>"
description: "<%= I18n.t 'goals.binding.description' %>"
goals_non_binding:
project_id: 1

View File

@ -46,7 +46,7 @@ class TargetsTest < BodyTrackingSystemTestCase
click_link t('targets.contextual.link_new_target')
assert_selector 'form#new-target-form', count: 1
within 'form#new-target-form' do
assert has_select?(t(:field_goal), selected: t('targets.form.binding_goal'))
assert has_select?(t(:field_goal), selected: t('goals.binding.name'))
assert has_field?(t(:field_effective_from), with: Date.current)
assert has_no_link?(t('targets.form.button_delete_target'))
end