From 9e5974e06413246b77be1e87c0b620205450cc73 Mon Sep 17 00:00:00 2001 From: Kartikey Chauhan Date: Thu, 25 Dec 2025 07:21:08 +0530 Subject: [PATCH 1/9] inital clearing and comments - ignore --- app/models/project.rb | 72 +++++-------------------------------------- 1 file changed, 7 insertions(+), 65 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index f576936c..8827dffb 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -50,10 +50,11 @@ class Project < ApplicationRecord scope :deleted, -> { where.not(deleted_at: nil) } scope :fire, -> { where.not(marked_fire_at: nil) } - belongs_to :marked_fire_by, class_name: "User", optional: true - + # we're soft deleting! default_scope { kept } + belongs_to :marked_fire_by, class_name: "User", optional: true + has_many :memberships, class_name: "Project::Membership", dependent: :destroy has_many :users, through: :memberships has_many :hackatime_projects, class_name: "User::HackatimeProject", dependent: :nullify @@ -66,7 +67,9 @@ class Project < ApplicationRecord has_many :project_follows, dependent: :destroy has_many :followers, through: :project_follows, source: :user + # needs to be implemented has_one_attached :demo_video + # https://github.com/rails/rails/pull/39135 has_one_attached :banner do |attachable| # using resize_to_limit to preserve aspect ratio without cropping @@ -113,37 +116,6 @@ def validate_project_categories end end - scope :votable_by, ->(user) { - where.not(id: user.projects.select(:id)) - .where("NOT EXISTS ( - SELECT 1 FROM votes - WHERE votes.user_id = ? - AND votes.ship_event_id = latest_ship.id - )", user.id) - } - - scope :looking_for_votes, -> { - joins("INNER JOIN LATERAL ( - SELECT post_ship_events.id, post_ship_events.votes_count - FROM posts - INNER JOIN post_ship_events ON post_ship_events.id = posts.postable_id::bigint - WHERE posts.project_id = projects.id - AND posts.postable_type = 'Post::ShipEvent' - AND post_ship_events.payout IS NULL - AND post_ship_events.certification_status = 'approved' - AND post_ship_events.votes_count < #{Post::ShipEvent::VOTES_REQUIRED_FOR_PAYOUT} - ORDER BY posts.created_at DESC - LIMIT 1 - ) latest_ship ON true") - .where("EXISTS ( - SELECT 1 FROM project_memberships - INNER JOIN users ON users.id = project_memberships.user_id - WHERE project_memberships.project_id = projects.id - AND users.verification_status = 'verified' - )") - .order("latest_ship.votes_count ASC") - } - def time total_seconds = Rails.cache.fetch("project/#{id}/time_seconds", expires_in: 10.minutes) do Post::Devlog.where(id: posts.where(postable_type: "Post::Devlog").select("postable_id::bigint")).sum(:duration_seconds) || 0 @@ -155,8 +127,7 @@ def time OpenStruct.new(hours: hours, minutes: minutes) end - - + # this can probaby be better? def soft_delete! update!(deleted_at: Time.current) end @@ -168,40 +139,11 @@ def restore! def deleted? deleted_at.present? end + def hackatime_keys hackatime_projects.pluck(:name) end - def total_hackatime_hours - return 0 if hackatime_projects.empty? - - owner = memberships.owner.first&.user - return 0 unless owner - - result = owner.try_sync_hackatime_data! - return 0 unless result - - project_times = result[:projects] - total_seconds = hackatime_projects.sum { |hp| project_times[hp.name].to_i } - (total_seconds / 3600.0).round(1) - end - - def hackatime_projects_with_time - owner = memberships.owner.first&.user - return [] unless owner - - result = owner.try_sync_hackatime_data! - return [] unless result - - project_times = result[:projects] - hackatime_projects.map do |hp| - { - name: hp.name, - hours: (project_times[hp.name].to_i / 3600.0).round(1) - } - end - end - aasm column: :ship_status do state :draft, initial: true state :submitted From 156217b64b4a62fe022cd2469e89b866e42214eb Mon Sep 17 00:00:00 2001 From: Kartikey Chauhan Date: Thu, 25 Dec 2025 07:58:16 +0530 Subject: [PATCH 2/9] feat: switch to Project::Report --- app/controllers/admin/reports_controller.rb | 12 ++--- .../{ => project}/reports_controller.rb | 10 ++-- app/models/project.rb | 2 +- app/models/project/report.rb | 48 +++++++++++++++++++ app/models/report.rb | 48 ------------------- app/models/user.rb | 2 +- app/views/admin/application/index.html.erb | 2 +- app/views/admin/reports/index.html.erb | 2 +- app/views/projects/show.html.erb | 2 +- app/views/votes/_project_showcase.html.erb | 2 +- config/routes.rb | 3 +- ...20344_rename_reports_to_project_reports.rb | 5 ++ db/schema.rb | 32 ++++++------- 13 files changed, 87 insertions(+), 83 deletions(-) rename app/controllers/{ => project}/reports_controller.rb (64%) create mode 100644 app/models/project/report.rb delete mode 100644 app/models/report.rb create mode 100644 db/migrate/20251225020344_rename_reports_to_project_reports.rb diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index 2c7a061a..2bf35187 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -5,15 +5,15 @@ class ReportsController < Admin::ApplicationController def index authorize :admin, :access_reports? - @reports = Report.includes(:reporter, :project).order(created_at: :desc) + @reports = Project::Report.includes(:reporter, :project).order(created_at: :desc) @reports = @reports.where(status: params[:status]) if params[:status].present? @reports = @reports.where(reason: params[:reason]) if params[:reason].present? @counts = { - pending: Report.pending.count, - reviewed: Report.reviewed.count, - dismissed: Report.dismissed.count + pending: Project::Report.pending.count, + reviewed: Project::Report.reviewed.count, + dismissed: Project::Report.dismissed.count } end @@ -34,7 +34,7 @@ def dismiss private def set_report - @report = Report.find(params[:id]) + @report = Project::Report.find(params[:id]) end def update_status(new_status, notice_message) @@ -42,7 +42,7 @@ def update_status(new_status, notice_message) if @report.update(status: new_status) PaperTrail::Version.create!( - item_type: "Report", + item_type: "Project::Report", item_id: @report.id, event: "update", whodunnit: current_user.id, diff --git a/app/controllers/reports_controller.rb b/app/controllers/project/reports_controller.rb similarity index 64% rename from app/controllers/reports_controller.rb rename to app/controllers/project/reports_controller.rb index 3753f115..399b74ac 100644 --- a/app/controllers/reports_controller.rb +++ b/app/controllers/project/reports_controller.rb @@ -1,7 +1,7 @@ -class ReportsController < ApplicationController +class Project::ReportsController < ApplicationController def create authorize :report, :create? - @project = Project.find(params[:project_id]) + @project = ::Project.find(params[:project_id]) @report = current_user.reports.build(report_params.merge(project: @project)) @@ -14,7 +14,7 @@ def create private - def report_params - params.require(:report).permit(:reason, :details) - end + def report_params + params.require(:project_report).permit(:reason, :details) + end end diff --git a/app/models/project.rb b/app/models/project.rb index 8827dffb..ffc899d4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -63,7 +63,7 @@ class Project < ApplicationRecord has_many :ship_posts, -> { where(postable_type: "Post::ShipEvent").order(created_at: :desc) }, class_name: "Post" has_one :latest_ship_post, -> { where(postable_type: "Post::ShipEvent").order(created_at: :desc) }, class_name: "Post" has_many :votes, dependent: :destroy - has_many :reports, dependent: :destroy + has_many :reports, class_name: "Project::Report", dependent: :destroy has_many :project_follows, dependent: :destroy has_many :followers, through: :project_follows, source: :user diff --git a/app/models/project/report.rb b/app/models/project/report.rb new file mode 100644 index 00000000..b759c112 --- /dev/null +++ b/app/models/project/report.rb @@ -0,0 +1,48 @@ +# == Schema Information +# +# Table name: project_reports +# +# id :bigint not null, primary key +# details :text not null +# reason :string not null +# status :integer default("pending"), not null +# created_at :datetime not null +# updated_at :datetime not null +# project_id :bigint not null +# reporter_id :bigint not null +# +# Indexes +# +# index_project_reports_on_project_id (project_id) +# index_project_reports_on_reporter_id (reporter_id) +# index_project_reports_on_reporter_id_and_project_id (reporter_id,project_id) UNIQUE +# +# Foreign Keys +# +# fk_rails_... (project_id => projects.id) +# fk_rails_... (reporter_id => users.id) +# +class Project::Report < ApplicationRecord + belongs_to :reporter, class_name: "User" + belongs_to :project + after_create :notify_slack_channel + + REASONS = %w[low_effort undeclared_ai demo_broken other].freeze + + enum :status, { pending: 0, reviewed: 1, dismissed: 2 }, default: :pending + + validates :reason, presence: true, inclusion: { in: REASONS } + validates :details, presence: true, length: { minimum: 20 } + validates :reporter_id, uniqueness: { scope: :project_id, message: "has already reported this project" } + + validates :reporter, exclusion: { + in: ->(report) { report.project&.users || [] }, + message: "cannot report own project" + }, unless: -> { Rails.env.development? } + + private + + def notify_slack_channel + SendSlackDmJob.perform_later("C0A1YJ9PDAS", "New report received", blocks_path: "notifications/reports/slack_message", locals: { report: self }) + end +end diff --git a/app/models/report.rb b/app/models/report.rb deleted file mode 100644 index f72af536..00000000 --- a/app/models/report.rb +++ /dev/null @@ -1,48 +0,0 @@ -# == Schema Information -# -# Table name: reports -# -# id :bigint not null, primary key -# details :text not null -# reason :string not null -# status :integer default("pending"), not null -# created_at :datetime not null -# updated_at :datetime not null -# project_id :bigint not null -# reporter_id :bigint not null -# -# Indexes -# -# index_reports_on_project_id (project_id) -# index_reports_on_reporter_id (reporter_id) -# index_reports_on_reporter_id_and_project_id (reporter_id,project_id) UNIQUE -# -# Foreign Keys -# -# fk_rails_... (project_id => projects.id) -# fk_rails_... (reporter_id => users.id) -# -class Report < ApplicationRecord - belongs_to :reporter, class_name: "User" - belongs_to :project - after_create :share_with_channel - - REASONS = %w[low_effort undeclared_ai demo_broken other].freeze - - enum :status, { pending: 0, reviewed: 1, dismissed: 2 }, default: :pending - - validates :reason, presence: true, inclusion: { in: REASONS } - validates :details, presence: true, length: { minimum: 20 } - validates :reporter_id, uniqueness: { scope: :project_id, message: "has already reported this project" } - - validate :reporter_cannot_report_own_project, unless: -> { Rails.env.development? } - - private - - def reporter_cannot_report_own_project - errors.add(:reporter, "cannot report own project") if project&.users&.exists?(reporter_id) - end - def share_with_channel - SendSlackDmJob.perform_later("C0A1YJ9PDAS", "New report received", blocks_path: "notifications/reports/slack_message", locals: { report: self }) - end -end diff --git a/app/models/user.rb b/app/models/user.rb index cbfbb09b..8d31089d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -52,7 +52,7 @@ class User < ApplicationRecord has_many :hackatime_projects, class_name: "User::HackatimeProject", dependent: :destroy has_many :shop_orders, dependent: :destroy has_many :votes, dependent: :destroy - has_many :reports, foreign_key: :reporter_id, dependent: :destroy + has_many :reports, class_name: "Project::Report", foreign_key: :reporter_id, dependent: :destroy has_many :likes, dependent: :destroy has_many :comments, dependent: :destroy has_many :ledger_entries, dependent: :destroy diff --git a/app/views/admin/application/index.html.erb b/app/views/admin/application/index.html.erb index a789b78a..a343b1ba 100644 --- a/app/views/admin/application/index.html.erb +++ b/app/views/admin/application/index.html.erb @@ -17,7 +17,7 @@ <%= link_to "Audit Logs", admin_audit_logs_path, class: button_class %> <% end %> <% if policy(:admin).access_reports? %> - <% pending_reports = Report.pending.count %> + <% pending_reports = Project::Report.pending.count %> <%= link_to admin_reports_path, class: button_class do %> Reports <% if pending_reports > 0 %> diff --git a/app/views/admin/reports/index.html.erb b/app/views/admin/reports/index.html.erb index ad9be59c..6cd32461 100644 --- a/app/views/admin/reports/index.html.erb +++ b/app/views/admin/reports/index.html.erb @@ -27,7 +27,7 @@
<%= f.label :reason, "Reason" %> - <%= f.select :reason, options_for_select(Report::REASONS.map { |r| [r.titleize, r] }, params[:reason]), { include_blank: "All" }, class: "form-control" %> + <%= f.select :reason, options_for_select(Project::Report::REASONS.map { |r| [r.titleize, r] }, params[:reason]), { include_blank: "All" }, class: "form-control" %>
diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb index cc29c8b0..f0e6e0ba 100644 --- a/app/views/projects/show.html.erb +++ b/app/views/projects/show.html.erb @@ -72,7 +72,7 @@ <%= render layout: "shared/modal", locals: { id: "project-report-modal", title: "Report project?" } do %>

