From 460daf36f9b345f09ef569dfd0d0ec55f98cb3ec Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 30 Nov 2025 15:05:13 +1030 Subject: [PATCH] Add revert functionality to admin crops page (#4346) * feat(admin): add revert functionality to crops page This change adds a "Revert" button to the admin crops page, allowing crop wranglers to revert changes to a previous version. It introduces a new `Admin::VersionsController` with a `revert` action that uses `paper_trail`'s `reify` method to restore a previous version of a `Crop` object. The view is updated to include a "Revert" button, which is guarded by a `can?(:wrangle, Crop)` check to ensure only authorized users can see it. The controller also includes an authorization check to prevent unauthorized users from accessing the revert action directly. A feature spec is added to test the new functionality, including the authorization logic. * Consistent UX * Specs --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: Daniel O'Connor --- app/controllers/admin/crops_controller.rb | 1 + app/controllers/admin/versions_controller.rb | 24 ++++++ app/views/admin/crops/index.html.haml | 91 ++++++++++++-------- config/routes.rb | 3 + spec/features/admin/reverting_crops_spec.rb | 38 ++++++++ 5 files changed, 119 insertions(+), 38 deletions(-) create mode 100644 app/controllers/admin/versions_controller.rb create mode 100644 spec/features/admin/reverting_crops_spec.rb diff --git a/app/controllers/admin/crops_controller.rb b/app/controllers/admin/crops_controller.rb index 596071cf6..8a498db62 100644 --- a/app/controllers/admin/crops_controller.rb +++ b/app/controllers/admin/crops_controller.rb @@ -8,6 +8,7 @@ class Admin::CropsController < ApplicationController @versions = PaperTrail::Version.where(item_type: 'Crop').order(created_at: :desc).limit(100) member_ids = @versions.map(&:whodunnit).compact.map(&:to_i) @members = Member.where(id: member_ids).index_by(&:id) + @crop_wranglers = Role.crop_wranglers end private diff --git a/app/controllers/admin/versions_controller.rb b/app/controllers/admin/versions_controller.rb new file mode 100644 index 000000000..ff2691642 --- /dev/null +++ b/app/controllers/admin/versions_controller.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Admin + class VersionsController < ApplicationController + before_action :authenticate_member! + before_action :authorize_admin! + + def revert + @version = PaperTrail::Version.find(params[:id]) + @object = @version.reify + if @object.save + redirect_to admin_crops_path, notice: "Reverted to version from #{@version.created_at.strftime('%B %d, %Y')}" + else + redirect_to admin_crops_path, alert: "Could not revert to version from #{@version.created_at.strftime('%B %d, %Y')}. Errors: #{@object.errors.full_messages.to_sentence}" + end + end + + private + + def authorize_admin! + authorize! :wrangle, Crop + end + end +end diff --git a/app/views/admin/crops/index.html.haml b/app/views/admin/crops/index.html.haml index a62cda0ae..ce410b93b 100644 --- a/app/views/admin/crops/index.html.haml +++ b/app/views/admin/crops/index.html.haml @@ -1,41 +1,56 @@ -- content_for :title, "Crop Wrangler Admin" -- content_for :breadcrumbs do - %li.breadcrumb-item= link_to 'Admin', '#' - %li.breadcrumb-item.active Crop Wrangler +- content_for :title, "Crop Wrangling" -%h1 Crop Wrangler Admin +%h1 Crop Wrangling -%ul#myTab.nav.nav-tabs{role: "tablist"} - %li.nav-item - %a#home-tab.nav-link{ href: admin_crops_path, role: "tab", class: 'active'} - Recently edited - %li.nav-item - %a#home-tab.nav-link{ href: wrangle_crops_path, role: "tab"} - Recently added - %li.nav-item - %a#profile-tab.nav-link{ href: wrangle_crops_path(approval_status: "pending"), role: "tab"} - Pending approval - %li.nav-item - %a#contact-tab.nav-link{ href: wrangle_crops_path(approval_status: "rejected"), role: "tab"} - Rejected +%nav.nav + = link_to "Full crop hierarchy", hierarchy_crops_path, class: 'nav-link' + = link_to "Add Crop", new_crop_path, class: 'btn' -.card - %ul.list-group.list-group-flush - - @versions.each do |version| - - crop = version.item || version.reify - - if crop - %li.list-group-item - .d-flex.w-100.justify-content-between - %h5.mb-1 - - if version.event == "destroy" - = crop.name - - else - = link_to crop.name, crop - %small.text-muted= "was #{version.event}d" - %small= time_ago_in_words(version.created_at) + " ago" - - member = @members[version.whodunnit.to_i] - - if member - %p.mb-1 - Made by - = link_to member.name, member - = render 'shared/version_changeset', version: version +%section.crop_wranglers + %h2 Crop Wranglers + - @crop_wranglers.each do |crop_wrangler| + = render 'members/tiny', member: crop_wrangler + +%hr/ + +%section + %h2 Crops + + + %ul#myTab.nav.nav-tabs{role: "tablist"} + %li.nav-item + %a#home-tab.nav-link{ href: admin_crops_path, role: "tab", class: 'active'} + Recently edited + %li.nav-item + %a#home-tab.nav-link{ href: wrangle_crops_path, role: "tab"} + Recently added + %li.nav-item + %a#profile-tab.nav-link{ href: wrangle_crops_path(approval_status: "pending"), role: "tab"} + Pending approval + %li.nav-item + %a#contact-tab.nav-link{ href: wrangle_crops_path(approval_status: "rejected"), role: "tab"} + Rejected + + .card + %ul.list-group.list-group-flush + - @versions.each do |version| + - crop = version.item || version.reify + - if crop + %li.list-group-item + .d-flex.w-100.justify-content-between + %h5.mb-1 + - if version.event == "destroy" + = crop.name + - else + = link_to crop.name, crop + %small.text-muted= "was #{version.event}d" + .d-inline-block + %small.mr-2= time_ago_in_words(version.created_at) + " ago" + - if can?(:wrangle, Crop) + = link_to "Revert", revert_admin_version_path(version), method: :post, class: "btn btn-sm btn-outline-danger" + - member = @members[version.whodunnit.to_i] + - if member + %p.mb-1 + Made by + = link_to member.name, member + = render 'shared/version_changeset', version: version diff --git a/config/routes.rb b/config/routes.rb index edac51fdb..93d4d4b35 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -94,6 +94,9 @@ Rails.application.routes.draw do namespace :admin do resources :crops, only: [:index] + resources :versions, only: [] do + post :revert, on: :member, as: :revert + end end resources :comments diff --git a/spec/features/admin/reverting_crops_spec.rb b/spec/features/admin/reverting_crops_spec.rb new file mode 100644 index 000000000..f6f436f1b --- /dev/null +++ b/spec/features/admin/reverting_crops_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.feature 'Reverting crops' do + let(:wrangler) { create(:crop_wrangling_member) } + let(:member) { create(:member) } + let!(:crop) { create(:crop, name: 'Initial Name') } + + before do + crop.update(name: 'Updated Name') + end + + context 'when logged in as an wrangler' do + before do + login_as(wrangler, scope: :member) + end + + scenario 'Admin reverts a crop' do + visit admin_crops_path + click_link 'Revert', match: :first + expect(page).to have_content('Reverted to version from') + crop.reload + expect(crop.name).to eq('Initial Name') + end + end + + context 'when logged in as a regular member' do + before do + login_as(member, scope: :member) + end + + scenario 'Member cannot revert a crop' do + visit admin_crops_path + expect(page).not_to have_link('Revert') + end + end +end