Added sorting columns to recipe index

This commit is contained in:
Dan Elbert 2016-09-28 17:08:43 -05:00
parent 779cdb6173
commit 36cbf92df0
17 changed files with 229 additions and 15 deletions

View File

@ -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'

View File

@ -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)

View File

@ -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;
} }

View File

@ -72,4 +72,32 @@ body {
height: $footer_height; height: $footer_height;
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;
} }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View File

@ -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>

View 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

View File

@ -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|

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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