Module: Organizations::Models::Concerns::HasOrganizations::InstanceMethods

Defined in:
lib/organizations/models/concerns/has_organizations.rb

Overview

Instance methods included in the user model

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#_current_organization_idObject

Store for current organization ID (set by controller)



201
202
203
# File 'lib/organizations/models/concerns/has_organizations.rb', line 201

def _current_organization_id
  @_current_organization_id
end

Instance Method Details

#belongs_to_any_organization?Boolean

Check if user belongs to any organization Uses efficient EXISTS query Falls back to DB query when loaded association is empty to avoid stale false negatives

Returns:

  • (Boolean)


253
254
255
256
257
# File 'lib/organizations/models/concerns/has_organizations.rb', line 253

def belongs_to_any_organization?
  return true if memberships.loaded? && memberships.any?

  memberships.exists?
end

#clear_organization_cache!Object

Clear cached organization data (called when switching orgs)



239
240
241
242
243
244
245
# File 'lib/organizations/models/concerns/has_organizations.rb', line 239

def clear_organization_cache!
  @_current_organization = nil
  @_current_organization_id_cached = nil
  @_current_membership = nil
  @_current_membership_org_id = nil
  @_current_organization_id = nil
end

#create_organization!(name_or_attributes) ⇒ Organizations::Organization

Creates a new organization with this user as owner Sets the new organization as the current organization

Examples:

With just a name

user.create_organization!("Acme Corp")

With additional attributes

user.create_organization!(name: "Acme Corp", support_email: "support@acme.com")

Parameters:

  • name_or_attributes (String, Hash)

    Organization name or attributes hash

Returns:

Raises:



432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
# File 'lib/organizations/models/concerns/has_organizations.rb', line 432

def create_organization!(name_or_attributes)
  attributes = name_or_attributes.is_a?(Hash) ? name_or_attributes : { name: name_or_attributes }

  # Check max organizations limit
  settings = self.class.organization_settings
  max = settings[:max_organizations]
  if max && owned_organizations.count >= max
    raise OrganizationLimitReached, Organizations.t(:"errors.organization_limit_reached", max: max)
  end

  org = nil
  ActiveRecord::Base.transaction do
    org = Organizations::Organization.create!(attributes)

    Organizations::Membership.create!(
      user: self,
      organization: org,
      role: "owner"
    )
  end

  # Set as current organization context
  # Order matters: set the ID first, then set the cached values
  # (don't call clear_organization_cache! which would clear the ID)
  @_current_organization = org
  @_current_organization_id_cached = org.id
  @_current_membership = nil
  @_current_membership_org_id = nil
  self._current_organization_id = org.id

  Callbacks.dispatch(:organization_created, organization: org, user: self)

  org
end

#current_membershipOrganizations::Membership?

Returns the membership in the current organization Keyed by org_id to handle org switches correctly

Returns:



220
221
222
223
224
225
226
227
228
229
230
# File 'lib/organizations/models/concerns/has_organizations.rb', line 220

def current_membership
  return nil unless current_organization

  # Key memoization by org_id to avoid staleness after org switch
  if @_current_membership_org_id != current_organization.id
    @_current_membership = nil
    @_current_membership_org_id = current_organization.id
  end

  @_current_membership ||= memberships.find_by(organization_id: current_organization.id)
end

#current_organizationOrganizations::Organization?

Returns the current organization for this session

Returns:



205
206
207
208
209
210
# File 'lib/organizations/models/concerns/has_organizations.rb', line 205

def current_organization
  return @_current_organization if defined?(@_current_organization) && @_current_organization_id_cached == _current_organization_id

  @_current_organization_id_cached = _current_organization_id
  @_current_organization = _current_organization_id ? organizations.find_by(id: _current_organization_id) : nil
end

#current_organization_roleSymbol?

Returns the role in the current organization

Returns:

  • (Symbol, nil)


234
235
236
# File 'lib/organizations/models/concerns/has_organizations.rb', line 234

