Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# This migration comes from co_plan (originally 20260422000000)
class ExpandPlanCollaboratorRoles < ActiveRecord::Migration[8.0]
def change
add_column :coplan_plan_collaborators, :approved_at, :datetime
add_column :coplan_plan_collaborators, :highlighted_reason, :text
end
end
18 changes: 10 additions & 8 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion engine/app/controllers/coplan/api/v1/plans_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,14 @@ def reference_json(ref)
end

def collaborator_json(collaborator)
{
json = {
id: collaborator.id,
user: user_json(collaborator.user),
role: collaborator.role
}
json[:approved_at] = collaborator.approved_at if collaborator.role == "approver"
json[:highlighted_reason] = collaborator.highlighted_reason if collaborator.role == "highlighted"
json
end

def snapshot_threads_json(threads)
Expand Down
27 changes: 26 additions & 1 deletion engine/app/models/coplan/plan_collaborator.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,37 @@
module CoPlan
class PlanCollaborator < ApplicationRecord
ROLES = %w[author reviewer viewer].freeze
ROLES = %w[author reviewer viewer approver highlighted].freeze

belongs_to :plan
belongs_to :user, class_name: "CoPlan::User"
belongs_to :added_by_user, class_name: "CoPlan::User", optional: true

validates :role, presence: true, inclusion: { in: ROLES }
validates :user_id, uniqueness: { scope: :plan_id }
validates :highlighted_reason, presence: true, if: -> { role == "highlighted" }

before_validation :clear_irrelevant_role_data

private

def clear_irrelevant_role_data
self.approved_at = nil unless role == "approver"
self.highlighted_reason = nil unless role == "highlighted"
end

public

scope :authors, -> { where(role: "author") }
scope :reviewers, -> { where(role: "reviewer") }
scope :approvers, -> { where(role: "approver") }
scope :highlighted, -> { where(role: "highlighted") }

def approve!
update!(approved_at: Time.current)
end

def approved?
approved_at.present?
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class ExpandPlanCollaboratorRoles < ActiveRecord::Migration[8.0]
def change
add_column :coplan_plan_collaborators, :approved_at, :datetime
add_column :coplan_plan_collaborators, :highlighted_reason, :text
end
end
9 changes: 9 additions & 0 deletions spec/factories/plan_collaborators.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,14 @@
plan
user { association(:coplan_user) }
role { "reviewer" }

trait :approver do
role { "approver" }
end

trait :highlighted do
role { "highlighted" }
highlighted_reason { "Domain expert" }
end
end
end
101 changes: 101 additions & 0 deletions spec/models/plan_collaborator_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
require "rails_helper"

RSpec.describe CoPlan::PlanCollaborator, type: :model do
it "is valid with valid attributes" do
collaborator = create(:plan_collaborator)
expect(collaborator).to be_valid
end

it "requires role" do
collaborator = build(:plan_collaborator, role: nil)
expect(collaborator).not_to be_valid
end

it "validates role inclusion" do
collaborator = build(:plan_collaborator, role: "admin")
expect(collaborator).not_to be_valid
end

it "allows all valid roles" do
plan = create(:plan)
CoPlan::PlanCollaborator::ROLES.each do |role|
user = create(:coplan_user)
attrs = { plan: plan, user: user, role: role }
attrs[:highlighted_reason] = "Top expert" if role == "highlighted"
collaborator = build(:plan_collaborator, **attrs)
expect(collaborator).to be_valid, "Expected role '#{role}' to be valid"
end
end

it "requires unique user per plan" do
collaborator = create(:plan_collaborator)
duplicate = build(:plan_collaborator, plan: collaborator.plan, user: collaborator.user)
expect(duplicate).not_to be_valid
expect(duplicate.errors[:user_id]).to include("has already been taken")
end

describe "highlighted role" do
it "requires highlighted_reason" do
collaborator = build(:plan_collaborator, role: "highlighted", highlighted_reason: nil)
expect(collaborator).not_to be_valid
expect(collaborator.errors[:highlighted_reason]).to include("can't be blank")
end

it "is valid with highlighted_reason" do
collaborator = build(:plan_collaborator, role: "highlighted", highlighted_reason: "Domain expert in payments")
expect(collaborator).to be_valid
end
end

describe "approver role" do
it "tracks approval via approve!" do
collaborator = create(:plan_collaborator, :approver)
expect(collaborator.approved?).to be false

collaborator.approve!
expect(collaborator.approved?).to be true
expect(collaborator.approved_at).to be_present
end
end

describe "role data cleanup" do
it "clears approved_at when role changes from approver" do
collaborator = create(:plan_collaborator, :approver)
collaborator.approve!
expect(collaborator.approved_at).to be_present

collaborator.update!(role: "reviewer")
expect(collaborator.approved_at).to be_nil
end

it "clears highlighted_reason when role changes from highlighted" do
collaborator = create(:plan_collaborator, :highlighted)
expect(collaborator.highlighted_reason).to be_present

collaborator.update!(role: "viewer")
expect(collaborator.highlighted_reason).to be_nil
end
end

describe "scopes" do
let(:plan) { create(:plan) }

it ".authors returns only author collaborators" do
author = create(:plan_collaborator, plan: plan, role: "author")
create(:plan_collaborator, plan: plan, role: "reviewer")
expect(plan.plan_collaborators.authors).to eq([author])
end

it ".approvers returns only approver collaborators" do
approver = create(:plan_collaborator, plan: plan, role: "approver")
create(:plan_collaborator, plan: plan, role: "viewer")
expect(plan.plan_collaborators.approvers).to eq([approver])
end

it ".highlighted returns only highlighted collaborators" do
highlighted = create(:plan_collaborator, plan: plan, role: "highlighted", highlighted_reason: "Expert")
create(:plan_collaborator, plan: plan, role: "author")
expect(plan.plan_collaborators.highlighted).to eq([highlighted])
end
end
end
Loading