diff --git a/Gemfile b/Gemfile index 3f1e6961..29aa7b86 100644 --- a/Gemfile +++ b/Gemfile @@ -39,6 +39,9 @@ gem 'simple_form' # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', '>= 1.1.0', require: false +gem 'will_paginate', '~> 3.1.0' +gem 'acts_as_votable' + group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] @@ -58,6 +61,10 @@ group :development do gem 'spring-watcher-listen', '~> 2.0.0' end +group :test do + gem 'database_cleaner-active_record' +end + # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] diff --git a/Gemfile.lock b/Gemfile.lock index 16c3b7bc..20882829 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -42,6 +42,7 @@ GEM i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) + acts_as_votable (0.13.1) addressable (2.5.2) public_suffix (>= 2.0.2, < 4.0) arel (9.0.0) @@ -68,6 +69,10 @@ GEM coffee-script-source (1.12.2) concurrent-ruby (1.0.5) crass (1.0.4) + database_cleaner-active_record (2.0.0) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0.0) + database_cleaner-core (2.0.1) devise (4.4.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -220,6 +225,7 @@ GEM websocket-driver (0.7.0) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.3) + will_paginate (3.1.8) xpath (3.0.0) nokogiri (~> 1.8) @@ -227,10 +233,12 @@ PLATFORMS ruby DEPENDENCIES + acts_as_votable bootsnap (>= 1.1.0) byebug capybara coffee-rails (~> 4.2) + database_cleaner-active_record devise factory_bot_rails jbuilder (~> 2.5) @@ -249,9 +257,10 @@ DEPENDENCIES tzinfo-data uglifier (>= 1.3.0) web-console (>= 3.3.0) + will_paginate (~> 3.1.0) RUBY VERSION ruby 2.4.4p296 BUNDLED WITH - 1.16.2 + 2.2.11 diff --git a/app/controllers/dogs_controller.rb b/app/controllers/dogs_controller.rb index cb9eebc5..fb2c50b7 100644 --- a/app/controllers/dogs_controller.rb +++ b/app/controllers/dogs_controller.rb @@ -1,10 +1,16 @@ class DogsController < ApplicationController - before_action :set_dog, only: [:show, :edit, :update, :destroy] + before_action :set_dog, only: [:show, :edit, :update, :destroy, :like] # GET /dogs # GET /dogs.json def index - @dogs = Dog.all + if params[:sort] == 'likes' + dogs = Dog.order(:cached_weighted_average => :desc) + else + dogs = Dog + end + @sorted = params[:sort] == 'likes' + @dogs = dogs.paginate(page: params[:page], per_page: 5) end # GET /dogs/1 @@ -14,17 +20,22 @@ def show # GET /dogs/new def new + redirect_to dogs_url, notice: 'Dog was successfully destroyed.' if current_user.nil? @dog = Dog.new end # GET /dogs/1/edit def edit + redirect_to(@dog, notice: 'You do not own this dog') if current_user.nil? || @dog.owner != current_user end # POST /dogs # POST /dogs.json def create + redirect_to(@dog, notice: 'You are not signed in.') if current_user.nil? + @dog = Dog.new(dog_params) + @dog.user = current_user respond_to do |format| if @dog.save @@ -42,6 +53,7 @@ def create # PATCH/PUT /dogs/1 # PATCH/PUT /dogs/1.json def update + redirect_to(@dog, notice: 'You do not own this dog.') if current_user.nil? || @dog.owner != current_user respond_to do |format| if @dog.update(dog_params) @dog.images.attach(params[:dog][:image]) if params[:dog][:image].present? @@ -58,6 +70,7 @@ def update # DELETE /dogs/1 # DELETE /dogs/1.json def destroy + redirect_to(@dog, notice: 'You do not own this dog.') if current_user.nil? || @dog.owner != current_user @dog.destroy respond_to do |format| format.html { redirect_to dogs_url, notice: 'Dog was successfully destroyed.' } @@ -65,6 +78,23 @@ def destroy end end + # GET /dogs/1/like + # GET /dogs/1/like.json + def like + redirect_to(@dog, notice: 'You own this dog.') if current_user.nil? || @dog.owner == current_user + + if current_user.voted_for? @dog + @dog.unliked_by current_user + else + @dog.liked_by current_user + end + + respond_to do |format| + format.html { redirect_to @dog } + format.json { render :show, status: :ok, location: @dog } + end + end + private # Use callbacks to share common setup or constraints between actions. def set_dog @@ -73,6 +103,6 @@ def set_dog # Never trust parameters from the scary internet, only allow the white list through. def dog_params - params.require(:dog).permit(:name, :description, :images) + params.require(:dog).permit(:name, :description, images: []) end end diff --git a/app/models/dog.rb b/app/models/dog.rb index eb40bf6e..7f700256 100644 --- a/app/models/dog.rb +++ b/app/models/dog.rb @@ -1,3 +1,13 @@ class Dog < ApplicationRecord has_many_attached :images + belongs_to :user + + alias_method :owner, :user + + validates :name, presence: true + validates :owner, presence: true + validates :description, presence: true + validates :images, presence: true + + acts_as_votable end diff --git a/app/models/user.rb b/app/models/user.rb index b2091f9a..8b379908 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,4 +3,8 @@ class User < ApplicationRecord # :confirmable, :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable + + has_many :dogs + + acts_as_voter end diff --git a/app/views/dogs/_form.html.erb b/app/views/dogs/_form.html.erb index 6dfc7dc9..54915388 100644 --- a/app/views/dogs/_form.html.erb +++ b/app/views/dogs/_form.html.erb @@ -1,10 +1,10 @@ <%= simple_form_for @dog do |f| %> <%= f.input :name %> <%= f.input :description, as: :text %> - <%= f.input :image, as: :file %> + <%= f.input :images, as: :file, input_html: { multiple: true } %> - <% if @dog.images.any? %> - <%= image_tag @dog.images.first %> + <% @dog.images.each do |image| %> + <%= image_tag image %> <% end %> <%= f.button :submit %> diff --git a/app/views/dogs/_thumbnail.html.erb b/app/views/dogs/_thumbnail.html.erb index 4d6bb441..23aae5e9 100644 --- a/app/views/dogs/_thumbnail.html.erb +++ b/app/views/dogs/_thumbnail.html.erb @@ -4,3 +4,6 @@ <%= image_tag url_for(dog.images.first), class: "dog-photo", alt: "Photo of #{dog.name}" %> +<% if dog_counter % 2 == 1 %> + <%= image_tag 'ad.jpg', class: "ad", alt: "A very cool ad." %> +<% end %> diff --git a/app/views/dogs/index.html.erb b/app/views/dogs/index.html.erb index 91ea5603..aa08f33d 100644 --- a/app/views/dogs/index.html.erb +++ b/app/views/dogs/index.html.erb @@ -1 +1,8 @@ +<% if @sorted %> + <%= link_to "Unsort", root_path, title: "sort by likes", id: 'sort' %> +<% else %> + <%= link_to "Sort by Likes", root_path + '?sort=likes', title: "unsort by likes", id: 'sort' %> +<% end %> +<%= will_paginate @dogs %> <%= render partial: 'thumbnail', collection: @dogs, as: :dog %> +<%= will_paginate @dogs %> diff --git a/app/views/dogs/show.html.erb b/app/views/dogs/show.html.erb index 562324d2..2dea1231 100644 --- a/app/views/dogs/show.html.erb +++ b/app/views/dogs/show.html.erb @@ -1,5 +1,14 @@
-