def current_organization_role
  current_membership&.role&.to_sym
end

#has_organization_permission_to?(permission) ⇒ Boolean

Check if user has a specific permission in current organization Uses pre-computed permission sets for O(1) lookup

Parameters:

  • permission (Symbol, String)

    The permission to check

Returns:

  • (Boolean)


412
413
414
415
416
# File 'lib/organizations/models/concerns/has_organizations.rb', line 412

def has_organization_permission_to?(permission)
  return false unless current_organization_role

  Roles.has_permission?(current_organization_role, permission)
end

#has_organization_role?(role) ⇒ Boolean

Check if user has at least the specified role

Parameters:

  • role (Symbol, String)

    The minimum required role

Returns:

  • (Boolean)


311
312
313
314
315
316
# File 'lib/organizations/models/concerns/has_organizations.rb', line 311

def has_organization_role?(role)
  current_role = current_organization_role
  return false unless current_role

  Roles.at_least?(current_role, role.to_sym)
end

#has_pending_organization_invitations?Boolean

Check if user has pending invitations Uses efficient EXISTS query

Returns:

  • (Boolean)


262
263
264
265
266
# File 'lib/organizations/models/concerns/has_organizations.rb', line 262

def has_pending_organization_invitations?
  return false unless respond_to?(:email) && email.present?

  Organizations::Invitation.pending.for_email(email).exists?
end

#is_admin_of?(org) ⇒ Boolean

Check if user is admin (or owner) of specific organization

Parameters:

Returns:

  • (Boolean)


363
364
365
366
367
368
# File 'lib/organizations/models/concerns/has_organizations.rb', line 363

def is_admin_of?(org)
  role = role_in(org)
  return false unless role

  Roles.at_least?(role, :admin)
end

#is_at_least?(role, in: nil) ⇒ Boolean

Check if user is at least the specified role in an organization

Examples:

user.is_at_least?(:admin, in: org)

Parameters:

  • role (Symbol, String)

    The minimum required role

  • in (Organization) (defaults to: nil)

    The organization (keyword arg)

Returns:

  • (Boolean)


398
399
400
401
402
403
404
# File 'lib/organizations/models/concerns/has_organizations.rb', line 398

def is_at_least?(role, in: nil)
  org = binding.local_variable_get(:in)
  user_role = org ? role_in(org) : current_organization_role
  return false unless user_role

  Roles.at_least?(user_role, role.to_sym)
end

#is_member_of?(org) ⇒ Boolean

Check if user is a member of specific organization Uses efficient EXISTS query Falls back to DB query when loaded association misses to avoid stale false negatives (e.g., membership created via organization.memberships.create! bypasses user cache)

Parameters:

Returns:

  • (Boolean)


376
377
378
379
380
381
# File 'lib/organizations/models/concerns/has_organizations.rb', line 376

def is_member_of?(org)
  return false unless org
  return true if memberships.loaded? && memberships.any? { |m| m.organization_id == org.id }

  memberships.exists?(organization_id: org.id)
end

#is_organization_admin?Boolean

Check if user is admin (or owner) of current organization

Returns:

  • (Boolean)


286
287
288
289
290
291
# File 'lib/organizations/models/concerns/has_organizations.rb', line 286

def is_organization_admin?
  role = current_organization_role
  return false unless role

  Roles.at_least?(role, :admin)
end

#is_organization_member?Boolean

Check if user is member (or higher) of current organization

Returns:

  • (Boolean)


295
296
297
298
299
300
# File 'lib/organizations/models/concerns/has_organizations.rb', line 295

def is_organization_member?
  role = current_organization_role
  return false unless role

  Roles.at_least?(role, :member)
end

#is_organization_owner?Boolean

Check if user is owner of current organization

Returns:

  • (Boolean)


280
281
282
# File 'lib/organizations/models/concerns/has_organizations.rb', line 280

def is_organization_owner?
  current_organization_role == :owner
end

#is_organization_viewer?Boolean

