Class: Member

Inherits:
ApplicationRecord show all
Extended by:
Gitlab::Utils::Override
Includes:
AfterCommitQueue, CreatedAtFilterable, EachBatch, Expirable, FromUnion, Gitlab::Access, Gitlab::Experiment::Dsl, Gitlab::Utils::StrongMemoize, Importable, Presentable, RestrictedSignup, Sortable, UpdateHighestRole
Defined in:
app/models/member.rb

Direct Known Subclasses

GroupMember, ProjectMember

Constant Summary collapse

AVATAR_SIZE =
40
ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT =
10
STATE_ACTIVE =
0
STATE_AWAITING =
1

Constants included from UpdateHighestRole

UpdateHighestRole::HIGHEST_ROLE_JOB_DELAY, UpdateHighestRole::HIGHEST_ROLE_LEASE_TIMEOUT

Constants included from Gitlab::Access

Gitlab::Access::ADMIN, Gitlab::Access::AccessDeniedError, Gitlab::Access::DEVELOPER, Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS, Gitlab::Access::GUEST, Gitlab::Access::MAINTAINER, Gitlab::Access::MAINTAINER_PROJECT_ACCESS, Gitlab::Access::MAINTAINER_SUBGROUP_ACCESS, Gitlab::Access::MINIMAL_ACCESS, Gitlab::Access::NO_ACCESS, Gitlab::Access::NO_ONE_PROJECT_ACCESS, Gitlab::Access::OWNER, Gitlab::Access::OWNER_SUBGROUP_ACCESS, Gitlab::Access::PROTECTION_DEV_CAN_INITIAL_PUSH, Gitlab::Access::PROTECTION_DEV_CAN_MERGE, Gitlab::Access::PROTECTION_DEV_CAN_PUSH, Gitlab::Access::PROTECTION_FULL, Gitlab::Access::PROTECTION_NONE, Gitlab::Access::REPORTER

Constants included from Expirable

Expirable::DAYS_TO_EXPIRE

Constants inherited from ApplicationRecord

ApplicationRecord::MAX_PLUCK

Constants included from ResetOnUnionError

ResetOnUnionError::MAX_RESET_PERIOD

Instance Attribute Summary collapse

Attributes included from Importable

#imported, #importing

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Gitlab::Utils::Override

extended, extensions, included, method_added, override, prepended, queue_verification, verify!

Methods included from Presentable

#present

Methods included from Gitlab::Access

all_values, human_access, #human_access, #human_access_with_none, human_access_with_none, options, options_with_none, options_with_owner, #owner?, project_creation_level_name, project_creation_options, project_creation_string_options, project_creation_string_values, project_creation_values, protection_options, protection_values, subgroup_creation_options, subgroup_creation_string_options, subgroup_creation_string_values, subgroup_creation_values, sym_options, sym_options_with_owner

Methods included from Expirable

#expired?, #expires?, #expires_soon?

Methods included from AfterCommitQueue

#run_after_commit, #run_after_commit_or_now

Methods inherited from ApplicationRecord

cached_column_list, #create_or_load_association, declarative_enum, default_select_columns, id_in, id_not_in, iid_in, pluck_primary_key, primary_key_in, #readable_by?, safe_ensure_unique, safe_find_or_create_by, safe_find_or_create_by!, #to_ability_name, underscore, where_exists, where_not_exists, with_fast_read_statement_timeout, without_order

Methods included from SensitiveSerializableHash

#serializable_hash

Instance Attribute Details

#raw_invite_tokenObject

Returns the value of attribute raw_invite_token.



25
26
27
# File 'app/models/member.rb', line 25

def raw_invite_token
  @raw_invite_token
end

Class Method Details

.access_for_user_ids(user_ids) ⇒ Object



360
361
362
# File 'app/models/member.rb', line 360

def access_for_user_ids(user_ids)
  where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h
end

.filter_by_2fa(value) ⇒ Object



