Added MealsController#new and form autocomplete for Food
Renamed QuantityColumn -> Exposure
This commit is contained in:
parent
8e8160c41a
commit
e78803e474
@ -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 = []
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
5
app/helpers/meals_helper.rb
Normal file
5
app/helpers/meals_helper.rb
Normal 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
4
app/models/exposure.rb
Normal file
@ -0,0 +1,4 @@
|
||||
class Exposure < ActiveRecord::Base
|
||||
belongs_to :view, polymorphic: true
|
||||
belongs_to :quantity
|
||||
end
|
@ -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
8
app/models/ingredient.rb
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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|
|
||||
|
@ -1,4 +0,0 @@
|
||||
class QuantityColumn < ActiveRecord::Base
|
||||
belongs_to :column_view, polymorphic: true
|
||||
belongs_to :quantity
|
||||
end
|
1
app/views/foods/autocomplete.json.erb
Normal file
1
app/views/foods/autocomplete.json.erb
Normal file
@ -0,0 +1 @@
|
||||
<%= raw @foods.map { |f| {id: f.id, label: f.name, value: f.name} }.to_json %>
|
4
app/views/meals/_contextual.html.erb
Normal file
4
app/views/meals/_contextual.html.erb
Normal 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 %>
|
79
app/views/meals/_form.html.erb
Normal file
79
app/views/meals/_form.html.erb
Normal 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 %>
|
21
app/views/meals/_index.html.erb
Normal file
21
app/views/meals/_index.html.erb
Normal 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 %>
|
18
app/views/meals/_new_form.html.erb
Normal file
18
app/views/meals/_new_form.html.erb
Normal 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>
|
0
app/views/meals/_show.html.erb
Normal file
0
app/views/meals/_show.html.erb
Normal file
11
app/views/meals/index.html.erb
Normal file
11
app/views/meals/index.html.erb
Normal 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>
|
2
app/views/meals/new.js.erb
Normal file
2
app/views/meals/new.js.erb
Normal file
@ -0,0 +1,2 @@
|
||||
$('#new-meal')
|
||||
.html('<%= j render partial: 'meals/new_form' %>');
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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'
|
||||
|
@ -28,6 +28,7 @@ resources :projects, shallow: true do
|
||||
get 'nutrients'
|
||||
post 'toggle_column'
|
||||
get 'filter'
|
||||
get 'autocomplete'
|
||||
post 'import'
|
||||
end
|
||||
end
|
||||
|
@ -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
|
||||
|
2
init.rb
2
init.rb
@ -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],
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user