Check if user is viewer (or higher) of current organization

Returns:

  • (Boolean)


304
305
306
# File 'lib/organizations/models/concerns/has_organizations.rb', line 304

def is_organization_viewer?
  current_organization_role.present?
end

#is_owner_of?(org) ⇒ Boolean

Check if user is owner of specific organization

Parameters:

Returns:

  • (Boolean)


356
357
358
# File 'lib/organizations/models/concerns/has_organizations.rb', line 356

def is_owner_of?(org)
  role_in(org) == :owner
end

#is_viewer_of?(org) ⇒ Boolean

Check if user is viewer (or higher) of specific organization

Parameters:

Returns:

  • (Boolean)


386
387
388
# File 'lib/organizations/models/concerns/has_organizations.rb', line 386

def is_viewer_of?(org)
  role_in(org).present?
end

#leave_current_organization!Object

Leave current organization

Raises:



510
511
512
513
514
515
516
# File 'lib/organizations/models/concerns/has_organizations.rb', line 510

def leave_current_organization!
  unless current_organization
    raise NoCurrentOrganization, Organizations.t(:"errors.no_current_organization_to_leave")
  end

  leave_organization!(current_organization)
end

#leave_organization!(org) ⇒ Object

Leave an organization

Parameters:

Raises:



471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
# File 'lib/organizations/models/concerns/has_organizations.rb', line 471

def leave_organization!(org)
  membership = memberships.find_by(organization_id: org.id)
  return unless membership

  ActiveRecord::Base.transaction do
    # Lock organization to prevent race condition with other leave operations
    org.lock!

    # Check if this is the only owner
    if membership.role.to_sym == :owner
      owner_count = org.memberships.where(role: "owner").count
      if owner_count == 1
        raise CannotLeaveAsLastOwner, Organizations.t(:"errors.cannot_leave_as_last_owner")
      end
    end

    # Check require_organization setting
    settings = self.class.organization_settings
    if settings[:require_organization] && organizations.count == 1
      raise CannotLeaveLastOrganization, Organizations.t(:"errors.cannot_leave_last_organization")
    end

    membership.destroy!
  end

  # Clear cache if leaving current organization
  clear_organization_cache! if _current_organization_id == org.id

  Callbacks.dispatch(
    :member_removed,
    organization: org,
    user: self,
    membership: membership,
    removed_by: self
  )
end

#organizationObject

Alias for current_organization (convenience as per README)



213
214
215
# File 'lib/organizations/models/concerns/has_organizations.rb', line 213

def organization
  current_organization
end

#pending_join_request_for(organization) ⇒ Organizations::JoinRequest?

This user's open request for an organization, if any

Parameters:

Returns:



595
596
597
# File 'lib/organizations/models/concerns/has_organizations.rb', line 595

def pending_join_request_for(organization)
  organization_join_requests.pending.find_by(organization_id: organization.id)
end

#pending_organization_invitationsActiveRecord::Relation

Get pending invitations for this user

Returns:

  • (ActiveRecord::Relation)


270
271
272
273
274
# File 'lib/organizations/models/concerns/has_organizations.rb', line 270

def pending_organization_invitations
  return Organizations::Invitation.none unless respond_to?(:email) && email.present?

  Organizations::Invitation.pending.for_email(email)
end

#request_to_join!(organization, message: nil) ⇒ Organizations::JoinRequest

Petition to join an organization (request-to-join workflow). Idempotent: returns the existing open request when one exists. Fires the :join_request_created callback for new requests.

NOTE: max_organizations deliberately does NOT apply here — that setting caps orgs a user can OWN. Hosts wanting to cap plain memberships should check before approving (see README).

Parameters:

Returns:



559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
# File 'lib/organizations/models/concerns/has_organizations.rb', line 559