321
322
323
324
325
326
327
328
329
330
# File 'app/models/member.rb', line 321

def filter_by_2fa(value)
  case value
  when 'enabled'
    left_join_users.merge(User.with_two_factor)
  when 'disabled'
    left_join_users.merge(User.without_two_factor)
  else
    all
  end
end

.filter_by_user_type(value) ⇒ Object



332
333
334
335
336
# File 'app/models/member.rb', line 332

def filter_by_user_type(value)
  return unless ::User.user_types.key?(value)

  left_join_users.merge(::User.where(user_type: value))
end

.find_by_invite_token(raw_invite_token) ⇒ Object



364
365
366
367
# File 'app/models/member.rb', line 364

def find_by_invite_token(raw_invite_token)
  invite_token = Devise.token_generator.digest(self, :invite_token, raw_invite_token)
  find_by(invite_token: invite_token)
end

.left_join_usersObject



355
356
357
358
# File 'app/models/member.rb', line 355

def left_join_users
  left_outer_joins(:user)
    .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417456")
end

.pluck_user_idsObject



373
374
375
# File 'app/models/member.rb', line 373

def pluck_user_ids
  pluck(:user_id)
end

.search(query) ⇒ Object



295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'app/models/member.rb', line 295

def search(query)
  scope = joins(:user)
            .merge(User.search(query, use_minimum_char_limit: false))
            .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417456")

  return scope unless Gitlab::Pagination::Keyset::Order.keyset_aware?(scope)

  # If the User.search method returns keyset pagination aware AR scope then we
  # need call apply_cursor_conditions which adds the ORDER BY columns from the scope
  # to the SELECT clause.
  #
  # Why is this needed:
  # When using keyset pagination, the next page is loaded using the ORDER BY
  # values of the last record (cursor). This query selects `members.*` and
  # orders by a custom SQL expression on `users` and `users.name`. The values
  # will not be part of `members.*`.
  #
  # Result: `SELECT members.*, users.column1, users.column2 FROM members ...`
  order = Gitlab::Pagination::Keyset::Order.extract_keyset_order_object(scope)
  order.apply_cursor_conditions(scope).reorder(order)
end

.search_invite_email(query) ⇒ Object



317
318
319
# File 'app/models/member.rb', line 317

def search_invite_email(query)
  invite.where(['invite_email ILIKE ?', "%#{query}%"])
end

.sort_by_attribute(method) ⇒ Object



338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'app/models/member.rb', line 338

def sort_by_attribute(method)
  case method.to_s
  when 'access_level_asc' then reorder(access_level: :asc)
  when 'access_level_desc' then reorder(access_level: :desc)
  when 'recent_sign_in' then 
  when 'oldest_sign_in' then 
  when 'recent_created_user' then order_recent_created_user
  when 'oldest_created_user' then order_oldest_created_user
  when 'recent_last_activity' then order_recent_last_activity
  when 'oldest_last_activity' then order_oldest_last_activity
  when 'last_joined' then order_created_desc
  when 'oldest_joined' then order_created_asc
  else
    order_by(method)
  end
end

.valid_email?(email) ⇒ Boolean

Returns:

  • (Boolean)


369
370
371
# File 'app/models/member.rb', line 369

def valid_email?(email)
  Devise.email_regexp.match?(email)
end

Instance Method Details

#accept_invite!(new_user) ⇒ Object



413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# File 'app/models/member.rb', line 413

def accept_invite!(new_user)
  return false unless invite?
  return false unless new_user

  self.user = new_user
  return false unless self.user.save

  self.invite_token = nil
  self.invite_accepted_at = Time.current.utc

  saved = self.save

  after_accept_invite if saved

  saved
end

#accept_request(current_user) ⇒ Object



404
405
406
407
408
409
410
411
# File 'app/models/member.rb', line 404

def accept_request(current_user)
  return false unless request?

  updated = self.update(requested_at: nil, created_by: current_user)
  after_accept_request if updated

  updated
