mirror of
https://github.com/Growstuff/growstuff.git
synced 2026-05-11 09:17:20 -04:00
* 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>
200 lines
7.1 KiB
Ruby
200 lines
7.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class Member < ApplicationRecord
|
|
include Discard::Model
|
|
|
|
acts_as_messageable # messages can be sent to this model
|
|
include Geocodable
|
|
include MemberFlickr
|
|
include MemberNewsletter
|
|
|
|
extend FriendlyId
|
|
|
|
friendly_id :login_name, use: %i(slugged finders)
|
|
|
|
#
|
|
# Relationships
|
|
has_many :posts, foreign_key: 'author_id', dependent: :destroy, inverse_of: :author
|
|
has_many :comments, foreign_key: 'author_id', dependent: :destroy, inverse_of: :author
|
|
has_many :forums, foreign_key: 'owner_id', dependent: :nullify, inverse_of: :owner
|
|
has_many :gardens, foreign_key: 'owner_id', dependent: :destroy, inverse_of: :owner
|
|
has_many :plantings, foreign_key: 'owner_id', dependent: :destroy, inverse_of: :owner
|
|
has_many :seeds, foreign_key: 'owner_id', dependent: :destroy, inverse_of: :owner
|
|
has_many :harvests, foreign_key: 'owner_id', dependent: :destroy, inverse_of: :owner
|
|
has_many :activities, foreign_key: 'owner_id', dependent: :destroy, inverse_of: :owner
|
|
has_and_belongs_to_many :roles
|
|
has_many :notifications, foreign_key: 'recipient_id', inverse_of: :recipient
|
|
has_many :sent_notifications, foreign_key: 'sender_id', inverse_of: :sender, class_name: "Notification"
|
|
has_many :authentications, dependent: :destroy
|
|
has_one :api_token, -> { where(provider: 'api') }, class_name: 'Authentication', dependent: :destroy
|
|
|
|
def api_token?
|
|
api_token.present?
|
|
end
|
|
|
|
def regenerate_api_token
|
|
api_token.destroy if api_token?
|
|
create_api_token(
|
|
provider: 'api',
|
|
uid: id,
|
|
token: SecureRandom.hex(16)
|
|
)
|
|
end
|
|
has_many :photos, inverse_of: :owner
|
|
has_many :likes, dependent: :destroy
|
|
|
|
#
|
|
# Following other members
|
|
has_many :follows, class_name: "Follow", foreign_key: "follower_id", dependent: :destroy,
|
|
inverse_of: :follower
|
|
has_many :inverse_follows, class_name: "Follow", foreign_key: "followed_id",
|
|
dependent: :destroy, inverse_of: :followed
|
|
has_many :followed, through: :follows
|
|
has_many :followers, through: :inverse_follows, source: :follower
|
|
|
|
#
|
|
# Blocking other members
|
|
has_many :blocks, class_name: "Block", foreign_key: "blocker_id", dependent: :destroy,
|
|
inverse_of: :blocker
|
|
has_many :inverse_blocks, class_name: "Block", foreign_key: "blocked_id",
|
|
dependent: :destroy, inverse_of: :blocked
|
|
has_many :blocked_members, through: :blocks, source: :blocked
|
|
has_many :blockers, through: :inverse_blocks, source: :blocker
|
|
|
|
#
|
|
# Global data records this member created
|
|
has_many :requested_crops, class_name: 'Crop', foreign_key: 'requester_id', dependent: :nullify,
|
|
inverse_of: :requester
|
|
has_many :created_crops, class_name: 'Crop', foreign_key: 'creator_id', dependent: :nullify,
|
|
inverse_of: :creator
|
|
has_many :created_alternate_names, class_name: 'AlternateName', foreign_key: 'creator_id', inverse_of: :creator
|
|
has_many :created_scientific_names, class_name: 'ScientificName', foreign_key: 'creator_id', inverse_of: :creator
|
|
|
|
#
|
|
# Scopes
|
|
scope :confirmed, -> { where.not(confirmed_at: nil) }
|
|
scope :located, -> { where.not(location: '').where.not(latitude: nil).where.not(longitude: nil) }
|
|
scope :recently_signed_in, -> { reorder(updated_at: :desc) }
|
|
scope :recently_joined, -> { reorder(confirmed_at: :desc) }
|
|
scope :interesting, -> { confirmed.located.recently_signed_in.has_plantings }
|
|
scope :has_plantings, -> { joins(:plantings).group("members.id") }
|
|
scope :wants_reminders, -> { where(send_planting_reminder: true) }
|
|
|
|
# Include default devise modules. Others available are:
|
|
# :token_authenticatable, :confirmable,
|
|
# :lockable, :timeoutable and :omniauthable
|
|
devise :database_authenticatable, :registerable,
|
|
:recoverable, :rememberable, :trackable, :validatable,
|
|
:confirmable, :lockable, :timeoutable, :omniauthable
|
|
|
|
# discarded (deleted) member cannot log in
|
|
def active_for_authentication?
|
|
super && !discarded?
|
|
end
|
|
|
|
# set up geocoding
|
|
geocoded_by :location
|
|
|
|
# Virtual attribute for authenticating by either username or email
|
|
# This is in addition to a real persisted field like 'username'
|
|
attr_accessor :login
|
|
|
|
#
|
|
# Validations
|
|
# Requires acceptance of the Terms of Service
|
|
validates :tos_agreement, acceptance: { allow_nil: true, accept: true }
|
|
validates :login_name,
|
|
length: {
|
|
minimum: 2, maximum: 25, message: :login_name_length
|
|
},
|
|
exclusion: {
|
|
in: %w(growstuff admin moderator staff nearby), message: :login_name_reserved
|
|
},
|
|
format: {
|
|
with: /\A\w+\z/, message: :login_name_format
|
|
},
|
|
uniqueness: {
|
|
case_sensitive: false
|
|
}
|
|
validates :website_url, format: { with: %r{\Ahttps?://}, message: :url_format }, allow_blank: true
|
|
validates :other_url, format: { with: %r{\Ahttps?://}, message: :url_format }, allow_blank: true
|
|
validates :instagram_handle, :facebook_handle, :bluesky_handle,
|
|
format: { without: %r{\Ahttps?://|/}, message: :handle_format }, allow_blank: true
|
|
|
|
#
|
|
# Triggers
|
|
after_validation :geocode
|
|
after_validation :empty_unwanted_geocodes
|
|
|
|
# Give each new member a default garden
|
|
# we use find_or_create to avoid accidentally creating a second one,
|
|
# which can happen sometimes especially with FactoryBot associations
|
|
after_create { |member| Garden.create(name: "Garden", owner_id: member.id) }
|
|
|
|
# allow login via either login_name or email address
|
|
def self.find_first_by_auth_conditions(warden_conditions)
|
|
conditions = warden_conditions.dup
|
|
login = conditions.delete(:login)
|
|
return where(conditions).login_name_or_email(login).first if login
|
|
|
|
find_by(conditions)
|
|
end
|
|
|
|
def to_s
|
|
discarded? ? 'deleted' : login_name
|
|
end
|
|
|
|
def to_param
|
|
slug
|
|
end
|
|
|
|
def mailboxer_email(_messageable)
|
|
send_notification_email ? email : false
|
|
end
|
|
|
|
def role?(role_sym)
|
|
roles.any? { |r| r.name.gsub(/\s+/, "_").underscore.to_sym == role_sym }
|
|
end
|
|
|
|
def auth(provider)
|
|
authentications.find_by(provider:)
|
|
end
|
|
|
|
def unread_count
|
|
receipts.where(is_read: false).count
|
|
end
|
|
|
|
def self.login_name_or_email(login)
|
|
where(["lower(login_name) = :value OR lower(email) = :value", { value: login.downcase }])
|
|
end
|
|
|
|
def self.case_insensitive_login_name(login)
|
|
where(["lower(login_name) = :value", { value: login.downcase }])
|
|
end
|
|
|
|
def self.nearest_to(place)
|
|
nearby_members = []
|
|
if place
|
|
latitude, longitude = Geocoder.coordinates(place, params: { limit: 1 })
|
|
nearby_members = Member.located.sort_by { |x| x.distance_from([latitude, longitude]) } if latitude && longitude
|
|
end
|
|
nearby_members
|
|
end
|
|
|
|
def already_following?(member)
|
|
follows.exists?(followed_id: member.id)
|
|
end
|
|
|
|
def get_follow(member)
|
|
follows.find_by(followed_id: member.id) if already_following?(member)
|
|
end
|
|
|
|
def already_blocking?(member)
|
|
blocks.exists?(blocked_id: member.id)
|
|
end
|
|
|
|
def get_block(member)
|
|
blocks.find_by(blocked_id: member.id) if already_blocking?(member)
|
|
end
|
|
end
|