Added sorting columns to recipe index
This commit is contained in:
parent
779cdb6173
commit
36cbf92df0
2
Gemfile
2
Gemfile
@ -12,7 +12,7 @@ gem 'therubyracer', platforms: :ruby
|
|||||||
# Use jquery as the JavaScript library
|
# Use jquery as the JavaScript library
|
||||||
gem 'jquery-rails', '~> 4.1.1'
|
gem 'jquery-rails', '~> 4.1.1'
|
||||||
gem 'bootstrap-sass', '~> 3.3.6'
|
gem 'bootstrap-sass', '~> 3.3.6'
|
||||||
# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
|
gem 'kaminari', '~> 0.17.0'
|
||||||
gem 'turbolinks', '~> 5.0.0'
|
gem 'turbolinks', '~> 5.0.0'
|
||||||
gem 'jbuilder', '~> 2.5'
|
gem 'jbuilder', '~> 2.5'
|
||||||
gem 'cocoon', '~> 1.2.9'
|
gem 'cocoon', '~> 1.2.9'
|
||||||
|
10
Gemfile.lock
10
Gemfile.lock
@ -39,7 +39,7 @@ GEM
|
|||||||
minitest (~> 5.1)
|
minitest (~> 5.1)
|
||||||
tzinfo (~> 1.1)
|
tzinfo (~> 1.1)
|
||||||
arel (7.1.2)
|
arel (7.1.2)
|
||||||
autoprefixer-rails (6.4.1.1)
|
autoprefixer-rails (6.5.0)
|
||||||
execjs
|
execjs
|
||||||
bcrypt (3.1.11)
|
bcrypt (3.1.11)
|
||||||
blankslate (3.1.3)
|
blankslate (3.1.3)
|
||||||
@ -87,6 +87,9 @@ GEM
|
|||||||
rails-dom-testing (>= 1, < 3)
|
rails-dom-testing (>= 1, < 3)
|
||||||
railties (>= 4.2.0)
|
railties (>= 4.2.0)
|
||||||
thor (>= 0.14, < 2.0)
|
thor (>= 0.14, < 2.0)
|
||||||
|
kaminari (0.17.0)
|
||||||
|
actionpack (>= 3.0.0)
|
||||||
|
activesupport (>= 3.0.0)
|
||||||
libv8 (3.16.14.15)
|
libv8 (3.16.14.15)
|
||||||
liner (0.2.4)
|
liner (0.2.4)
|
||||||
listen (3.1.5)
|
listen (3.1.5)
|
||||||
@ -105,7 +108,7 @@ GEM
|
|||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2016.0521)
|
mime-types-data (3.2016.0521)
|
||||||
mini_portile2 (2.1.0)
|
mini_portile2 (2.1.0)
|
||||||
minitest (5.9.0)
|
minitest (5.9.1)
|
||||||
multi_json (1.12.1)
|
multi_json (1.12.1)
|
||||||
nenv (0.3.0)
|
nenv (0.3.0)
|
||||||
nio4r (1.2.1)
|
nio4r (1.2.1)
|
||||||
@ -149,7 +152,7 @@ GEM
|
|||||||
method_source
|
method_source
|
||||||
rake (>= 0.8.7)
|
rake (>= 0.8.7)
|
||||||
thor (>= 0.18.1, < 2.0)
|
thor (>= 0.18.1, < 2.0)
|
||||||
rake (11.2.2)
|
rake (11.3.0)
|
||||||
rb-fsevent (0.9.7)
|
rb-fsevent (0.9.7)
|
||||||
rb-inotify (0.9.7)
|
rb-inotify (0.9.7)
|
||||||
ffi (>= 0.5.0)
|
ffi (>= 0.5.0)
|
||||||
@ -235,6 +238,7 @@ DEPENDENCIES
|
|||||||
guard-rspec
|
guard-rspec
|
||||||
jbuilder (~> 2.5)
|
jbuilder (~> 2.5)
|
||||||
jquery-rails (~> 4.1.1)
|
jquery-rails (~> 4.1.1)
|
||||||
|
kaminari (~> 0.17.0)
|
||||||
pg (~> 0.18.4)
|
pg (~> 0.18.4)
|
||||||
rails (= 5.0.0)
|
rails (= 5.0.0)
|
||||||
rspec-rails (~> 3.5.0)
|
rspec-rails (~> 3.5.0)
|
||||||
|
@ -14,7 +14,7 @@ window.INGREDIENT_API = {};
|
|||||||
},{
|
},{
|
||||||
name: 'usdaFoods',
|
name: 'usdaFoods',
|
||||||
source: usdaFoodSearchEngine,
|
source: usdaFoodSearchEngine,
|
||||||
limit: 10,
|
limit: 20,
|
||||||
display: function(datum) {
|
display: function(datum) {
|
||||||
return datum.name;
|
return datum.name;
|
||||||
}
|
}
|
||||||
|
@ -73,3 +73,31 @@ body {
|
|||||||
background-color: $gray-lighter;
|
background-color: $gray-lighter;
|
||||||
border-top: solid 1px $gray-light;
|
border-top: solid 1px $gray-light;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.sorted {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.sorted.asc:after {
|
||||||
|
content: " ";
|
||||||
|
position: absolute;
|
||||||
|
margin: 8px 0 0 6px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 5px solid transparent;
|
||||||
|
border-right: 5px solid transparent;
|
||||||
|
|
||||||
|
border-top: 5px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.sorted.desc:after {
|
||||||
|
content: " ";
|
||||||
|
position: absolute;
|
||||||
|
margin: 8px 0 0 6px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 5px solid transparent;
|
||||||
|
border-right: 5px solid transparent;
|
||||||
|
|
||||||
|
border-bottom: 5px solid black;
|
||||||
|
}
|
@ -6,7 +6,8 @@ class RecipesController < ApplicationController
|
|||||||
|
|
||||||
# GET /recipes
|
# GET /recipes
|
||||||
def index
|
def index
|
||||||
@recipes = Recipe.active
|
@criteria = ViewModels::RecipeCriteria.new(params[:criteria])
|
||||||
|
@recipes = Recipe.for_criteria(@criteria)
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /recipes/1
|
# GET /recipes/1
|
||||||
|
@ -25,7 +25,7 @@ class RecipeDecorator < BaseDecorator
|
|||||||
end
|
end
|
||||||
|
|
||||||
def average_rating
|
def average_rating
|
||||||
@average_rating ||= (Log.for_recipe(wrapped).for_user(self.user).where('rating IS NOT NULL').average(:rating) || 0)
|
rating
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
@ -32,4 +32,40 @@ module RecipesHelper
|
|||||||
].compact.join("\n".html_safe).html_safe
|
].compact.join("\n".html_safe).html_safe
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def index_sort_header(text, field, criteria)
|
||||||
|
uri = URI(request.original_fullpath)
|
||||||
|
query = Rack::Utils.parse_query(uri.query)
|
||||||
|
|
||||||
|
directions = [:asc, :desc]
|
||||||
|
|
||||||
|
current_field = criteria.sort_column
|
||||||
|
current_direction = criteria.sort_direction
|
||||||
|
field_param = 'criteria[sort_column]'
|
||||||
|
direction_param = 'criteria[sort_direction]'
|
||||||
|
|
||||||
|
if request.get?
|
||||||
|
is_sorted = current_field == field.to_sym
|
||||||
|
|
||||||
|
if is_sorted && directions.include?(current_direction)
|
||||||
|
direction = (directions.reject { |d| d == current_direction }).first
|
||||||
|
else
|
||||||
|
direction = directions.first
|
||||||
|
end
|
||||||
|
|
||||||
|
if is_sorted && direction == :asc
|
||||||
|
link_class = 'sorted desc'
|
||||||
|
elsif is_sorted && direction == :desc
|
||||||
|
link_class = 'sorted asc'
|
||||||
|
else
|
||||||
|
link_class = 'sorted'
|
||||||
|
end
|
||||||
|
|
||||||
|
query[field_param.to_s] = field.to_s
|
||||||
|
query[direction_param.to_s] = direction.to_s
|
||||||
|
link_to text, "#{uri.path}?#{query.to_query}", class: link_class
|
||||||
|
else
|
||||||
|
text
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -8,9 +8,17 @@ class Log < ActiveRecord::Base
|
|||||||
validates :user_id, presence: true
|
validates :user_id, presence: true
|
||||||
validates :rating, numericality: { only_integer: true, allow_blank: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 5, message: 'must be an integer between 1 and 5, inclusive' }
|
validates :rating, numericality: { only_integer: true, allow_blank: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 5, message: 'must be an integer between 1 and 5, inclusive' }
|
||||||
|
|
||||||
scope :for_user, ->(user) { where(user: user) }
|
scope :for_user, ->(user) { where(user_id: user) }
|
||||||
scope :for_recipe, ->(recipe) { where(source_recipe: recipe) }
|
scope :for_recipe, ->(recipe) { where(source_recipe_id: recipe) }
|
||||||
|
|
||||||
accepts_nested_attributes_for :recipe, update_only: true, allow_destroy: false
|
accepts_nested_attributes_for :recipe, update_only: true, allow_destroy: false
|
||||||
|
|
||||||
|
after_save :update_rating
|
||||||
|
|
||||||
|
def update_rating
|
||||||
|
if self.source_recipe
|
||||||
|
self.source_recipe.update_rating!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -7,6 +7,7 @@ class Recipe < ActiveRecord::Base
|
|||||||
scope :undeleted, -> { where('deleted <> ? OR deleted IS NULL', true) }
|
scope :undeleted, -> { where('deleted <> ? OR deleted IS NULL', true) }
|
||||||
scope :not_log, -> { where('is_log <> ? OR is_log IS NULL', true) }
|
scope :not_log, -> { where('is_log <> ? OR is_log IS NULL', true) }
|
||||||
scope :active, -> { undeleted.not_log }
|
scope :active, -> { undeleted.not_log }
|
||||||
|
scope :for_criteria, ->(criteria) { active.order(criteria.sort_column => criteria.sort_direction).page(criteria.page).per(criteria.per) }
|
||||||
|
|
||||||
accepts_nested_attributes_for :recipe_ingredients, allow_destroy: true
|
accepts_nested_attributes_for :recipe_ingredients, allow_destroy: true
|
||||||
accepts_nested_attributes_for :recipe_steps, allow_destroy: true
|
accepts_nested_attributes_for :recipe_steps, allow_destroy: true
|
||||||
@ -59,6 +60,11 @@ class Recipe < ActiveRecord::Base
|
|||||||
@parsed_yield
|
@parsed_yield
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_rating!
|
||||||
|
self.rating = Log.for_recipe(self).for_user(self.user_id).where('rating IS NOT NULL').average(:rating)
|
||||||
|
save(validate: false)
|
||||||
|
end
|
||||||
|
|
||||||
# Creates a copy of this recipe suitable for associating to a log
|
# Creates a copy of this recipe suitable for associating to a log
|
||||||
def log_copy(user)
|
def log_copy(user)
|
||||||
copy = Recipe.new
|
copy = Recipe.new
|
||||||
|
48
app/models/view_models/recipe_criteria.rb
Normal file
48
app/models/view_models/recipe_criteria.rb
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
module ViewModels
|
||||||
|
class RecipeCriteria
|
||||||
|
|
||||||
|
SORT_COLUMNS = :created_at, :name, :rating, :total_time
|
||||||
|
|
||||||
|
attr_writer :sort_column, :sort_direction
|
||||||
|
attr_writer :page, :per
|
||||||
|
|
||||||
|
def initialize(params = {})
|
||||||
|
params ||= {}
|
||||||
|
([:sort_column, :sort_direction, :page, :per]).each do |attr|
|
||||||
|
setter = "#{attr}="
|
||||||
|
if params[attr]
|
||||||
|
self.send(setter, params[attr])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def sort_column
|
||||||
|
@sort_column ||= SORT_COLUMNS.first
|
||||||
|
@sort_column = @sort_column.to_sym
|
||||||
|
unless SORT_COLUMNS.include? @sort_column
|
||||||
|
@sort_column = SORT_COLUMNS.first
|
||||||
|
end
|
||||||
|
|
||||||
|
@sort_column
|
||||||
|
end
|
||||||
|
|
||||||
|
def sort_direction
|
||||||
|
@sort_direction ||= :asc
|
||||||
|
@sort_direction = @sort_direction.to_sym
|
||||||
|
unless [:asc, :desc].include? @sort_direction
|
||||||
|
@sort_direction = :asc
|
||||||
|
end
|
||||||
|
|
||||||
|
@sort_direction
|
||||||
|
end
|
||||||
|
|
||||||
|
def page
|
||||||
|
@page.to_i || 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def per
|
||||||
|
@per.to_i || 50
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
@ -9,15 +9,17 @@
|
|||||||
<p>No Recipes</p>
|
<p>No Recipes</p>
|
||||||
<% else %>
|
<% else %>
|
||||||
|
|
||||||
|
<%= paginate @recipes, :param_name => 'criteria[page]' %>
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="recipe-table table table-striped table-hover">
|
<table class="recipe-table table table-striped table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th><%= index_sort_header('Name', :name, @criteria) %></th>
|
||||||
<th>Rating</th>
|
<th><%= index_sort_header('Rating', :rating, @criteria) %></th>
|
||||||
<th>Yields</th>
|
<th>Yields</th>
|
||||||
<th>Time</th>
|
<th><%= index_sort_header('Time', :total_time, @criteria) %></th>
|
||||||
<th>Created</th>
|
<th><%= index_sort_header('Created', :created_at, @criteria) %></th>
|
||||||
<% if current_user? %>
|
<% if current_user? %>
|
||||||
<th></th>
|
<th></th>
|
||||||
<% end %>
|
<% end %>
|
||||||
@ -57,6 +59,8 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<%= paginate @recipes, :param_name => 'criteria[page]' %>
|
||||||
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
19
db/migrate/20160928212209_add_rating_to_recipe.rb
Normal file
19
db/migrate/20160928212209_add_rating_to_recipe.rb
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
class AddRatingToRecipe < ActiveRecord::Migration[5.0]
|
||||||
|
class Recipe < ActiveRecord::Base
|
||||||
|
end
|
||||||
|
|
||||||
|
class Log < ActiveRecord::Base
|
||||||
|
end
|
||||||
|
|
||||||
|
def change
|
||||||
|
add_column :recipes, :rating, :float
|
||||||
|
|
||||||
|
Recipe.reset_column_information
|
||||||
|
Log.reset_column_information
|
||||||
|
|
||||||
|
Recipe.all.each do |r|
|
||||||
|
r.rating = Log.where(user_id: r.user_id).where(source_recipe_id: r).where('rating IS NOT NULL').average(:rating)
|
||||||
|
r.save!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 20160812205919) do
|
ActiveRecord::Schema.define(version: 20160928212209) do
|
||||||
|
|
||||||
create_table "ingredient_units", force: :cascade do |t|
|
create_table "ingredient_units", force: :cascade do |t|
|
||||||
t.integer "ingredient_id", null: false
|
t.integer "ingredient_id", null: false
|
||||||
@ -99,6 +99,7 @@ ActiveRecord::Schema.define(version: 20160812205919) do
|
|||||||
t.boolean "deleted"
|
t.boolean "deleted"
|
||||||
t.integer "user_id"
|
t.integer "user_id"
|
||||||
t.boolean "is_log"
|
t.boolean "is_log"
|
||||||
|
t.float "rating"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "usda_food_weights", force: :cascade do |t|
|
create_table "usda_food_weights", force: :cascade do |t|
|
||||||
|
@ -6,6 +6,7 @@ FactoryGirl.define do
|
|||||||
yields 4
|
yields 4
|
||||||
total_time 20
|
total_time 20
|
||||||
active_time 10
|
active_time 10
|
||||||
|
user
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -1,4 +1,30 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe Log, type: :model do
|
RSpec.describe Log, type: :model do
|
||||||
|
|
||||||
|
describe 'Rating Update' do
|
||||||
|
it 'updates recipe rating on create' do
|
||||||
|
r = create(:recipe)
|
||||||
|
expect(r.rating).to be_nil
|
||||||
|
|
||||||
|
l = build(:log, source_recipe: r, user: r.user)
|
||||||
|
l.save
|
||||||
|
|
||||||
|
r.reload
|
||||||
|
expect(r.rating).to eq 1
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates recipe rating on update' do
|
||||||
|
r = create(:recipe)
|
||||||
|
l = create(:log, source_recipe: r, user: r.user)
|
||||||
|
r.update_rating!
|
||||||
|
|
||||||
|
l.rating = 5
|
||||||
|
l.save
|
||||||
|
|
||||||
|
r.reload
|
||||||
|
expect(r.rating).to eq 5
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -1,4 +1,31 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe Recipe, type: :model do
|
RSpec.describe Recipe, type: :model do
|
||||||
|
describe '#update_rating!' do
|
||||||
|
|
||||||
|
it 'should set rating to nil with no ratings' do
|
||||||
|
r = create(:recipe)
|
||||||
|
r.update_rating!
|
||||||
|
expect(r.rating).to be_nil
|
||||||
|
|
||||||
|
create(:log, rating: nil, source_recipe: r)
|
||||||
|
|
||||||
|
r.update_rating!
|
||||||
|
expect(r.rating).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should set rating based on user logs' do
|
||||||
|
user = create(:user)
|
||||||
|
other_user = create(:user)
|
||||||
|
r = create(:recipe, user: user)
|
||||||
|
create(:log, rating: 2, source_recipe: r, user: user)
|
||||||
|
create(:log, rating: 4, source_recipe: r, user: user)
|
||||||
|
create(:log, rating: 5, source_recipe: r, user: other_user)
|
||||||
|
|
||||||
|
r.update_rating!
|
||||||
|
|
||||||
|
expect(r.rating).to eq 3
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -8,7 +8,8 @@ RSpec.describe UsdaFood do
|
|||||||
unsalted_butter: create(:usda_food, long_description: 'Unsalted Butter'),
|
unsalted_butter: create(:usda_food, long_description: 'Unsalted Butter'),
|
||||||
flour: create(:usda_food, long_description: 'Flour'),
|
flour: create(:usda_food, long_description: 'Flour'),
|
||||||
bread_flour: create(:usda_food, long_description: 'Bread Flour'),
|
bread_flour: create(:usda_food, long_description: 'Bread Flour'),
|
||||||
sugar: create(:usda_food, long_description: 'Sugar,Granulated')
|
sugar: create(:usda_food, long_description: 'Sugar,Granulated'),
|
||||||
|
mustard: create(:usda_food, long_description: 'HONEY MUSTARD DIPPING SAUCE')
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -30,6 +31,10 @@ RSpec.describe UsdaFood do
|
|||||||
r = UsdaFood.matches_tokens(:long_description, ['sal', 'butter'])
|
r = UsdaFood.matches_tokens(:long_description, ['sal', 'butter'])
|
||||||
expect(r.length).to eq 1
|
expect(r.length).to eq 1
|
||||||
expect(r).to contain_exactly *items(:salted_butter)
|
expect(r).to contain_exactly *items(:salted_butter)
|
||||||
|
|
||||||
|
r = UsdaFood.matches_tokens(:long_description, ['butter', 'sal'])
|
||||||
|
expect(r.length).to eq 1
|
||||||
|
expect(r).to contain_exactly *items(:salted_butter)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'treats commas like spaces' do
|
it 'treats commas like spaces' do
|
||||||
|
Loading…
Reference in New Issue
Block a user