# frozen_string_literal: true class Member < ApplicationRecord include Discard::Model acts_as_messageable # messages can be sent to this model include Geocodable include MemberFlickr 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 # Where they raise seedlings before planting them out. has_one :nursery, -> { nurseries }, class_name: 'Garden', foreign_key: 'owner_id', dependent: nil, 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 :jobs, 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 destroy_api_token create_api_token( provider: 'api', uid: id, token: SecureRandom.hex(16) ) end def destroy_api_token api_token.destroy if api_token? 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 # Crops they have said they don't want suggested has_many :crop_dismissals, dependent: :destroy has_many :dismissed_crops, through: :crop_dismissals, source: :crop has_many :member_dismissals, dependent: :destroy has_many :job_idea_dismissals, dependent: :destroy # # 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 has_many :created_crop_job_templates, class_name: 'CropJobTemplate', foreign_key: 'creator_id', dependent: :nullify, 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) } scope :wants_harvest_reminders, -> { where(send_harvest_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 # When they first opened a feature, from settings['features_seen'], or nil. def feature_first_seen_at(feature) seen = settings.dig('features_seen', feature.to_s) Time.zone.parse(seen) if seen end # Records the first time only; later visits leave it alone. def feature_seen!(feature) return if feature_first_seen_at(feature) seen = settings.fetch('features_seen', {}).merge(feature.to_s => Time.current.iso8601) update_column(:settings, settings.merge('features_seen' => seen)) # rubocop:disable Rails/SkipsModelValidations end # Whether they've said they don't want a nursery, so we stop asking. Having # one trumps it. def nursery_declined? settings['nursery_declined'] == true && nursery.nil? end def decline_nursery! update_column(:settings, settings.merge('nursery_declined' => true)) # rubocop:disable Rails/SkipsModelValidations end def accept_nursery! update_column(:settings, settings.except('nursery_declined')) # rubocop:disable Rails/SkipsModelValidations 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) } # Their nursery, made the first time it's wanted, as most members never need # one. The member may already have a garden called "Nursery", and garden names # are unique per member, so ours is numbered until it's free. def nursery! return nursery if nursery base = I18n.t('gardens.nursery_name') taken = gardens.pluck(:name) name = taken.include?(base) ? "#{base} #{(2..).find { |n| taken.exclude?("#{base} #{n}") }}" : base accept_nursery! create_nursery!(name:) end # 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 @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, limit = 50) return Member.none if place.blank? latitude, longitude = Geocoder.coordinates(place, params: { limit: 1 }) return Member.none unless latitude && longitude relation = Member.located.near([latitude, longitude], 1000) limit ? relation.limit(limit) : relation 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 def has_activity? gardens.not_nurseries.many? || plantings.exists? || harvests.exists? || seeds.exists? || photos.exists? || forums.exists? || jobs.exists? || posts.exists? || comments.exists? || requested_crops.exists? || created_crops.exists? || likes.exists? || created_alternate_names.exists? || created_scientific_names.exists? || follows.exists? || inverse_follows.exists? || blocks.exists? || inverse_blocks.exists? end end