def request_to_join!(organization, message: nil)
  existing = pending_join_request_for(organization)
  return existing if existing

  # A member doesn't need to request — surface their (approved)
  # state instead of creating a nonsense pending row.
  existing_membership = memberships.find_by(organization_id: organization.id)
  if existing_membership
    raise Organizations::JoinRequestAlreadyDecided,
          Organizations.t(:"errors.join_request_already_member")
  end

  # SAVEPOINT: the RecordNotUnique rescue below must stay reachable
  # when a HOST wraps this call in its own transaction (PostgreSQL
  # aborted-transaction semantics — see JoinRequest#create_membership!).
  request = ActiveRecord::Base.transaction(requires_new: true) do
    organization.join_requests.create!(user: self, message: message)
  end

  Callbacks.dispatch(
    :join_request_created,
    organization: organization,
    user: self,
    join_request: request
  )

  request
rescue ActiveRecord::RecordNotUnique
  # Race: a concurrent request slipped past the pre-check.
  pending_join_request_for(organization) ||
    raise(Organizations::JoinRequestError, Organizations.t(:"errors.join_request_create_failed"))
end

#role_in(org) ⇒ Symbol?

Get user's role in a specific organization Smart about reusing loaded associations, with DB fallback for stale caches

Parameters:

Returns:

  • (Symbol, nil)


324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/organizations/models/concerns/has_organizations.rb', line 324

def role_in(org)
  return nil unless org

  # Try org's loaded memberships first
  if org.respond_to?(:association) && org.association(:memberships).loaded?
    membership = org.memberships.find { |m| m.user_id == id }
    return membership.role.to_sym if membership
  end

  # Try user's loaded memberships
  if memberships.loaded?
    membership = memberships.find { |m| m.organization_id == org.id }
    return membership.role.to_sym if membership
  end

  # Try loaded organizations' memberships
  if organizations.loaded?
    loaded_org = organizations.find { |candidate| candidate.id == org.id }
    if loaded_org && loaded_org.respond_to?(:association) && loaded_org.association(:memberships).loaded?
      membership = loaded_org.memberships.find { |m| m.user_id == id }
      return membership.role.to_sym if membership
    end
  end

  # DB fallback - loaded associations may be stale if membership was created
  # via another association path (e.g., organization.memberships.create!)
  memberships.find_by(organization_id: org.id)&.role&.to_sym
end

#send_organization_invite_to!(email, organization: nil, role: nil) ⇒ Organizations::Invitation

Send invitation to join organization

Parameters:

  • email (String)

    Email address to invite

  • organization (Organizations::Organization) (defaults to: nil)

    (optional, defaults to current)

  • role (Symbol) (defaults to: nil)

    (optional, defaults to configured default)

Returns:

Raises:



524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
# File 'lib/organizations/models/concerns/has_organizations.rb', line 524

def send_organization_invite_to!(email, organization: nil, role: nil)
  org = organization || current_organization

  unless org
    raise NoCurrentOrganization, "No organization specified and no current organization set"
  end

  # Check permission (permission-based, not role-based)
  # This respects custom role configurations
  user_role = role_in(org)
  unless user_role && Roles.has_permission?(user_role, :invite_members)
    raise Organizations::NotAuthorized.new(
      Organizations.t(:"errors.invite_not_authorized"),
      permission: :invite_members,
      organization: org,
      user: self
    )
  end

  org.send_invite_to!(email, invited_by: self, role: role)
end

#should_create_personal_organization?Boolean

Extension seam for personal organization creation. Override this method in your User model to implement custom logic.

Examples:

Skip personal org for invited signups

class User < ApplicationRecord
  has_organizations
  attr_accessor :skip_personal_organization

  def should_create_personal_organization?
    return false if skip_personal_organization
    super
  end
end

Returns:

  • (Boolean)

    true if a personal organization should be created



615
616
617
618
619
620
621
622
623
624
# File 'lib/organizations/models/concerns/has_organizations.rb', line 615

def should_create_personal_organization?
  setting = self.class.organization_settings[:create_personal_org]

  case setting
  when Proc
    setting.call(self)
  else
    !!setting
  end
end