We want good, high quality projects in Flavortown. If you believe this project violates our community guidelines, please let us know why:

- <%= form_with model: Report.new, url: reports_path(project_id: @project.id), local: true do |f| %> + <%= form_with model: Project::Report.new, url: project_reports_path(@project), local: true do |f| %>
<%= f.radio_button :reason, "low_effort", id: "project_report_reason_low_effort", required: true %> diff --git a/app/views/votes/_project_showcase.html.erb b/app/views/votes/_project_showcase.html.erb index f3052d42..7f11f3b7 100644 --- a/app/views/votes/_project_showcase.html.erb +++ b/app/views/votes/_project_showcase.html.erb @@ -59,7 +59,7 @@ <%= render layout: "shared/modal", locals: { id: "report-modal", title: "Report project?" } do %>

We want good, high quality projects in Flavortown. If you believe this project violates our community guidelines, please let us know why:

- <%= form_with model: Report.new, url: reports_path(project_id: project.id), local: true do |f| %> + <%= form_with model: Project::Report.new, url: project_reports_path(project), local: true do |f| %>
<%= f.radio_button :reason, "low_effort", id: "report_reason_low_effort", required: true %> diff --git a/config/routes.rb b/config/routes.rb index b1d03476..793df41a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -66,8 +66,6 @@ def self.matches?(request) get "explore/gallery", to: "explore#gallery", as: :explore_gallery get "explore/following", to: "explore#following", as: :explore_following - # Reports - resources :reports, only: [ :create ] # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live. @@ -234,6 +232,7 @@ def self.matches?(request) resources :projects, shallow: true do resources :memberships, only: [ :create, :destroy ], module: :project resources :devlogs, only: [ :new, :create ], module: :project + resources :reports, only: [ :create ], module: :project member do get :ship patch :update_ship diff --git a/db/migrate/20251225020344_rename_reports_to_project_reports.rb b/db/migrate/20251225020344_rename_reports_to_project_reports.rb new file mode 100644 index 00000000..ecfaf75f --- /dev/null +++ b/db/migrate/20251225020344_rename_reports_to_project_reports.rb @@ -0,0 +1,5 @@ +class RenameReportsToProjectReports < ActiveRecord::Migration[8.1] + def change + rename_table :reports, :project_reports + end +end diff --git a/db/schema.rb b/db/schema.rb index 6508e161..5fa0fe58 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.1].define(version: 2025_12_23_033610) do +ActiveRecord::Schema[8.1].define(version: 2025_12_25_020344) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -305,6 +305,19 @@ t.index ["user_id"], name: "index_project_memberships_on_user_id" end + create_table "project_reports", force: :cascade do |t| + t.datetime "created_at", null: false + t.text "details", null: false + t.bigint "project_id", null: false + t.string "reason", null: false + t.bigint "reporter_id", null: false + t.integer "status", default: 0, null: false + t.datetime "updated_at", null: false + t.index ["project_id"], name: "index_project_reports_on_project_id" + t.index ["reporter_id", "project_id"], name: "index_project_reports_on_reporter_id_and_project_id", unique: true + t.index ["reporter_id"], name: "index_project_reports_on_reporter_id" + end + create_table "projects", force: :cascade do |t| t.datetime "created_at", null: false t.datetime "deleted_at" @@ -329,19 +342,6 @@ t.index ["marked_fire_by_id"], name: "index_projects_on_marked_fire_by_id" end - create_table "reports", force: :cascade do |t| - t.datetime "created_at", null: false - t.text "details", null: false - t.bigint "project_id", null: false - t.string "reason", null: false - t.bigint "reporter_id", null: false - t.integer "status", default: 0, null: false - t.datetime "updated_at", null: false - t.index ["project_id"], name: "index_reports_on_project_id" - t.index ["reporter_id", "project_id"], name: "index_reports_on_reporter_id_and_project_id", unique: true - t.index ["reporter_id"], name: "index_reports_on_reporter_id" - end - create_table "rsvps", force: :cascade do |t| t.datetime "created_at", null: false t.string "email", null: false @@ -589,9 +589,9 @@ add_foreign_key "project_follows", "users" add_foreign_key "project_memberships", "projects" add_foreign_key "project_memberships", "users" + add_foreign_key "project_reports", "projects" + add_foreign_key "project_reports", "users", column: "reporter_id" add_foreign_key "projects", "users", column: "marked_fire_by_id" - add_foreign_key "reports", "projects" - add_foreign_key "reports", "users", column: "reporter_id" add_foreign_key "shop_card_grants", "shop_items" add_foreign_key "shop_card_grants", "users" add_foreign_key "shop_items", "users" From 846f1fda0f76e846f1ab55ec5dbea31bc820145e Mon Sep 17 00:00:00 2001 From: Kartikey Chauhan Date: Thu, 25 Dec 2025 07:58:47 +0530 Subject: [PATCH 3/9] chore: disable voting --- app/controllers/votes_controller.rb | 43 +++-------------------------- 1 file changed, 4 insertions(+), 39 deletions(-) diff --git a/app/controllers/votes_controller.rb b/app/controllers/votes_controller.rb index a3634a3e..3c265195 100644 --- a/app/controllers/votes_controller.rb +++ b/app/controllers/votes_controller.rb @@ -23,49 +23,14 @@ def index def new authorize :vote, :new? - project_id = Project.looking_for_votes - .votable_by(current_user) - .limit(10) - .pluck(:id) - .sample - - @project = Project.find_by(id: project_id) - @devlogs = if @project - @project.posts.includes(:postable, :user).order(created_at: :desc).limit(5) - else - [] - end + redirect_to root_path, alert: "Voting is currently disabled." + return end def create authorize :vote, :create? - @project = Project.find(params[:project_id]) - - created_votes = [] - - Vote.transaction do - votes_params = params.require(:votes) - - Rails.logger.info "VOTE PARAMS: time=#{params[:time_taken_to_vote]}, repo=#{params[:repo_url_clicked]}, demo=#{params[:demo_url_clicked]}" - - votes_params.values.each do |vote_params| - created_votes << current_user.votes.create!( - project: @project, - category: vote_params[:category], - score: vote_params[:score], - time_taken_to_vote: params[:time_taken_to_vote].to_i, - repo_url_clicked: params[:repo_url_clicked] == "true", - demo_url_clicked: params[:demo_url_clicked] == "true", - reason: params[:reason].presence - ) - end - end - - share_vote_to_slack(created_votes, params[:reason].presence) if current_user.send_votes_to_slack - - redirect_to new_vote_path, notice: "Voted!" - rescue ActiveRecord::RecordInvalid => e - redirect_to new_vote_path, alert: e.record.errors.full_messages.to_sentence + redirect_to root_path, alert: "Voting is currently disabled." + return end private From d8320bcccd34cf8c5cc0048d150ac26913d5e6f9 Mon Sep 17 00:00:00 2001 From: Kartikey Chauhan Date: Thu, 25 Dec 2025 08:06:22 +0530 Subject: [PATCH 4/9] fix: redirect_back_or_to instead of redirect_to + flash if duplicate report --- app/controllers/project/reports_controller.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/controllers/project/reports_controller.rb b/app/controllers/project/reports_controller.rb index 399b74ac..0787a8b7 100644 --- a/app/controllers/project/reports_controller.rb +++ b/app/controllers/project/reports_controller.rb @@ -3,12 +3,17 @@ def create authorize :report, :create? @project = ::Project.find(params[:project_id]) + if current_user.reports.exists?(project: @project) + redirect_back_or_to project_path(@project), alert: "You have already reported this project." + return + end + @report = current_user.reports.build(report_params.merge(project: @project)) if @report.save - redirect_to new_vote_path, notice: "Report submitted. Thank you for helping us maintain quality." + redirect_back_or_to project_path(@project), notice: "Report submitted. Thank you for helping us maintain quality." else - redirect_to new_vote_path, alert: @report.errors.full_messages.to_sentence + redirect_back_or_to project_path(@project), alert: @report.errors.full_messages.to_sentence end end From 752374521b08227c960ea7a9d03c62ed8e81d9ac Mon Sep 17 00:00:00 2001 From: Kartikey Chauhan Date: Thu, 25 Dec 2025 08:06:38 +0530 Subject: [PATCH 5/9] lint --- app/controllers/votes_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/votes_controller.rb b/app/controllers/votes_controller.rb index 3c265195..6f057a23 100644 --- a/app/controllers/votes_controller.rb +++ b/app/controllers/votes_controller.rb @@ -24,13 +24,13 @@ def index def new authorize :vote, :new? redirect_to root_path, alert: "Voting is currently disabled." - return + nil end def create authorize :vote, :create? redirect_to root_path, alert: "Voting is currently disabled." - return + nil end private From 113618d5a05996dfcc3720f6b910bb4d3296a4e4 Mon Sep 17 00:00:00 2001 From: Kartikey Chauhan Date: Thu, 25 Dec 2025 08:07:49 +0530 Subject: [PATCH 6/9] temp add back --- app/models/project.rb | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/app/models/project.rb b/app/models/project.rb index ffc899d4..29656e3f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -144,6 +144,36 @@ def hackatime_keys hackatime_projects.pluck(:name) end + def total_hackatime_hours + return 0 if hackatime_projects.empty? + + owner = memberships.owner.first&.user + return 0 unless owner + + result = owner.try_sync_hackatime_data! + return 0 unless result + + project_times = result[:projects] + total_seconds = hackatime_projects.sum { |hp| project_times[hp.name].to_i } + (total_seconds / 3600.0).round(1) + end + + def hackatime_projects_with_time + owner = memberships.owner.first&.user + return [] unless owner + + result = owner.try_sync_hackatime_data! + return [] unless result + + project_times = result[:projects] + hackatime_projects.map do |hp| + { + name: hp.name, + hours: (project_times[hp.name].to_i / 3600.0).round(1) + } + end + end + aasm column: :ship_status do state :draft, initial: true state :submitted From c128307b3d906fb8f3b5a193ccdda942f433e00e Mon Sep 17 00:00:00 2001 From: Kartikey Chauhan Date: Thu, 25 Dec 2025 22:22:17 +0530 Subject: [PATCH 7/9] refactor: move mark_fire/unmark_fire to its own controller --- .../project/fire_events_controller.rb | 50 +++++++++++++++ app/controllers/projects_controller.rb | 61 +------------------ config/routes.rb | 3 +- 3 files changed, 52 insertions(+), 62 deletions(-) create mode 100644 app/controllers/project/fire_events_controller.rb diff --git a/app/controllers/project/fire_events_controller.rb b/app/controllers/project/fire_events_controller.rb new file mode 100644 index 00000000..f4d97c3b --- /dev/null +++ b/app/controllers/project/fire_events_controller.rb @@ -0,0 +1,50 @@ +class Project::FireEventsController < ApplicationController + before_action -> { authorize :admin } + before_action :set_project! + + def create + if @project.fire? + return render json: { message: "Project is already marked as 🔥" }, status: :unprocessable_entity + end + + fire_event = Post::FireEvent.new(body: "🔥 #{current_user.display_name} marked your project as well cooked! As a prize for your nicely cooked project, look out for a bonus prize in the mail :)") + post = @project.posts.build(user: current_user, postable: fire_event) + + PaperTrail.request(whodunnit: current_user.id) do + unless post.save + errors = (post.errors.full_messages + fire_event.errors.full_messages).uniq + return render json: { message: errors.to_sentence.presence || "Failed to mark project as 🔥" }, status: :unprocessable_entity + end + + @project.mark_fire!(current_user) + log_version!("mark_fire", marked_fire_by_id: current_user.id, created_post_id: post.id) + end + + Project::PostToMagicJob.perform_later(@project) + Project::MagicHappeningLetterJob.perform_later(@project) + render json: { message: "Project marked as 🔥!", fire: true } + end + + def destroy + PaperTrail.request(whodunnit: current_user.id) do + @project.unmark_fire! + log_version!("unmark_fire") + end + render json: { message: "Project unmarked as 🔥", fire: false } + end + + private + + def set_project! + @project = Project.find_by(id: params[:project_id]) + render json: { message: "Project not found" }, status: :not_found unless @project + end + + def log_version!(event, changes = {}) + PaperTrail::Version.create!( + item_type: "Project", item_id: @project.id, event: event, whodunnit: current_user.id, + object_changes: { admin_action: [ nil, event ], **changes.transform_values { [ nil, _1 ] } } + ) + end +end + \ No newline at end of file diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 9177950f..9d901020 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,5 +1,5 @@ class ProjectsController < ApplicationController - before_action :set_project_minimal, only: [ :edit, :update, :destroy, :ship, :update_ship, :submit_ship, :mark_fire, :unmark_fire ] + before_action :set_project_minimal, only: [ :edit, :update, :destroy, :ship, :update_ship, :submit_ship ] before_action :set_project, only: [ :show ] def index @@ -199,65 +199,6 @@ def submit_ship redirect_to @project end - def mark_fire - authorize :admin, :manage_projects? - - return render(json: { message: "Project not found" }, status: :not_found) unless @project - - PaperTrail.request(whodunnit: current_user.id) do - fire_event = Post::FireEvent.new( - body: "🔥 #{current_user.display_name} marked your project as well cooked! As a prize for your nicely cooked project, look out for a bonus prize in the mail :)" - ) - post = @project.posts.build(user: current_user, postable: fire_event) - - if post.save - @project.mark_fire!(current_user) - - PaperTrail::Version.create!( - item_type: "Project", - item_id: @project.id, - event: "mark_fire", - whodunnit: current_user.id, - object_changes: { - admin_action: [ nil, "mark_fire" ], - marked_fire_by_id: [ nil, current_user.id ], - created_post_id: [ nil, post.id ] - } - ) - - Project::PostToMagicJob.perform_later(@project) - Project::MagicHappeningLetterJob.perform_later(@project) - - render json: { message: "Project marked as 🔥!", fire: true }, status: :ok - else - errors = (post.errors.full_messages + fire_event.errors.full_messages).uniq - render json: { message: errors.to_sentence.presence || "Failed to mark project as 🔥" }, status: :unprocessable_entity - end - end - end - - def unmark_fire - authorize :admin, :manage_projects? - - return render(json: { message: "Project not found" }, status: :not_found) unless @project - - PaperTrail.request(whodunnit: current_user.id) do - @project.unmark_fire! - - PaperTrail::Version.create!( - item_type: "Project", - item_id: @project.id, - event: "unmark_fire", - whodunnit: current_user.id, - object_changes: { - admin_action: [ nil, "unmark_fire" ] - } - ) - - render json: { message: "Project unmarked as 🔥", fire: false }, status: :ok - end - end - def follow return redirect_to(project_path(params[:id]), alert: "Please sign in first.") unless current_user diff --git a/config/routes.rb b/config/routes.rb index 793df41a..56f6bfdc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -233,12 +233,11 @@ def self.matches?(request) resources :memberships, only: [ :create, :destroy ], module: :project resources :devlogs, only: [ :new, :create ], module: :project resources :reports, only: [ :create ], module: :project + resources :fire_event, only: [ :create, :destroy], module: :project member do get :ship patch :update_ship post :submit_ship - post :mark_fire - post :unmark_fire post :follow delete :unfollow post :resend_webhook From 1f98d55e1a04606ef53872bdf9d498fe1db8d6b2 Mon Sep 17 00:00:00 2001 From: Kartikey Chauhan Date: Thu, 25 Dec 2025 22:22:42 +0530 Subject: [PATCH 8/9] fix: only one fire post per person and delete the post if unmarked as fire --- app/models/project.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index 29656e3f..542d0e20 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -62,6 +62,7 @@ class Project < ApplicationRecord has_many :devlogs, -> { where(postable_type: "Post::Devlog") }, class_name: "Post" has_many :ship_posts, -> { where(postable_type: "Post::ShipEvent").order(created_at: :desc) }, class_name: "Post" has_one :latest_ship_post, -> { where(postable_type: "Post::ShipEvent").order(created_at: :desc) }, class_name: "Post" + has_one :fire_post, -> { where(postable_type: "Post::FireEvent") }, class_name: "Post" has_many :votes, dependent: :destroy has_many :reports, class_name: "Project::Report", dependent: :destroy has_many :project_follows, dependent: :destroy @@ -230,11 +231,16 @@ def fire? end def mark_fire!(user) - update!(marked_fire_at: Time.current, marked_fire_by: user) + raise ArgumentError, "Project is already marked as fire" if fire? + + update!(marked_fire_at: Time.current, marked_fire_by: user) end def unmark_fire! + transaction do + fire_post&.destroy! update!(marked_fire_at: nil, marked_fire_by: nil) + end end def shipping_validations From facdf1f18ab13fbd766f59f9471af871dbb157fe Mon Sep 17 00:00:00 2001 From: Kartikey Chauhan Date: Thu, 25 Dec 2025 22:23:07 +0530 Subject: [PATCH 9/9] remove redudant stimulus controller --- app/javascript/controllers/index.js | 3 - .../controllers/project_fire_controller.js | 61 ------------------- 2 files changed, 64 deletions(-) delete mode 100644 app/javascript/controllers/project_fire_controller.js diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index aa17a881..521ce3e4 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -49,9 +49,6 @@ application.register("load-more", LoadMoreController); import DialogueIterationController from "./dialogue_iteration_controller"; application.register("dialogue-iteration", DialogueIterationController); -import ProjectFireController from "./project_fire_controller"; -application.register("project-fire", ProjectFireController); - import ModalController from "./modal_controller"; application.register("modal", ModalController); diff --git a/app/javascript/controllers/project_fire_controller.js b/app/javascript/controllers/project_fire_controller.js deleted file mode 100644 index c691e81a..00000000 --- a/app/javascript/controllers/project_fire_controller.js +++ /dev/null @@ -1,61 +0,0 @@ -import { Controller } from "@hotwired/stimulus"; - -export default class extends Controller { - static values = { projectId: Number, isFire: Boolean }; - static targets = ["text"]; - - async toggle(event) { - event.preventDefault(); - - const projectId = this.projectIdValue; - if (!projectId) return; - - const endpoint = this.isFireValue ? "unmark_fire" : "mark_fire"; - - const token = document - .querySelector('meta[name="csrf-token"]') - ?.getAttribute("content"); - - try { - const resp = await fetch(`/projects/${projectId}/${endpoint}`, { - method: "POST", - headers: { - Accept: "application/json", - ...(token ? { "X-CSRF-Token": token } : {}), - }, - }); - - const raw = await resp.text(); - let payload = {}; - try { - payload = JSON.parse(raw); - } catch { - payload = { message: raw }; - } - - if (!resp.ok) { - console.error(`${endpoint} failed`, resp.status, payload); - alert(payload.message || `Failed (${resp.status})`); - return; - } - - this.isFireValue = !this.isFireValue; - this.updateButtonText(); - - alert( - payload.message || (this.isFireValue ? "Marked as 🔥" : "Unmarked 🔥"), - ); - } catch (e) { - console.error(e); - alert("Request failed"); - } - } - - updateButtonText() { - const button = this.element; - const textSpan = button.querySelector(".button__text"); - if (textSpan) { - textSpan.textContent = this.isFireValue ? "Unmark 🔥" : "Mark as 🔥"; - } - } -}