1
0

Added MealsController#new and form autocomplete for Food

Renamed QuantityColumn -> Exposure
This commit is contained in:
cryptogopher 2020-04-15 23:42:58 +02:00
parent 8e8160c41a
commit e78803e474
27 changed files with 227 additions and 26 deletions

View File

@ -9,7 +9,7 @@ class FoodsController < ApplicationController
before_action :init_session_filters
before_action :find_project_by_project_id,
only: [:index, :new, :create, :nutrients, :filter, :import]
only: [:index, :new, :create, :nutrients, :filter, :autocomplete, :import]
before_action :find_quantity_by_quantity_id, only: [:toggle_column]
before_action :find_food, only: [:edit, :update, :destroy, :toggle]
before_action :authorize
@ -63,7 +63,7 @@ class FoodsController < ApplicationController
end
def toggle_column
@project.nutrient_columns.toggle!(@quantity)
@project.nutrient_exposures.toggle!(@quantity)
prepare_nutrients
end
@ -73,6 +73,10 @@ class FoodsController < ApplicationController
render :index
end
def autocomplete
@foods = @project.foods.where("name LIKE ?", "%#{params[:term]}%")
end
def import
warnings = []

View File

@ -13,6 +13,17 @@ class MealsController < ApplicationController
prepare_meals
end
def new
@meal = @project.meals.new
@meal.ingredients.new
end
def create
end
def destroy
end
private
def prepare_meals

View File

@ -33,7 +33,7 @@ class MeasurementsController < ApplicationController
@measurement.routine.project = @project
@routine = @measurement.routine
if @measurement.save
if @routine.columns.empty?
if @routine.exposures.empty?
@routine.quantities << @measurement.readouts.map(&:quantity).first(6)
end
@ -83,7 +83,7 @@ class MeasurementsController < ApplicationController
end
def toggle_column
@routine.columns.toggle!(@quantity)
@routine.exposures.toggle!(@quantity)
prepare_readouts
end

View File

@ -122,6 +122,6 @@ class QuantitiesController < ApplicationController
def prepare_quantities
@quantities = @project.quantities.filter(@project, session[:q_filters])
.includes(:columns, :formula, :parent)
.includes(:exposures, :formula, :parent)
end
end

View File

@ -0,0 +1,5 @@
module MealsHelper
def action_links(m)
delete_link(meal_path(m), {remote: true, data: {}}) if m.persisted?
end
end

4
app/models/exposure.rb Normal file
View File

@ -0,0 +1,4 @@
class Exposure < ActiveRecord::Base
belongs_to :view, polymorphic: true
belongs_to :quantity
end

View File

@ -34,6 +34,9 @@ class Food < ActiveRecord::Base
validates :ref_amount, numericality: {greater_than: 0}
validates :group, inclusion: {in: groups.keys}
scope :visible, -> { where(hidden: false) }
scope :hidden, -> { where(hidden: true) }
after_initialize do
if new_record?
self.ref_amount ||= 100

8
app/models/ingredient.rb Normal file
View File

@ -0,0 +1,8 @@
class Ingredient < ActiveRecord::Base
belongs_to :composition, inverse_of: :ingredients, required: true
belongs_to :food, required: true
belongs_to :part_of, required: false
validates :ready_ratio, numericality: {greater_than_or_equal_to: 0.0}
validates :amount, numericality: {greater_than_or_equal_to: 0.0}
end

View File

@ -1,6 +1,19 @@
class Meal < ActiveRecord::Base
belongs_to :project, required: true
has_many :ingredients, as: :composition, dependent: :destroy
has_many :ingredients, as: :composition, dependent: :destroy, validate: true
has_many :foods, through: :ingredients
validates :ingredients, presence: true
accepts_nested_attributes_for :ingredients, allow_destroy: true, reject_if: proc { |attrs|
attrs['food_id'].blank? && attrs['amount'].blank?
}
# Ingredient food_id + part_of_id uniqueness validation. Cannot be effectively
# checked on Ingredient model level.
validate do
ingredients = self.ingredients.reject { |i| i.marked_for_destruction? }
.map { |i| [i.food_id, i.part_of_id] }
if ingredients.length != ingredients.uniq.length
errors.add(:ingredients, :duplicated_ingredient)
end
end
end

View File

@ -3,9 +3,9 @@ class MeasurementRoutine < ActiveRecord::Base
has_many :measurements, -> { order "taken_at DESC" }, inverse_of: :routine,
foreign_key: 'routine_id', dependent: :restrict_with_error,
extend: BodyTracking::ItemsWithQuantities
has_many :readout_columns, as: :column_view, dependent: :destroy,
class_name: 'QuantityColumn', extend: BodyTracking::TogglableColumns
has_many :quantities, -> { order "lft" }, through: :readout_columns
has_many :readout_exposures, as: :view, dependent: :destroy,
class_name: 'Exposure', extend: BodyTracking::TogglableColumns
has_many :quantities, -> { order "lft" }, through: :readout_exposures
validates :name, presence: true, uniqueness: {scope: :project_id}
end

View File

