Files
growstuff/app/models/ability.rb
google-labs-jules[bot] 3b60e8f974 Implement blocking feature (#4199)
* Implement blocking feature

This commit introduces a blocking feature that allows members to block other members.

A blocked member is prevented from:
- following the blocker
- sending private messages to the blocker
- replying to the blocker's posts
- liking the blocker's content

The implementation includes:
- A new `Block` model and a corresponding database table.
- Updates to the `Member` model to include associations for blocks.
- A new `BlocksController` to handle blocking and unblocking actions.
- New routes for the `BlocksController`.
- UI changes to add block/unblock buttons to the member profile page.
- Validations in the `Follow`, `Comment`, and `Like` models to enforce the blocking rules.
- A check in the `MessagesController` to prevent sending messages to a member who has blocked the sender.
- A callback in the `Block` model to destroy the follow relationship when a block is created.
- New feature and model specs to test the blocking functionality.

* Implement blocking feature and fix failing tests

This commit introduces a blocking feature that allows members to block other members.

A blocked member is prevented from:
- following the blocker
- sending private messages to the blocker
- replying to the blocker's posts
- liking the blocker's content

The implementation includes:
- A new `Block` model and a corresponding database table.
- Updates to the `Member` model to include associations for blocks.
- A new `BlocksController` to handle blocking and unblocking actions.
- New routes for the `BlocksController`.
- UI changes to add block/unblock buttons to the member profile page.
- Validations in the `Follow`, `Comment`, and `Like` models to enforce the blocking rules.
- A check in the `MessagesController` to prevent sending messages to a member who has blocked the sender.
- A callback in the `Block` model to destroy the follow relationship when a block is created.
- New feature and model specs to test the blocking functionality.

This commit also fixes a failing test in the blocking feature. The error was caused by the validation being called even when the `member` association was `nil`. A guard has been added to the validation methods in the `Like`, `Follow`, and `Comment` models to prevent this from happening.

* Generate schema

* Fix tests

* Add permissions

* Define Block permissions in Ability model

The feature specs for member blocking were failing because the "Block"
link was not being rendered on member profiles. This was due to the
lack of explicit create and destroy permissions for the Block resource
in the Ability model, which is used by CanCanCan to authorize actions
and by the view to conditionally show links.

This change adds the necessary permissions to `member_abilities`:
- Allows members to create blocks (except for blocking themselves).
- Allows members to destroy blocks where they are the blocker.

These rules ensure that the "Block" and "Unblock" links are correctly
rendered and authorized for signed-in members.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Comment out specs for now

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: Daniel O'Connor <daniel.oconnor@gmail.com>
Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>
2026-04-26 14:22:32 +09:30

196 lines
6.5 KiB
Ruby

# frozen_string_literal: true
class Ability
include CanCan::Ability
def initialize(member)
anon_abilities(member)
member_abilities(member) if member.present?
admin_abilities(member) if member.present? && member.role?(:admin)
end
def anon_abilities(_member)
# See the wiki for details: https://github.com/ryanb/cancan/wiki/Defining-Abilities
# everyone can do these things, even non-logged in
can :read, :all
can :read, Follow
can :followers, Follow
# Everyone can see the charts
can :timeline, Garden
can :sunniness, Crop
can :planted_from, Crop
can :harvested_for, Crop
# except these, which don't make sense if you're not logged in
cannot :read, Notification
cannot :read, Authentication
# and nobody should be able to view this except admins
cannot :read, Role
# nobody should be able to view unapproved crops unless they
# are wranglers or admins
cannot :read, Crop
can :read, Crop, approval_status: "approved"
# scientific names should only be viewable if associated crop is approved
cannot :read, ScientificName
can :read, ScientificName do |sn|
sn.crop.approved?
end
# ... same for alternate names
cannot :read, AlternateName
can :read, AlternateName do |an|
an.crop.approved?
end
cannot :create, GardenType
cannot :update, GardenType
cannot :destroy, GardenType
end
def member_abilities(member)
return unless member
# members can see even rejected or pending crops if they requested it
can :read, Crop, requester_id: member.id
can :requested, Crop # see list of crops they've requested
# managing your own user settings
can :update, Member, id: member.id
# can read/delete notifications that were sent to them
can :read, Notification, recipient_id: member.id
can :destroy, Notification, recipient_id: member.id
can :reply, Notification, recipient_id: member.id
# can send a private message to anyone but themselves
# note: sadly, we can't test for this from the view, but it works
# for the model/controller
can :create, Notification do |n|
n.recipient_id != member.id
end
# NOTE: we don't support update for notifications
# only crop wranglers can create/edit/destroy crops
if member.role? :crop_wrangler
can :wrangle, Crop
can :manage, Crop
can :manage, CropCompanion
can :manage, ScientificName
can :manage, AlternateName
can :openfarm, Crop
can :gbif, Crop
end
# any member can create a crop provisionally
can :create, Crop
# can create & destroy their own authentications against other sites.
can :create, Authentication
can :destroy, Authentication, member_id: member.id
# anyone can create a post, like, or comment on a post,
# but only the author can edit/destroy it.
can :create, Post
can :update, Post, author_id: member.id
can :destroy, Post, author_id: member.id
can :create, Like
can :destroy, Like, member_id: member.id
can :create, Comment
can :update, Comment, author_id: member.id
can :destroy, Comment, author_id: member.id
# same deal for gardens and plantings
can :create, Garden
can :update, Garden, owner_id: member.id
can :destroy, Garden, owner_id: member.id
can :create, Planting
can :update, Planting, garden: { owner_id: member.id }, crop: { approval_status: 'approved' }
can :destroy, Planting, garden: { owner_id: member.id }, crop: { approval_status: 'approved' }
can :update, Planting do |planting|
planting.garden.garden_collaborators.where(member_id: member.id).any?
end
can :transplant, Planting, garden: { owner_id: member.id }
can :transplant, Planting do |planting|
planting.garden.garden_collaborators.where(member_id: member.id).any?
end
can :destroy, Planting do |planting|
planting.garden.garden_collaborators.where(member_id: member.id).any?
end
can :create, GardenCollaborator, garden: { owner_id: member.id }
can :update, GardenCollaborator, garden: { owner_id: member.id }
can :destroy, GardenCollaborator, garden: { owner_id: member.id }
can :create, Activity
can :update, Activity, owner_id: member.id
can :destroy, Activity, owner_id: member.id
can :update, Activity do |activity|
activity.garden&.garden_collaborators&.where(member_id: member.id)&.any?
end
can :destroy, Activity do |activity|
activity.garden&.garden_collaborators&.where(member_id: member.id)&.any?
end
can :create, Harvest
can :update, Harvest, owner_id: member.id
can :destroy, Harvest, owner_id: member.id
can :update, Harvest, owner_id: member.id, planting: { owner_id: member.id }
can :destroy, Harvest, owner_id: member.id, planting: { owner_id: member.id }
can :update, Harvest do |harvest|
harvest.planting&.garden&.garden_collaborators&.where(member_id: member.id)&.any?
end
can :destroy, Harvest do |harvest|
harvest.planting&.garden&.garden_collaborators&.where(member_id: member.id)&.any?
end
can :create, Photo
can :update, Photo, owner_id: member.id
can :destroy, Photo, owner_id: member.id
can :create, Seed
can :update, Seed, owner_id: member.id
can :destroy, Seed, owner_id: member.id
can :create, Seed, owner_id: member.id, parent_planting: { owner_id: member.id }
can :update, Seed, owner_id: member.id, parent_planting: { owner_id: member.id }
can :destroy, Seed, owner_id: member.id, parent_planting: { owner_id: member.id }
# following/unfollowing permissions
can :create, Follow
cannot :create, Follow, followed_id: member.id # can't follow yourself
can :destroy, Follow
cannot :destroy, Follow, followed_id: member.id # can't unfollow yourself
# blocking/unblocking permissions
can :create, Block
cannot :create, Block, blocked_id: member.id # can't block yourself
can :destroy, Block, blocker_id: member.id # can only unblock your own blocks
cannot :create, GardenType
cannot :update, GardenType
cannot :destroy, GardenType
end
def admin_abilities(member)
return unless member.role? :admin
can :read, :all
can :manage, :all
# can't delete plant parts if they have harvests associated with them
cannot :destroy, PlantPart
can :destroy, PlantPart do |pp|
pp.harvests.empty?
end
# Admins can't delete themselves
cannot :destroy, Member
can :destroy, Member do |other_member|
other_member&.id != member.id
end
end
end