<%= @dog.name %>

+

+ <%= @dog.name %> + <% unless current_user.nil? || @dog.owner == current_user %> + <% if current_user&.voted_for?(@dog) %> + <%= link_to "❤️", like_dog_path(@dog), title: "click to unlike", id: 'like_dog_link' %> + <% else %> + <%= link_to "🤍", like_dog_path(@dog), title: "click to like", id: 'unlike_dog_link' %> + <% end %> + <% end %> +

<% @dog.images.each do |image| %> <%= image_tag url_for(image), alt: "Photo of #{@dog.name}" %> @@ -7,7 +16,9 @@

<%= @dog.description %>

- <%= link_to "Edit #{@dog.name}'s Profile", edit_dog_path %> -
- <%= link_to "Delete #{@dog.name}'s Profile", dog_path, method: :delete, data: { confirm: 'Are you sure?' } %> + <% if current_user && @dog.owner == current_user %> + <%= link_to "Edit #{@dog.name}'s Profile", edit_dog_path %> +
+ <%= link_to "Delete #{@dog.name}'s Profile", dog_path, method: :delete, data: { confirm: 'Are you sure?' } %> + <% end %>
diff --git a/config/routes.rb b/config/routes.rb index 06b01adc..5efc9991 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,10 @@ Rails.application.routes.draw do # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html devise_for :users - resources :dogs + resources :dogs do + member do + get "like" + end + end root to: "dogs#index" end diff --git a/db/migrate/20210219021259_add_owner_to_dogs.rb b/db/migrate/20210219021259_add_owner_to_dogs.rb new file mode 100644 index 00000000..9727c3c2 --- /dev/null +++ b/db/migrate/20210219021259_add_owner_to_dogs.rb @@ -0,0 +1,5 @@ +class AddOwnerToDogs < ActiveRecord::Migration[5.2] + def change + add_reference :dogs, :user, index: true, foreign_key: true + end +end diff --git a/db/migrate/20210219042627_acts_as_votable_migration.rb b/db/migrate/20210219042627_acts_as_votable_migration.rb new file mode 100644 index 00000000..2516d700 --- /dev/null +++ b/db/migrate/20210219042627_acts_as_votable_migration.rb @@ -0,0 +1,22 @@ +class ActsAsVotableMigration < ActiveRecord::Migration[4.2] + def self.up + create_table :votes do |t| + + t.references :votable, :polymorphic => true + t.references :voter, :polymorphic => true + + t.boolean :vote_flag + t.string :vote_scope + t.integer :vote_weight + + t.timestamps + end + + add_index :votes, [:voter_id, :voter_type, :vote_scope] + add_index :votes, [:votable_id, :votable_type, :vote_scope] + end + + def self.down + drop_table :votes + end +end diff --git a/db/migrate/20210220172412_add_cached_votes_to_dogs.rb b/db/migrate/20210220172412_add_cached_votes_to_dogs.rb new file mode 100644 index 00000000..d6cae834 --- /dev/null +++ b/db/migrate/20210220172412_add_cached_votes_to_dogs.rb @@ -0,0 +1,16 @@ +class AddCachedVotesToDogs < ActiveRecord::Migration[5.2] + def change + change_table :dogs do |t| + t.integer :cached_votes_total, default: 0 + t.integer :cached_votes_score, default: 0 + t.integer :cached_votes_up, default: 0 + t.integer :cached_votes_down, default: 0 + t.integer :cached_weighted_score, default: 0 + t.integer :cached_weighted_total, default: 0 + t.float :cached_weighted_average, default: 0.0 + end + + # Uncomment this line to force caching of existing votes + Dog.find_each(&:update_cached_votes) + end +end diff --git a/db/schema.rb b/db/schema.rb index 462bd430..542af6a9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2018_06_07_114248) do +ActiveRecord::Schema.define(version: 2021_02_20_172412) do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false @@ -40,6 +40,15 @@ t.text "description" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "user_id" + t.integer "cached_votes_total", default: 0 + t.integer "cached_votes_score", default: 0 + t.integer "cached_votes_up", default: 0 + t.integer "cached_votes_down", default: 0 + t.integer "cached_weighted_score", default: 0 + t.integer "cached_weighted_total", default: 0 + t.float "cached_weighted_average", default: 0.0 + t.index ["user_id"], name: "index_dogs_on_user_id" end create_table "users", force: :cascade do |t| @@ -60,4 +69,18 @@ t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end + create_table "votes", force: :cascade do |t| + t.string "votable_type" + t.integer "votable_id" + t.string "voter_type" + t.integer "voter_id" + t.boolean "vote_flag" + t.string "vote_scope" + t.integer "vote_weight" + t.datetime "created_at" + t.datetime "updated_at" + t.index ["votable_id", "votable_type", "vote_scope"], name: "index_votes_on_votable_id_and_votable_type_and_vote_scope" + t.index ["voter_id", "voter_type", "vote_scope"], name: "index_votes_on_voter_id_and_voter_type_and_vote_scope" + end + end diff --git a/spec/factories/dogs.rb b/spec/factories/dogs.rb index 406874f5..79c86317 100644 --- a/spec/factories/dogs.rb +++ b/spec/factories/dogs.rb @@ -1,5 +1,7 @@ FactoryBot.define do factory :dog do + association :user + description { 'the goodest dog' } sequence :name do |n| "Good Pup #{n}" end diff --git a/spec/factories/user.rb b/spec/factories/user.rb new file mode 100644 index 00000000..dce0043a --- /dev/null +++ b/spec/factories/user.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory :user do + sequence :email do |n| + "email#{n}@example.com" + end + password { '123456' } + end +end diff --git a/spec/features/dog_resource_spec.rb b/spec/features/dog_resource_spec.rb index d9337477..8f428779 100644 --- a/spec/features/dog_resource_spec.rb +++ b/spec/features/dog_resource_spec.rb @@ -1,6 +1,10 @@ require 'rails_helper' +require_relative '../support/devise' describe 'Dog resource', type: :feature do + let!(:spec_user) { User.first || FactoryBot.create(:user) } + before { login_as spec_user } + it 'can create a profile' do visit new_dog_path fill_in 'Name', with: 'Speck' @@ -11,7 +15,7 @@ end it 'can edit a dog profile' do - dog = create(:dog) + dog = create(:dog, user: spec_user) visit edit_dog_path(dog) fill_in 'Name', with: 'Speck' click_button 'Update Dog' @@ -19,7 +23,7 @@ end it 'can delete a dog profile' do - dog = create(:dog) + dog = create(:dog, user: spec_user) visit dog_path(dog) click_link "Delete #{dog.name}'s Profile" expect(Dog.count).to eq(0) diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 4e322aeb..3642aba5 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -56,4 +56,23 @@ config.filter_rails_from_backtrace! # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") + config.expect_with :rspec do |c| + c.syntax = :expect + end + + config.include Devise::Test::ControllerHelpers, :type => :controller + + config.include Warden::Test::Helpers + + config.before(:suite) do + DatabaseCleaner.strategy = :transaction + DatabaseCleaner.clean_with(:truncation) + end + + config.around(:each) do |example| + DatabaseCleaner.cleaning do + example.run + end + end + end diff --git a/spec/support/controller_macros.rb b/spec/support/controller_macros.rb new file mode 100644 index 00000000..e0da1ab8 --- /dev/null +++ b/spec/support/controller_macros.rb @@ -0,0 +1,9 @@ +module ControllerMacros + def login_user + before(:each) do + @request.env["devise.mapping"] = Devise.mappings[:user] + user = FactoryBot.create(:user) + sign_in user + end + end +end diff --git a/spec/support/devise.rb b/spec/support/devise.rb new file mode 100644 index 00000000..888c28b3 --- /dev/null +++ b/spec/support/devise.rb @@ -0,0 +1,9 @@ +require_relative './controller_macros' # or require_relative './controller_macros' if write in `spec/support/devise.rb` + +RSpec.configure do |config| + # For Devise > 4.1.1 + config.include Devise::Test::ControllerHelpers, :type => :controller + # Use the following instead if you are on Devise <= 4.1.1 + # config.include Devise::TestHelpers, :type => :controller + config.extend ControllerMacros, :type => :controller +end