@ -9,7 +9,7 @@ class Quantity < ActiveRecord::Base
belongs_to :project, required: false
has_many :nutrients, dependent: :restrict_with_error
has_many :readouts, dependent: :restrict_with_error
has_many :columns, dependent: :destroy
has_many :exposures, dependent: :destroy
has_one :formula, inverse_of: :quantity, dependent: :destroy, validate: true
accepts_nested_attributes_for :formula, allow_destroy: true, reject_if: proc { |attrs|

View File

@ -1,4 +0,0 @@
class QuantityColumn < ActiveRecord::Base
belongs_to :column_view, polymorphic: true
belongs_to :quantity
end

View File

@ -0,0 +1 @@
<%= raw @foods.map { |f| {id: f.id, label: f.name, value: f.name} }.to_json %>

View File

@ -0,0 +1,4 @@
<% if User.current.allowed_to?(:manage_common, @project) %>
<%= link_to t(".link_new_meal"), new_project_meal_path(@project),
{remote: true, class: 'icon icon-add'} %>
<% end %>

View File

@ -0,0 +1,79 @@
<%= error_messages_for @meal %>
<div class="box">
<div class="tabular">
<% @meal.ingredients.each_with_index do |i, index| %>
<table style="width:95%;">
<%= f.fields_for 'ingredients_attributes', i, index: '' do |ff| %>
<tr class="ingredient">
<td style="width:90%;">
<p>
<%= ff.hidden_field :id %>
<%= ff.text_field :food_id, {class: "autocomplete food-autocomplete",
style: "width: 80%;",
required: true,
label: (index > 0 ? '' : :field_ingredients)} %>
<%= ff.number_field :amount, {style: "width: 8%", step: :any, label: ''} %>
<%= i.food.ref_unit.shortname if i.food %>
<%= ff.hidden_field :_destroy %>
</p>
</td>
<td style="width:10%;">
<%= link_to t(".button_delete_ingredient"), '#',
class: 'icon icon-del',
style: (@meal.ingredients.length > 1 ? "" : "display:none"),
onclick: "deleteIngredient(); return false;" %>
</td>
</tr>
<% end %>
</table>
<% end %>
<p>
<%= link_to t(".button_new_ingredient"), '#', class: 'icon icon-add',
onclick: 'newIngredient(); return false;' %>
</p>
</div>
</div>
<%= javascript_tag do %>
function autocompleteFood($row) {
$row.find('.food-autocomplete').autocomplete({
source: '<%= j autocomplete_project_foods_path(@project) %>',
minLength: 2,
position: {collision: 'flipfit'},
search: function(event){
$(event.target).closest('.food-autocomplete').addClass('ajax-loading');
},
response: function(event){
$(event.target).closest('.food-autocomplete').removeClass('ajax-loading');
}
});
}
autocompleteFood($('tr.ingredient:visible'));
function newIngredient() {
var form = $(event.target).closest('form');
var row = form.find('tr.ingredient:visible:last');
var new_row = row.clone().insertAfter(row);
new_row.find('input[id$=__id], input[id$=__amount], input[id$=__food_id]').val('');
new_row.find('input[id$=__destroy]').val('');
new_row.find('label:first').hide();
form.find('tr.ingredient:visible a.icon-del').show();
autocompleteFood(new_row);
}
function deleteIngredient() {
var form = $(event.target).closest('form');
var row = $(event.target).closest('tr.ingredient');
if (row.find('input[id$=__id]').val()) {
row.hide();
row.find('input[id$=__destroy]').val('1');
} else {
row.remove();
}
form.find('tr.ingredient:visible:first label:first').show();
if (form.find('tr.ingredient:visible').length <= 1) {
form.find('tr.ingredient:visible a.icon-del').hide();
}
}
<% end %>

View File

@ -0,0 +1,21 @@
<% if @meals.any? { |m| m.persisted? } %>
<table id="meals" class="odd-even">
<tbody>
<% @meals.group_by { |m| m.eaten_at.date if m.eaten_at }.each do |d, meals| %>
<% meals.each_with_index do |m, index| %>
<tr id="meal-<%= m.new_record? ? 'new' : m.id %>" class="primary meal">
<td class="name">
<h4>
<%= "#{t '.label_meal'}" %><%= index ? " ##{index+1}" : " (new)" %>
<%= ", #{m.eaten_at.time}" if m.eaten_at %>
</h4>
</td>
<td class="action unwrappable" style="width:5%"><%= action_links(m) %></td>
</tr>
<% end %>
<% end %>
</tbody>
</table>
<% else %>
<p class="nodata"><%= l(:label_no_data) %></p>
<% end %>

View File

@ -0,0 +1,18 @@
<h2><%= t ".heading_new_meal" %></h2>
<%= labelled_form_for @meal,
url: project_meals_path(@project),
remote: true,
html: {id: 'new-meal-form', name: 'new-meal-form'} do |f| %>
<%= render partial: 'meals/form', locals: {f: f} %>
<div class="tabular">
<p>
<%= submit_tag l(:button_create) %>
<%= link_to l(:button_cancel), "#",
onclick: '$("#new-meal").empty(); return false;' %>
</p>
</div>
<% end %>
<hr>

View File

View File

@ -0,0 +1,11 @@
<div class="contextual">
<%= render partial: 'meals/contextual' %>
</div>
<div id="new-meal">
</div>
<h2><%= t ".heading" %></h2>
<div id='meals'>
<%= render partial: 'meals/index' %>
</div>

View File

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

View File

@ -18,12 +18,12 @@
<%
next if q.new_record?
quantity_class = "quantity"
quantity_class += " primary" unless q.columns.empty?
quantity_class += " primary" unless q.exposures.empty?
quantity_class += " project idnt idnt-#{level+1}"
%>
<tr id="quantity-<%= q.id %>" class="<%= quantity_class %>">
<td class="name unwrappable">
<div class="icon <%= q.columns.empty? ? 'icon-fav-off' : 'icon-fav' %>">
<div class="icon <%= q.exposures.empty? ? 'icon-fav-off' : 'icon-fav' %>">
<%= q.name %>
</div>
</td>

View File

@ -32,10 +32,13 @@ fieldset#filters table.filter td {padding-left: 8px;}
.icon-bullet-open { background-image: url(../../../images/bullet_toggle_minus.png); }
.icon-bullet-closed { background-image: url(../../../images/bullet_toggle_plus.png); }
.ui-state-focus, .ui-widget-content .ui-state-focus { font-weight: normal; }
input[type=number] {
-moz-appearance:textfield;
text-align: right;
height: 1.5em;
}
input[type=date], input[type=time], input[type=number], textarea {
border:1px solid #d7d7d7;

View File

@ -1,6 +1,7 @@
# English strings go here for Rails i18n
en:
body_trackers_menu_caption: 'Body trackers'
field_ingredients: 'Ingredients'
field_measurement_routine: 'Routine'
field_readouts: 'Readouts'
field_taken_at_date: 'Taken at'
@ -22,10 +23,14 @@ en:
activerecord:
errors:
models:
meal:
attributes:
ingredients:
duplicated_ingredient: 'each ingredient can only be specified once per meal'
measurement:
attributes:
readouts:
duplicated_quantity_unit_pair: 'you can define each quantity/unit pair only
duplicated_quantity_unit_pair: 'each quantity+unit pair can only be specified
once per measurement'
food:
attributes:
@ -55,15 +60,27 @@ en:
heading_diet: 'Diet'
heading_common: 'Common'
link_summary: 'Summary'
link_meals: 'Meals'
link_measurements: 'Measurements'
link_meals: 'Meals'
link_foods: 'Foods'
link_nutrients: 'Nutrients'
link_sources: 'Data sources'
link_quantities: 'Quantities'
link_units: 'Units'
link_defaults: 'Load defaults'
confirm_defaults: 'This will load default quantities and units. Continue?'
confirm_defaults: 'This will load default data sources, quantities and units. Continue?'
meals:
contextual:
link_new_meal: 'New meal'
form:
button_new_ingredient: 'Add ingredient'
button_delete_ingredient: 'Delete'
new_form:
heading_new_meal: 'New meal'
index:
heading: 'Meals'
show:
label_meal: 'Meal'
measurements:
contextual:
link_new_measurement: 'New measurement'

View File

@ -28,6 +28,7 @@ resources :projects, shallow: true do
get 'nutrients'
post 'toggle_column'
get 'filter'
get 'autocomplete'
post 'import'
end
end

View File

@ -27,8 +27,8 @@ class CreateSchema < ActiveRecord::Migration
t.timestamps null: false
end
create_table :quantity_columns do |t|
t.references :column_view, polymorphic: true
create_table :exposures do |t|
t.references :view, polymorphic: true
t.references :quantity
end
@ -94,9 +94,9 @@ class CreateSchema < ActiveRecord::Migration
create_table :ingredients do |t|
t.references :composition, polymorphic: true
t.references :food
t.decimal :amount, precision: 12, scale: 6
t.references :part_of
t.decimal :ready_ratio, precision: 12, scale: 6
t.decimal :amount, precision: 12, scale: 6
t.timestamps null: false
end
end

View File

@ -16,7 +16,7 @@ Redmine::Plugin.register :body_tracking do
meals: [:index],
measurement_routines: [:show],
measurements: [:index, :readouts, :filter],
foods: [:index, :nutrients, :filter],
foods: [:index, :nutrients, :filter, :autocomplete],
sources: [:index],
quantities: [:index, :parents, :filter],
units: [:index],

View File

@ -12,9 +12,9 @@ module BodyTracking::ProjectPatch
has_many :quantities, -> { order "lft" }, dependent: :destroy
has_many :units, dependent: :destroy
has_many :nutrient_columns, as: :column_view, dependent: :destroy,
class_name: 'QuantityColumn', extend: BodyTracking::TogglableColumns
has_many :nutrient_quantities, -> { order "lft" }, through: :nutrient_columns,
has_many :nutrient_exposures, as: :view, dependent: :destroy,
class_name: 'Exposure', extend: BodyTracking::TogglableColumns
has_many :nutrient_quantities, -> { order "lft" }, through: :nutrient_exposures,
source: 'quantity'
end
end