Skip to content
Open
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
20 changes: 20 additions & 0 deletions app/assets/stylesheets/pages/admin/_mission_submissions.scss
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,26 @@
resize: vertical;
}

&__detach {
display: flex;
flex-direction: column;
gap: var(--space-xxs);
}

&__detach-option {
display: flex;
align-items: center;
gap: var(--space-xs);
cursor: pointer;
font-size: 0.95rem;
}

&__detach-hint {
font-size: 0.85rem;
color: var(--muted);
margin: 0;
}

&__payout-note {
font-size: 0.9rem;
color: var(--color-brand-yellow);
Expand Down
22 changes: 21 additions & 1 deletion app/controllers/admin/missions/submissions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ def update
alert: "This submission can't be #{new_status} right now." and return
end

detach_requested = new_status == "rejected" &&
ActiveModel::Type::Boolean.new.cast(params.dig(:mission_submission, :detach_project))
detached = false

Mission::Submission.transaction do
if new_status == "approved"
@submission.update!(reviewed_by: current_user, reviewed_at: Time.current, rejection_message: nil)
Expand All @@ -102,14 +106,16 @@ def update
else
@submission.update!(reviewed_by: current_user, reviewed_at: Time.current, rejection_message: feedback)
@submission.reject!
detached = detach_submission_project! if detach_requested
end
end

notify_builder(new_status)

reviewed = Mission::Submission.reviewed_today(current_user, mission: @mission)
verdict = detached ? "Rejected and detached the project" : new_status.titleize
redirect_to next_admin_mission_submissions_path(mission_slug),
notice: "#{new_status.titleize}. That's #{reviewed} reviewed today."
notice: "#{verdict}. That's #{reviewed} reviewed today."
end

def next
Expand Down Expand Up @@ -281,6 +287,20 @@ def reverse_fixed_stardust_if_granted
)
end

# Detaches the submission's project from the mission it was rejected on,
# but only while that mission is still the project's current one — so we
# never yank a mission the builder has since swapped to. Returns whether
# a detach actually happened. The MissionAttachment change is versioned
# by PaperTrail (whodunnit set in the admin controller chain).
def detach_submission_project!
project = @submission.ship_event&.post&.project
return false unless project
return false unless project.current_mission == @submission.mission

project.detach_mission!
true
Comment thread
cursor[bot] marked this conversation as resolved.
end

def notify_builder(status)
builder = @submission.ship_event&.post&.user
return unless builder
Expand Down
11 changes: 11 additions & 0 deletions app/views/admin/missions/submissions/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@
placeholder: "Required for rejections. Optional for approvals." %>
</label>

<div class="mission-review__detach">
<label class="mission-review__detach-option">
<%= check_box_tag "mission_submission[detach_project]", "1", false %>
Also detach this project from the mission
</label>
<p class="mission-review__detach-hint">
Only applies when rejecting. Frees the project from <%= mission.name %> so
the builder can pick a different mission.
</p>
</div>

<% grants = [] %>
<% if submission.payout_path == "static_prize" %>
<% grants << "#{mission.fixed_stardust_payout} stardust" if mission.fixed_stardust_payout&.positive? %>
Expand Down
35 changes: 35 additions & 0 deletions test/controllers/admin/missions/submissions_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,41 @@ class Admin::Missions::SubmissionsControllerTest < ActionDispatch::IntegrationTe
assert @submission.reload.pending?
end

test "plain reject leaves the project attached to the mission" do
sign_in @reviewer
Mission::Submission.atomic_claim!(@submission.id, @reviewer)

patch admin_mission_submission_path(@mission.slug, @submission),
params: { mission_submission: { status: "rejected", feedback: "Needs work" } }

assert_redirected_to next_admin_mission_submissions_path(@mission.slug)
assert @submission.reload.rejected?
assert_equal @mission, @project.reload.current_mission
end

test "reject and detach frees the project from the mission" do
sign_in @reviewer
Mission::Submission.atomic_claim!(@submission.id, @reviewer)

patch admin_mission_submission_path(@mission.slug, @submission),
params: { mission_submission: { status: "rejected", feedback: "Needs work", detach_project: "1" } }

assert_redirected_to next_admin_mission_submissions_path(@mission.slug)
assert @submission.reload.rejected?
assert_nil @project.reload.current_mission
end

test "detach flag is ignored when approving" do
sign_in @reviewer
Mission::Submission.atomic_claim!(@submission.id, @reviewer)

patch admin_mission_submission_path(@mission.slug, @submission),
params: { mission_submission: { status: "approved", detach_project: "1" } }

assert @submission.reload.approved?
assert_equal @mission, @project.reload.current_mission
end

test "claims are exclusive while fresh and stealable when expired" do
other = User.create!(email: "other-#{SecureRandom.hex(4)}@example.test",
display_name: "other-#{SecureRandom.hex(4)}",
Expand Down