From e78803e474b3f4328bc74402c86413094a0badf8 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Wed, 15 Apr 2020 23:42:58 +0200 Subject: [PATCH] Added MealsController#new and form autocomplete for Food Renamed QuantityColumn -> Exposure --- app/controllers/foods_controller.rb | 8 ++- app/controllers/meals_controller.rb | 11 +++ app/controllers/measurements_controller.rb | 4 +- app/controllers/quantities_controller.rb | 2 +- app/helpers/meals_helper.rb | 5 ++ app/models/exposure.rb | 4 ++ app/models/food.rb | 3 + app/models/ingredient.rb | 8 +++ app/models/meal.rb | 15 +++- app/models/measurement_routine.rb | 6 +- app/models/quantity.rb | 2 +- app/models/quantity_column.rb | 4 -- app/views/foods/autocomplete.json.erb | 1 + app/views/meals/_contextual.html.erb | 4 ++ app/views/meals/_form.html.erb | 79 ++++++++++++++++++++++ app/views/meals/_index.html.erb | 21 ++++++ app/views/meals/_new_form.html.erb | 18 +++++ app/views/meals/_show.html.erb | 0 app/views/meals/index.html.erb | 11 +++ app/views/meals/new.js.erb | 2 + app/views/quantities/_index.html.erb | 4 +- assets/stylesheets/body_tracking.css | 3 + config/locales/en.yml | 23 ++++++- config/routes.rb | 1 + db/migrate/001_create_schema.rb | 6 +- init.rb | 2 +- lib/body_tracking/project_patch.rb | 6 +- 27 files changed, 227 insertions(+), 26 deletions(-) create mode 100644 app/helpers/meals_helper.rb create mode 100644 app/models/exposure.rb create mode 100644 app/models/ingredient.rb delete mode 100644 app/models/quantity_column.rb create mode 100644 app/views/foods/autocomplete.json.erb create mode 100644 app/views/meals/_contextual.html.erb create mode 100644 app/views/meals/_form.html.erb create mode 100644 app/views/meals/_index.html.erb create mode 100644 app/views/meals/_new_form.html.erb create mode 100644 app/views/meals/_show.html.erb create mode 100644 app/views/meals/index.html.erb create mode 100644 app/views/meals/new.js.erb diff --git a/app/controllers/foods_controller.rb b/app/controllers/foods_controller.rb index bbacaff..efa84c2 100644 --- a/app/controllers/foods_controller.rb +++ b/app/controllers/foods_controller.rb @@ -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 = [] diff --git a/app/controllers/meals_controller.rb b/app/controllers/meals_controller.rb index cc81507..3bc20be 100644 --- a/app/controllers/meals_controller.rb +++ b/app/controllers/meals_controller.rb @@ -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 diff --git a/app/controllers/measurements_controller.rb b/app/controllers/measurements_controller.rb index 57882bc..be996d9 100644 --- a/app/controllers/measurements_controller.rb +++ b/app/controllers/measurements_controller.rb @@ -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 diff --git a/app/controllers/quantities_controller.rb b/app/controllers/quantities_controller.rb index 85ca764..73af8be 100644 --- a/app/controllers/quantities_controller.rb +++ b/app/controllers/quantities_controller.rb @@ -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 diff --git a/app/helpers/meals_helper.rb b/app/helpers/meals_helper.rb new file mode 100644 index 0000000..1a49f0a --- /dev/null +++ b/app/helpers/meals_helper.rb @@ -0,0 +1,5 @@ +module MealsHelper + def action_links(m) + delete_link(meal_path(m), {remote: true, data: {}}) if m.persisted? + end +end diff --git a/app/models/exposure.rb b/app/models/exposure.rb new file mode 100644 index 0000000..f56fabd --- /dev/null +++ b/app/models/exposure.rb @@ -0,0 +1,4 @@ +class Exposure < ActiveRecord::Base + belongs_to :view, polymorphic: true + belongs_to :quantity +end diff --git a/app/models/food.rb b/app/models/food.rb index f06490d..d3b044c 100644 --- a/app/models/food.rb +++ b/app/models/food.rb @@ -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 diff --git a/app/models/ingredient.rb b/app/models/ingredient.rb new file mode 100644 index 0000000..ae9a171 --- /dev/null +++ b/app/models/ingredient.rb @@ -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 diff --git a/app/models/meal.rb b/app/models/meal.rb index 9e5bd52..9fea6ae 100644 --- a/app/models/meal.rb +++ b/app/models/meal.rb @@ -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 diff --git a/app/models/measurement_routine.rb b/app/models/measurement_routine.rb index 04f304a..44f05d7 100644 --- a/app/models/measurement_routine.rb +++ b/app/models/measurement_routine.rb @@ -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 diff --git a/app/models/quantity.rb b/app/models/quantity.rb index 83d9041..1224801 100644 --- a/app/models/quantity.rb +++ b/app/models/quantity.rb @@ -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| diff --git a/app/models/quantity_column.rb b/app/models/quantity_column.rb deleted file mode 100644 index 3d0b620..0000000 --- a/app/models/quantity_column.rb +++ /dev/null @@ -1,4 +0,0 @@ -class QuantityColumn < ActiveRecord::Base - belongs_to :column_view, polymorphic: true - belongs_to :quantity -end diff --git a/app/views/foods/autocomplete.json.erb b/app/views/foods/autocomplete.json.erb new file mode 100644 index 0000000..3360e02 --- /dev/null +++ b/app/views/foods/autocomplete.json.erb @@ -0,0 +1 @@ +<%= raw @foods.map { |f| {id: f.id, label: f.name, value: f.name} }.to_json %> diff --git a/app/views/meals/_contextual.html.erb b/app/views/meals/_contextual.html.erb new file mode 100644 index 0000000..15f2a49 --- /dev/null +++ b/app/views/meals/_contextual.html.erb @@ -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 %> diff --git a/app/views/meals/_form.html.erb b/app/views/meals/_form.html.erb new file mode 100644 index 0000000..b7e99f7 --- /dev/null +++ b/app/views/meals/_form.html.erb @@ -0,0 +1,79 @@ +<%= error_messages_for @meal %> + +
+
+ <% @meal.ingredients.each_with_index do |i, index| %> + + <%= f.fields_for 'ingredients_attributes', i, index: '' do |ff| %> + + + + + <% end %> +
+

