From da586ae3d3bba7900e6b5934ee115e6d094f9fc2 Mon Sep 17 00:00:00 2001 From: Mohammad Kanaan Date: Sat, 30 May 2026 23:23:13 +0300 Subject: [PATCH] feat: add votable concern --- .opencode/memory/2026-05-30.logfmt | 4 +++ app/models/concerns/votable.rb | 42 ++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 .opencode/memory/2026-05-30.logfmt create mode 100644 app/models/concerns/votable.rb diff --git a/.opencode/memory/2026-05-30.logfmt b/.opencode/memory/2026-05-30.logfmt new file mode 100644 index 0000000..999922c --- /dev/null +++ b/.opencode/memory/2026-05-30.logfmt @@ -0,0 +1,4 @@ +ts=2026-05-30T17:25:04.225Z type=context scope=developer-profile content="Developer Profile - Abdo Achkar: TS dev 2yr, mostly React frontend. Backend: Next.js server actions, basic Laravel ecommerce (admin middleware, no policies). Light Java/C++, solid OOP but functional instincts. Ruby beginner-intermediate (built mini-spec DSL with blocks/Procs/modules). Rails early (understands AR, polymorphic, nested routes, no auth yet). Project: price-point-api Rails 8 API-only crowdsourced price reporting. Learning goal: deep Rails understanding through building features. Growth areas: 1) Ruby idioms 2) Rails conventions 3) AR depth 4) testing 5) auth." tags=profile,developer,ruby,rails +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 diff --git a/app/models/concerns/votable.rb b/app/models/concerns/votable.rb new file mode 100644 index 0000000..b11bf4f --- /dev/null +++ b/app/models/concerns/votable.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Votable + extend ActiveSupport::Concern + included do + has_many :votes, as: :votable, dependent: :destroy + end + + def upvote!(user) + vote = votes.find_by(user:) + if vote + if vote.upvote? + vote.destroy! + else + vote.update!(direction: :upvote) + end + else + Vote.create!(user: user, votable: self, direction: :upvote) + end + end + + def downvote!(user) + vote = votes.find_by(user:) + if vote + if vote.downvote? + vote.destroy! + else + vote.update!(direction: :downvote) + end + else + Vote.create!(user: user, votable: self, direction: :downvote) + end + end + + def upvote_count + votes.where(direction: :upvote).count + end + + def downvote_count + votes.where(direction: :downvote).count + end +end