From 2a7a19c93ee0e0d77e4e388d43f36a721c7ab715 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Fri, 20 Oct 2023 21:27:06 -0400 Subject: Added post voting --- app/assets/stylesheets/main/entries.scss | 10 ++++++ app/controllers/blogs_controller.rb | 48 ++++++++++++++++++++++++++ app/models/blog.rb | 1 + app/models/concerns/votable.rb | 43 +++++++++++++++++++++++ app/models/vote.rb | 6 ++++ app/views/blogs/_blog.html.haml | 4 +++ app/views/blogs/voted.js.erb | 12 +++++++ config/routes.rb | 3 ++ db/migrate/20231020194529_create_votes.rb | 11 ++++++ db/migrate/20231020195330_make_blog_votable.rb | 8 +++++ db/schema.rb | 14 +++++++- 11 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 app/models/concerns/votable.rb create mode 100644 app/models/vote.rb create mode 100644 app/views/blogs/voted.js.erb create mode 100644 db/migrate/20231020194529_create_votes.rb create mode 100644 db/migrate/20231020195330_make_blog_votable.rb diff --git a/app/assets/stylesheets/main/entries.scss b/app/assets/stylesheets/main/entries.scss index edf1706..3b7215c 100644 --- a/app/assets/stylesheets/main/entries.scss +++ b/app/assets/stylesheets/main/entries.scss @@ -416,3 +416,13 @@ font-weight: bold; } } + +.post-vote { + float: right; + position: relative; + right: 0.5em; + + a { + text-decoration: none; + } +} diff --git a/app/controllers/blogs_controller.rb b/app/controllers/blogs_controller.rb index 0d218ae..2f9df49 100644 --- a/app/controllers/blogs_controller.rb +++ b/app/controllers/blogs_controller.rb @@ -38,4 +38,52 @@ class BlogsController < ApplicationController }) end + def upvote + @blog = Blog.find_by_slug(params[:slug]) + + raise ActiveRecord::RecordNotFound unless @blog + raise ActiveRecord::RecordNotFound unless @blog.published + + respond_to do |format| + if @blog.upvote! request.remote_ip + format.html do + flash[:notice] = "You have upvoted the blog post \"#{@blog.title}\"." + redirect_to @blog + end + format.js { render "voted" } + format.xml { head :ok } + else + format.html do + flash[:notice] = "You have already voted on the blog post \"#{@blog.title}\"." + redirect_to @blog + end + format.xml { render :xml => { :error => "Someone from your IP address has already voted on this blog post."} } + end + end + end + + def downvote + @blog = Blog.find_by_slug(params[:slug]) + + raise ActiveRecord::RecordNotFound unless @blog + raise ActiveRecord::RecordNotFound unless @blog.published + + respond_to do |format| + if @blog.downvote! request.remote_ip + format.html do + flash[:notice] = "You have downvoted the blog post \"#{@blog.title}\"." + redirect_to @blog + end + format.js { render "voted" } + format.xml { head :ok } + else + format.html do + flash[:notice] = "You have already voted on the blog post \"#{@blog.title}\"." + redirect_to @blog + end + format.xml { render :xml => { :error => "Someone from your IP address has already voted on this blog post."} } + end + end + end + end diff --git a/app/models/blog.rb b/app/models/blog.rb index 6db75ec..03643bf 100644 --- a/app/models/blog.rb +++ b/app/models/blog.rb @@ -1,5 +1,6 @@ class Blog < ApplicationRecord include Recordable + include Votable acts_as_taggable diff --git a/app/models/concerns/votable.rb b/app/models/concerns/votable.rb new file mode 100644 index 0000000..ba6e6d5 --- /dev/null +++ b/app/models/concerns/votable.rb @@ -0,0 +1,43 @@ +module Votable + extend ActiveSupport::Concern + + included do + has_many :votes, as: :votable + + def already_upvoted?(ip) + !votes.where(ip: ip, upvote: 1).empty? + end + + def already_downvoted?(ip) + !votes.where(ip: ip, upvote: 0).empty? + end + + def upvote!(ip) + return false if already_upvoted?(ip) + + if already_downvoted?(ip) + votes.where(ip: ip, upvote: 0).first.delete + self.downvotes -= 1 + save! + else + votes.create(ip: ip, upvote: 1).save + self.upvotes += 1 + save! + end + end + + def downvote!(ip) + return false if already_downvoted?(ip) + + if already_upvoted?(ip) + votes.where(ip: ip, upvote: 1).first.delete + self.upvotes -= 1 + save! + else + votes.create(ip: ip, upvote: 0).save + self.downvotes += 1 + save! + end + end + end +end diff --git a/app/models/vote.rb b/app/models/vote.rb new file mode 100644 index 0000000..e2d8386 --- /dev/null +++ b/app/models/vote.rb @@ -0,0 +1,6 @@ +class Vote < ApplicationRecord + belongs_to :votable, polymorphic: true + + validates :upvote, presence: true, inclusion: { in: [0, 1] } + validates :ip, presence: true +end diff --git a/app/views/blogs/_blog.html.haml b/app/views/blogs/_blog.html.haml index ec61bb5..1f86ae8 100644 --- a/app/views/blogs/_blog.html.haml +++ b/app/views/blogs/_blog.html.haml @@ -19,3 +19,7 @@ %strong= blog.user.login.capitalize on = blog.visible_date.strftime("%B #{blog.visible_date.day.ordinalize}, %Y at %-I:%M:%S%P") + .post-vote{ id: "blog-vote-section-#{blog.id}" } + %span.vote-link{ id: "blog-upvote-link-#{blog.id}" }= link_to_unless (not blog.published or blog.already_upvoted?(request.remote_ip)), "👍", upvote_blog_path(blog), remote: true, rel: "nofollow", class: "blog-upvote-link", method: :post + %span.post-rating{ id: "blog-rating-#{blog.id}" }= blog.upvotes - blog.downvotes + %span.vote-link{ id: "blog-downvote-link-#{blog.id}" }= link_to_unless (not blog.published or blog.already_downvoted?(request.remote_ip)), "👎", downvote_blog_path(blog), remote: true, rel: "nofollow", class: "blog-downvote-link", method: :post diff --git a/app/views/blogs/voted.js.erb b/app/views/blogs/voted.js.erb new file mode 100644 index 0000000..951c740 --- /dev/null +++ b/app/views/blogs/voted.js.erb @@ -0,0 +1,12 @@ +$("#blog-rating-<%= @blog.id %>").html('<%= escape_javascript("#{@blog.upvotes - @blog.downvotes}") %>'); + +<% if @blog.already_upvoted? request.remote_ip %> + $("#blog-upvote-link-<%= @blog.id %>").html("👍"); +<% elsif @blog.already_downvoted? request.remote_ip %> + $("#blog-downvote-link-<%= @blog.id %>").html("👎"); +<% else %> + $("#blog-upvote-link-<%= @blog.id %>").html('<%= escape_javascript(link_to("👍", upvote_blog_path(@blog), remote: true, rel: "nofollow", class: "blog-upvote-link", method: :post)) %>'); + $("#blog-downvote-link-<%= @blog.id %>").html('<%= escape_javascript(link_to("👎", downvote_blog_path(@blog), remote: true, rel: "nofollow", class: "blog-downvote-link", method: :post)) %>'); +<% end %> + +$("#blog-vote-section-<%= @blog.id %>").effect('highlight', {}, 2000); diff --git a/config/routes.rb b/config/routes.rb index 416e939..6363590 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -38,6 +38,9 @@ Rails.application.routes.draw do resources :blogs, only: [:index, :show], param: :slug, path: "blog" do member do + post 'upvote' + post 'downvote' + resources :comments, only: [:create] end end diff --git a/db/migrate/20231020194529_create_votes.rb b/db/migrate/20231020194529_create_votes.rb new file mode 100644 index 0000000..947652b --- /dev/null +++ b/db/migrate/20231020194529_create_votes.rb @@ -0,0 +1,11 @@ +class CreateVotes < ActiveRecord::Migration[7.0] + def change + create_table :votes do |t| + t.references :votable, polymorphic: true + t.integer :upvote + t.string :ip + + t.timestamps + end + end +end diff --git a/db/migrate/20231020195330_make_blog_votable.rb b/db/migrate/20231020195330_make_blog_votable.rb new file mode 100644 index 0000000..4d1e42a --- /dev/null +++ b/db/migrate/20231020195330_make_blog_votable.rb @@ -0,0 +1,8 @@ +class MakeBlogVotable < ActiveRecord::Migration[7.0] + def change + change_table :blogs do |t| + t.integer :upvotes, default: 0, null: false + t.integer :downvotes, default: 0, null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 3a9e111..f8a8c49 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[7.0].define(version: 2023_10_17_153558) do +ActiveRecord::Schema[7.0].define(version: 2023_10_20_195330) do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -70,6 +70,8 @@ ActiveRecord::Schema[7.0].define(version: 2023_10_17_153558) do t.boolean "published", default: false, null: false t.datetime "published_at", precision: nil t.integer "user_id" + t.integer "upvotes", default: 0, null: false + t.integer "downvotes", default: 0, null: false t.index ["user_id"], name: "index_blogs_on_user_id" end @@ -393,6 +395,16 @@ ActiveRecord::Schema[7.0].define(version: 2023_10_17_153558) do 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.integer "upvote" + t.string "ip" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["votable_type", "votable_id"], name: "index_votes_on_votable" + end + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "blogs", "users" -- cgit 1.4.1