end

#access_fieldObject



382
383
384
# File 'app/models/member.rb', line 382

def access_field
  access_level
end

#create_notification_settingObject



466
467
468
# File 'app/models/member.rb', line 466

def create_notification_setting
  user.notification_settings.find_or_create_for(source)
end

#created_by_nameObject



500
501
502
# File 'app/models/member.rb', line 500

def created_by_name
  created_by&.name
end

#decline_invite!Object



430
431
432
433
434
435
436
437
438
# File 'app/models/member.rb', line 430

def decline_invite!
  return false unless invite?

  destroyed = self.destroy

  after_decline_invite if destroyed

  destroyed
end

#destroy_notification_settingObject



470
471
472
# File 'app/models/member.rb', line 470

def destroy_notification_setting
  notification_setting&.destroy
end

#generate_invite_tokenObject



440
441
442
443
444
# File 'app/models/member.rb', line 440

def generate_invite_token
  raw, enc = Devise.token_generator.generate(self.class, :invite_token)
  @raw_invite_token = raw
  self.invite_token = enc
end

#generate_invite_token!Object



446
447
448
# File 'app/models/member.rb', line 446

def generate_invite_token!
  generate_invite_token && save(validate: false)
end

#highest_group_memberObject

Find the user’s group member with a highest access level



488
489
490
491
492
493
494
# File 'app/models/member.rb', line 488

def highest_group_member
  strong_memoize(:highest_group_member) do
    next unless user_id && source&.ancestors&.any?

    GroupMember.where(source: source.ancestors, user_id: user_id).order(:access_level).last
  end
end

#hook_prerequisites_met?Boolean

Returns:

  • (Boolean)


398
399
400
401
402
# File 'app/models/member.rb', line 398

def hook_prerequisites_met?
  # It is essential that an associated user record exists
  # so that we can successfully fire any member related hooks/notifications.
  user.present?
end

#invite?Boolean

Returns:

  • (Boolean)


386
387
388
# File 'app/models/member.rb', line 386

def invite?
  self.invite_token.present?
end

#invite_to_unknown_user?Boolean

Returns:

  • (Boolean)


496
497
498
# File 'app/models/member.rb', line 496

def invite_to_unknown_user?
  invite? && user_id.nil?
end

#notifiable?(type, opts = {}) ⇒ Boolean

rubocop: disable CodeReuse/ServiceClass

Returns:

  • (Boolean)


479
480
481
482
483
484
# File 'app/models/member.rb', line 479

def notifiable?(type, opts = {})
  # always notify when there isn't a user yet
  return true if user.blank?

  NotificationRecipients::BuildService.notifiable?(user, type, notifiable_options.merge(opts))
end

#notification_settingObject



474
475
476
# File 'app/models/member.rb', line 474

def notification_setting
  @notification_setting ||= user&.notification_settings_for(source)
end

#pending?Boolean

Returns:

  • (Boolean)


394
395
396
# File 'app/models/member.rb', line 394

def pending?
  invite? || request?
end

#real_source_typeObject



378
379
380
# File 'app/models/member.rb', line 378

def real_source_type
  source_type
end

#request?Boolean

Returns:

  • (Boolean)


390
391
392
# File 'app/models/member.rb', line 390

def request?
  requested_at.present?
end

#resend_inviteObject



450
451
452
453
454
455
456
# File 'app/models/member.rb', line 450

def resend_invite
  return unless invite?

  generate_invite_token! unless @raw_invite_token

  send_invite
end

#send_invitation_reminder(reminder_index) ⇒ Object



458
459
460
461
462
463
464
# File 'app/models/member.rb', line 458

def send_invitation_reminder(reminder_index)
  return unless invite?

  generate_invite_token! unless @raw_invite_token

  run_after_commit_or_now { notification_service.invite_member_reminder(self, @raw_invite_token, reminder_index) }
end