+ <%= 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 %> +

+
+ <%= link_to t(".button_delete_ingredient"), '#', + class: 'icon icon-del', + style: (@meal.ingredients.length > 1 ? "" : "display:none"), + onclick: "deleteIngredient(); return false;" %> +
+ <% end %> +

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

+
+
+ +<%= 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 %> diff --git a/app/views/meals/_index.html.erb b/app/views/meals/_index.html.erb new file mode 100644 index 0000000..a30017e --- /dev/null +++ b/app/views/meals/_index.html.erb @@ -0,0 +1,21 @@ +<% if @meals.any? { |m| m.persisted? } %> + + + <% @meals.group_by { |m| m.eaten_at.date if m.eaten_at }.each do |d, meals| %> + <% meals.each_with_index do |m, index| %> + + + + + <% end %> + <% end %> + +
+

+ <%= "#{t '.label_meal'}" %><%= index ? " ##{index+1}" : " (new)" %> + <%= ", #{m.eaten_at.time}" if m.eaten_at %> +

+
<%= action_links(m) %>
+<% else %> +

<%= l(:label_no_data) %>

+<% end %> diff --git a/app/views/meals/_new_form.html.erb b/app/views/meals/_new_form.html.erb new file mode 100644 index 0000000..d1b5b77 --- /dev/null +++ b/app/views/meals/_new_form.html.erb @@ -0,0 +1,18 @@ +

<%= t ".heading_new_meal" %>

+ +<%= 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} %> + +
+

+ <%= submit_tag l(:button_create) %> + <%= link_to l(:button_cancel), "#", + onclick: '$("#new-meal").empty(); return false;' %> +

+
+<% end %> +
diff --git a/app/views/meals/_show.html.erb b/app/views/meals/_show.html.erb new file mode 100644 index 0000000..e69de29 diff --git a/app/views/meals/index.html.erb b/app/views/meals/index.html.erb new file mode 100644 index 0000000..12a6749 --- /dev/null +++ b/app/views/meals/index.html.erb @@ -0,0 +1,11 @@ +
+ <%= render partial: 'meals/contextual' %> +
+ +
+
+ +

<%= t ".heading" %>

+
+ <%= render partial: 'meals/index' %> +
diff --git a/app/views/meals/new.js.erb b/app/views/meals/new.js.erb new file mode 100644 index 0000000..e24f70c --- /dev/null +++ b/app/views/meals/new.js.erb @@ -0,0 +1,2 @@ +$('#new-meal') + .html('<%= j render partial: 'meals/new_form' %>'); diff --git a/app/views/quantities/_index.html.erb b/app/views/quantities/_index.html.erb index 280d64f..0feb4e8 100644 --- a/app/views/quantities/_index.html.erb +++ b/app/views/quantities/_index.html.erb @@ -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}" %> -
+
<%= q.name %>
diff --git a/assets/stylesheets/body_tracking.css b/assets/stylesheets/body_tracking.css index 852f502..6abefc6 100644 --- a/assets/stylesheets/body_tracking.css +++ b/assets/stylesheets/body_tracking.css @@ -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; diff --git a/config/locales/en.yml b/config/locales/en.yml index e7bfac3..1e1c612 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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' diff --git a/config/routes.rb b/config/routes.rb index 26af335..71e084d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -28,6 +28,7 @@ resources :projects, shallow: true do get 'nutrients' post 'toggle_column' get 'filter' + get 'autocomplete' post 'import' end end diff --git a/db/migrate/001_create_schema.rb b/db/migrate/001_create_schema.rb index b4bd8d9..df697c7 100644 --- a/db/migrate/001_create_schema.rb +++ b/db/migrate/001_create_schema.rb @@ -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 diff --git a/init.rb b/init.rb index 7ed1935..a0e8b5b 100644 --- a/init.rb +++ b/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], diff --git a/lib/body_tracking/project_patch.rb b/lib/body_tracking/project_patch.rb index 3db3811..dfe9666 100644 --- a/lib/body_tracking/project_patch.rb +++ b/lib/body_tracking/project_patch.rb @@ -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