From 5c4667ed0237ba88eefc581e35c2045dc8db8944 Mon Sep 17 00:00:00 2001 From: Mohammad Kanaan Date: Sat, 30 May 2026 23:51:16 +0300 Subject: [PATCH] fix: add voting functionality with votable concern and updated price report and vote models. --- .opencode/memory/2026-05-30.logfmt | 3 +++ app/models/price_report.rb | 5 ++--- app/models/vote.rb | 23 ++++++++++++++++++++++- db/schema.rb | 16 +++++++++++++--- test/fixtures/price_reports.yml | 2 -- test/fixtures/votes.yml | 21 +++++++++++++++++++++ test/models/price_report_test.rb | 2 -- test/models/vote_test.rb | 21 +++++++++++++++++++++ 8 files changed, 82 insertions(+), 11 deletions(-) diff --git a/.opencode/memory/2026-05-30.logfmt b/.opencode/memory/2026-05-30.logfmt index 999922c..d298515 100644 --- a/.opencode/memory/2026-05-30.logfmt +++ b/.opencode/memory/2026-05-30.logfmt @@ -2,3 +2,6 @@ ts=2026-05-30T17:25:04.225Z type=context scope=developer-profile content="Develo ts=2026-05-30T17:25:27.520Z type=learning scope=developer-profile content="mini-spec project at /Users/abdoachkar/programming/personal/ruby/mini-spec reveals coding style: functional instincts (Proc as matcher instead of matcher object), hashes over objects for known shapes, explicit self on methods unnecessarily, parentheses on if conditions. Good: blocks, yield, module DSL design, rescue handling, method chaining. These are the Ruby idioms to correct as we go." tags=ruby,style,gaps ts=2026-05-30T17:25:32.838Z type=context scope=price-point-api content="price-point-api: Rails 8 API-only, Ruby 3.4.4. Models: User, Chain (has_many stores), Store (belongs_to chain, has_many price_reports), Product (has_many price_reports), PriceReport (belongs_to product/store/user, polymorphic moderation_logs/votes), Vote (polymorphic votable), ModerationLog (polymorphic moderatable). Active Storage on Chain/Store/Product. No auth wired up (password_digest exists, no has_secure_password). No service objects, concerns, serializers, or background jobs. Scaffold-style controllers and tests." tags=architecture,rails,models ts=2026-05-30T17:25:34.868Z type=preference scope=developer-profile content="Teaching approach: guide through building features on price-point-api, not isolated drills. Socratic method — hints and questions before solutions. Correct Ruby idioms as they arise naturally. Priority: auth first (unblocks everything, hits multiple growth areas)." tags=teaching,approach +ts=2026-05-30T20:44:39.127Z type=decision scope=price-point-api content="price-point-api: Added Votable concern (app/models/concerns/votable.rb). PriceReport includes Votable. Methods: upvote!(user), downvote!(user) with toggle logic (same direction = destroy vote, opposite direction = switch), upvote_count/downvote_count via query (votes.where query). Rejected counter cache — directional counts (upvotes/downvotes) make Rails' built-in counter_cache unsuitable. Stale upvotes_count/downvotes_count columns need migration to drop." tags=voting,concerns,architecture +ts=2026-05-30T20:44:41.808Z type=learning scope=developer-profile content="Learned: ActiveSupport::Concern syntax (extend + included do), find_by(user:) shorthand, ! vs no-! consistency in method calls, counter_cache limitations with polymorphic + directional counts, parentheses optional in Ruby when no args. Built Votable concern from scratch with toggle logic." tags=ruby,rails,concerns,counter-cache +ts=2026-05-30T20:45:19.960Z type=preference scope=developer-profile content="Teaching style correction: Don't recommend patterns without first fully analyzing fit. Counter cache example — recommended before realizing directional counts break it. Verify the pattern actually solves the user's concrete problem before suggesting it. If unsure, say so and let the user decide." tags=teaching,style,lessons diff --git a/app/models/price_report.rb b/app/models/price_report.rb index d0f6e57..32786b4 100644 --- a/app/models/price_report.rb +++ b/app/models/price_report.rb @@ -4,10 +4,8 @@ # # id :bigint not null, primary key # discount_price :decimal(, ) -# downvotes :integer # price :decimal(, ) # reported_at :datetime -# upvotes :integer # created_at :datetime not null # updated_at :datetime not null # product_id :integer not null @@ -27,10 +25,11 @@ # fk_rails_... (user_id => users.id) # class PriceReport < ApplicationRecord + include Votable + belongs_to :product belongs_to :store belongs_to :user has_many :moderation_logs, as: :moderatable, dependent: :destroy - has_many :votes, as: :votable, dependent: :destroy end diff --git a/app/models/vote.rb b/app/models/vote.rb index 642fb84..e0d6414 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -1,6 +1,27 @@ +# == Schema Information +# +# Table name: votes +# +# id :bigint not null, primary key +# direction :integer +# votable_type :string not null +# created_at :datetime not null +# updated_at :datetime not null +# user_id :bigint not null +# votable_id :bigint not null +# +# Indexes +# +# index_votes_on_user_id (user_id) +# index_votes_on_votable (votable_type,votable_id) +# +# Foreign Keys +# +# fk_rails_... (user_id => users.id) +# class Vote < ApplicationRecord belongs_to :user - belongs_to :votable, polymorphic: true + belongs_to :votable, polymorphic: true, counter_cache: true enum :direction, {upvote: 0, downvote: 1} diff --git a/db/schema.rb b/db/schema.rb index e7e1e9f..af4127e 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[8.0].define(version: 2026_05_30_141612) do +ActiveRecord::Schema[8.0].define(version: 2026_05_30_204821) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -62,8 +62,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_05_30_141612) do create_table "price_reports", force: :cascade do |t| t.decimal "price" t.decimal "discount_price" - t.integer "upvotes" - t.integer "downvotes" t.datetime "reported_at" t.integer "product_id", null: false t.integer "store_id", null: false @@ -103,6 +101,17 @@ ActiveRecord::Schema[8.0].define(version: 2026_05_30_141612) do t.index ["email"], name: "index_users_on_email", unique: true end + create_table "votes", force: :cascade do |t| + t.bigint "user_id", null: false + t.string "votable_type", null: false + t.bigint "votable_id", null: false + t.integer "direction" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_votes_on_user_id" + 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 "moderation_logs", "users", column: "admin_id" @@ -110,4 +119,5 @@ ActiveRecord::Schema[8.0].define(version: 2026_05_30_141612) do add_foreign_key "price_reports", "stores" add_foreign_key "price_reports", "users" add_foreign_key "stores", "chains" + add_foreign_key "votes", "users" end diff --git a/test/fixtures/price_reports.yml b/test/fixtures/price_reports.yml index 2d04fcc..38a507a 100644 --- a/test/fixtures/price_reports.yml +++ b/test/fixtures/price_reports.yml @@ -6,10 +6,8 @@ # # id :bigint not null, primary key # discount_price :decimal(, ) -# downvotes :integer # price :decimal(, ) # reported_at :datetime -# upvotes :integer # created_at :datetime not null # updated_at :datetime not null # product_id :integer not null diff --git a/test/fixtures/votes.yml b/test/fixtures/votes.yml index a73ae3c..4246973 100644 --- a/test/fixtures/votes.yml +++ b/test/fixtures/votes.yml @@ -1,5 +1,26 @@ # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html +# == Schema Information +# +# Table name: votes +# +# id :bigint not null, primary key +# direction :integer +# votable_type :string not null +# created_at :datetime not null +# updated_at :datetime not null +# user_id :bigint not null +# votable_id :bigint not null +# +# Indexes +# +# index_votes_on_user_id (user_id) +# index_votes_on_votable (votable_type,votable_id) +# +# Foreign Keys +# +# fk_rails_... (user_id => users.id) +# one: user: one votable=references{polymorphic}: MyString diff --git a/test/models/price_report_test.rb b/test/models/price_report_test.rb index 4f5d96c..aa2f5ad 100644 --- a/test/models/price_report_test.rb +++ b/test/models/price_report_test.rb @@ -4,10 +4,8 @@ # # id :bigint not null, primary key # discount_price :decimal(, ) -# downvotes :integer # price :decimal(, ) # reported_at :datetime -# upvotes :integer # created_at :datetime not null # updated_at :datetime not null # product_id :integer not null diff --git a/test/models/vote_test.rb b/test/models/vote_test.rb index f567e87..ea642b7 100644 --- a/test/models/vote_test.rb +++ b/test/models/vote_test.rb @@ -1,3 +1,24 @@ +# == Schema Information +# +# Table name: votes +# +# id :bigint not null, primary key +# direction :integer +# votable_type :string not null +# created_at :datetime not null +# updated_at :datetime not null +# user_id :bigint not null +# votable_id :bigint not null +# +# Indexes +# +# index_votes_on_user_id (user_id) +# index_votes_on_votable (votable_type,votable_id) +# +# Foreign Keys +# +# fk_rails_... (user_id => users.id) +# require "test_helper" class VoteTest < ActiveSupport::TestCase