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
|
||||
gem 'jquery-rails', '~> 4.1.1'
|
||||
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 'jbuilder', '~> 2.5'
|
||||
gem 'cocoon', '~> 1.2.9'
|
||||
|
10
Gemfile.lock
10
Gemfile.lock
@ -39,7 +39,7 @@ GEM
|
||||
minitest (~> 5.1)
|
||||
tzinfo (~> 1.1)
|
||||
arel (7.1.2)
|
||||
autoprefixer-rails (6.4.1.1)
|
||||
autoprefixer-rails (6.5.0)
|
||||
execjs
|
||||
bcrypt (3.1.11)
|
||||
blankslate (3.1.3)
|
||||
@ -87,6 +87,9 @@ GEM
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
kaminari (0.17.0)
|
||||
actionpack (>= 3.0.0)
|
||||
activesupport (>= 3.0.0)
|
||||
libv8 (3.16.14.15)
|
||||
liner (0.2.4)
|
||||
listen (3.1.5)
|
||||
@ -105,7 +108,7 @@ GEM
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2016.0521)
|
||||
mini_portile2 (2.1.0)
|
||||
minitest (5.9.0)
|
||||
minitest (5.9.1)
|
||||
multi_json (1.12.1)
|
||||
nenv (0.3.0)
|
||||
nio4r (1.2.1)
|
||||
@ -149,7 +152,7 @@ GEM
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.18.1, < 2.0)
|
||||
rake (11.2.2)
|
||||
rake (11.3.0)
|
||||
rb-fsevent (0.9.7)
|
||||
rb-inotify (0.9.7)
|
||||
ffi (>= 0.5.0)
|
||||
@ -235,6 +238,7 @@ DEPENDENCIES
|
||||
guard-rspec
|
||||
jbuilder (~> 2.5)
|
||||
jquery-rails (~> 4.1.1)
|
||||
kaminari (~> 0.17.0)
|
||||
pg (~> 0.18.4)
|
||||
rails (= 5.0.0)
|
||||
rspec-rails (~> 3.5.0)
|
||||
|
@ -14,7 +14,7 @@ window.INGREDIENT_API = {};
|
||||
},{
|
||||
name: 'usdaFoods',
|
||||
source: usdaFoodSearchEngine,
|
||||
limit: 10,
|
||||
limit: 20,
|
||||
display: function(datum) {
|
||||
return datum.name;
|
||||
}
|
||||
|
@ -72,4 +72,32 @@ body {
|
||||
height: $footer_height;
|
||||
background-color: $gray-lighter;
|
||||
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
|
||||
def index
|
||||
@recipes = Recipe.active
|
||||
@criteria = ViewModels::RecipeCriteria.new(params[:criteria])
|
||||
@recipes = Recipe.for_criteria(@criteria)
|
||||
end
|
||||
|
||||
# GET /recipes/1
|
||||
|
@ -25,7 +25,7 @@ class RecipeDecorator < BaseDecorator
|
||||
end
|
||||
|
||||
def average_rating
|
||||
@average_rating ||= (Log.for_recipe(wrapped).for_user(self.user).where('rating IS NOT NULL').average(:rating) || 0)
|
||||
rating
|
||||
end
|
||||
|
||||
end
|
@ -32,4 +32,40 @@ module RecipesHelper
|
||||
].compact.join("\n".html_safe).html_safe
|
||||
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
|
||||
|
@ -8,9 +8,17 @@ class Log < ActiveRecord::Base
|
||||
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' }
|
||||
|
||||
scope :for_user, ->(user) { where(user: user) }
|
||||
scope :for_recipe, ->(recipe) { where(source_recipe: recipe) }
|
||||
scope :for_user, ->(user) { where(user_id: user) }
|
||||
scope :for_recipe, ->(recipe) { where(source_recipe_id: recipe) }
|
||||
|
||||
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
|
||||
|
@ -7,6 +7,7 @@ class Recipe < ActiveRecord::Base
|
||||
scope :undeleted, -> { where('deleted <> ? OR deleted IS NULL', true) }
|
||||
scope :not_log, -> { where('is_log <> ? OR is_log IS NULL', true) }
|
||||
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_steps, allow_destroy: true
|
||||
@ -59,6 +60,11 @@ class Recipe < ActiveRecord::Base
|
||||
@parsed_yield
|
||||
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
|
||||
def log_copy(user)
|
||||
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>
|
||||
<% else %>
|
||||
|
||||
<%= paginate @recipes, :param_name => 'criteria[page]' %>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="recipe-table table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Rating</th>
|
||||
<th><%= index_sort_header('Name', :name, @criteria) %></th>
|
||||
<th><%= index_sort_header('Rating', :rating, @criteria) %></th>
|
||||
<th>Yields</th>
|
||||
<th>Time</th>
|
||||
<th>Created</th>
|
||||
<th><%= index_sort_header('Time', :total_time, @criteria) %></th>
|
||||
<th><%= index_sort_header('Created', :created_at, @criteria) %></th>
|
||||
<% if current_user? %>
|
||||
<th></th>
|
||||
<% end %>
|
||||
@ -57,6 +59,8 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<%= paginate @recipes, :param_name => 'criteria[page]' %>
|
||||
|
||||
<% end %>
|
||||
|
||||
<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.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20160812205919) do
|
||||
ActiveRecord::Schema.define(version: 20160928212209) do
|
||||
|
||||
create_table "ingredient_units", force: :cascade do |t|
|
||||
t.integer "ingredient_id", null: false
|
||||
@ -99,6 +99,7 @@ ActiveRecord::Schema.define(version: 20160812205919) do
|
||||
t.boolean "deleted"
|
||||
t.integer "user_id"
|
||||
t.boolean "is_log"
|
||||
t.float "rating"
|
||||
end
|
||||
|
||||
create_table "usda_food_weights", force: :cascade do |t|
|
||||
|
@ -6,6 +6,7 @@ FactoryGirl.define do
|
||||
yields 4
|
||||
total_time 20
|
||||
active_time 10
|
||||
user
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -1,4 +1,30 @@
|
||||
require 'rails_helper'
|
||||
|
||||
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
|
||||
|
@ -1,4 +1,31 @@
|
||||
require 'rails_helper'
|
||||
|
||||
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
|
||||
|
@ -8,7 +8,8 @@ RSpec.describe UsdaFood do
|
||||
unsalted_butter: create(:usda_food, long_description: 'Unsalted Butter'),
|
||||
flour: create(:usda_food, long_description: '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
|
||||
|
||||
@ -30,6 +31,10 @@ RSpec.describe UsdaFood do
|
||||
r = UsdaFood.matches_tokens(:long_description, ['sal', 'butter'])
|
||||
expect(r.length).to eq 1
|
||||
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
|
||||
|
||||
it 'treats commas like spaces' do
|
||||
|
Loading…
Reference in New Issue
Block a user