bullet_train 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/config/bullet_train_manifest.js +0 -0
  6. data/app/controllers/account/invitations_controller.rb +146 -0
  7. data/app/controllers/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments_controller.rb +83 -0
  8. data/app/controllers/account/memberships_controller.rb +132 -0
  9. data/app/controllers/account/onboarding/user_details_controller.rb +63 -0
  10. data/app/controllers/account/onboarding/user_email_controller.rb +65 -0
  11. data/app/controllers/account/teams_controller.rb +122 -0
  12. data/app/controllers/account/users_controller.rb +59 -0
  13. data/app/helpers/account/invitations_helper.rb +2 -0
  14. data/app/helpers/account/memberships_helper.rb +9 -0
  15. data/app/helpers/account/teams_helper.rb +80 -0
  16. data/app/helpers/account/users_helper.rb +81 -0
  17. data/app/helpers/invitation_only_helper.rb +10 -0
  18. data/app/helpers/invitations_helper.rb +17 -0
  19. data/app/models/invitation.rb +73 -0
  20. data/app/models/membership.rb +164 -0
  21. data/app/models/memberships/reassignments/assignment.rb +12 -0
  22. data/app/models/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignment.rb +38 -0
  23. data/app/models/memberships/reassignments.rb +5 -0
  24. data/app/models/team.rb +81 -0
  25. data/app/models/user.rb +191 -0
  26. data/app/views/account/invitations/_breadcrumbs.html.erb +9 -0
  27. data/app/views/account/invitations/_form.html.erb +49 -0
  28. data/app/views/account/invitations/_invitation.json.jbuilder +7 -0
  29. data/app/views/account/invitations/index.json.jbuilder +1 -0
  30. data/app/views/account/invitations/new.html.erb +12 -0
  31. data/app/views/account/invitations/show.html.erb +30 -0
  32. data/app/views/account/invitations/show.json.jbuilder +1 -0
  33. data/app/views/account/memberships/_breadcrumbs.html.erb +8 -0
  34. data/app/views/account/memberships/_form.html.erb +45 -0
  35. data/app/views/account/memberships/_index.html.erb +66 -0
  36. data/app/views/account/memberships/_menu_item.html.erb +8 -0
  37. data/app/views/account/memberships/_tombstones.html.erb +59 -0
  38. data/app/views/account/memberships/edit.html.erb +22 -0
  39. data/app/views/account/memberships/index.html.erb +7 -0
  40. data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/_breadcrumbs.html.erb +10 -0
  41. data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/_form.html.erb +24 -0
  42. data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/_scaffolding_completely_concrete_tangible_things_reassignment.json.jbuilder +8 -0
  43. data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/index.json.jbuilder +1 -0
  44. data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/new.html.erb +11 -0
  45. data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/show.json.jbuilder +1 -0
  46. data/app/views/account/memberships/show.html.erb +60 -0
  47. data/app/views/account/onboarding/user_details/edit.html.erb +72 -0
  48. data/app/views/account/onboarding/user_email/edit.html.erb +33 -0
  49. data/app/views/account/teams/_breadcrumbs.html.erb +11 -0
  50. data/app/views/account/teams/_form.html.erb +22 -0
  51. data/app/views/account/teams/_index.html.erb +33 -0
  52. data/app/views/account/teams/_menu_item.html.erb +8 -0
  53. data/app/views/account/teams/_team.json.jbuilder +9 -0
  54. data/app/views/account/teams/edit.html.erb +12 -0
  55. data/app/views/account/teams/index.html.erb +6 -0
  56. data/app/views/account/teams/index.json.jbuilder +1 -0
  57. data/app/views/account/teams/new.html.erb +88 -0
  58. data/app/views/account/teams/show.html.erb +25 -0
  59. data/app/views/account/teams/show.json.jbuilder +1 -0
  60. data/app/views/account/users/_breadcrumbs.html.erb +4 -0
  61. data/app/views/account/users/_form.html.erb +39 -0
  62. data/app/views/account/users/edit.html.erb +50 -0
  63. data/app/views/account/users/show.html.erb +17 -0
  64. data/config/locales/en/invitations.en.yml +69 -0
  65. data/config/locales/en/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments.en.yml +42 -0
  66. data/config/locales/en/memberships.en.yml +99 -0
  67. data/config/locales/en/onboarding/user_details.en.yml +11 -0
  68. data/config/locales/en/onboarding/user_email.en.yml +13 -0
  69. data/config/locales/en/teams.en.yml +85 -0
  70. data/config/locales/en/users.en.yml +110 -0
  71. data/config/routes.rb +2 -0
  72. data/db/migrate/20161115160419_devise_create_users.rb +41 -0
  73. data/db/migrate/20161116003852_add_api_key_to_user.rb +6 -0
  74. data/db/migrate/20161117154605_create_teams.rb +10 -0
  75. data/db/migrate/20161117154709_create_memberships.rb +11 -0
  76. data/db/migrate/20161203193930_add_current_team_to_user.rb +5 -0
  77. data/db/migrate/20161204234150_create_invitations.rb +11 -0
  78. data/db/migrate/20161205154821_add_team_to_invitation.rb +5 -0
  79. data/db/migrate/20161205164613_add_admin_to_invitation.rb +5 -0
  80. data/db/migrate/20170908205756_add_names_to_user.rb +6 -0
  81. data/db/migrate/20170915215309_add_team_to_thing.rb +5 -0
  82. data/db/migrate/20171105001408_remove_api_key_from_user.rb +6 -0
  83. data/db/migrate/20180326124105_add_timezone_to_user.rb +5 -0
  84. data/db/migrate/20180902142350_create_membership_roles.rb +10 -0
  85. data/db/migrate/20180902143758_remove_admin_from_membership.rb +5 -0
  86. data/db/migrate/20180902154611_create_invitation_roles.rb +10 -0
  87. data/db/migrate/20180902154652_migrate_admin_flag_on_invitations.rb +14 -0
  88. data/db/migrate/20180902195848_remove_admin_from_invitation.rb +5 -0
  89. data/db/migrate/20180903101707_add_last_seen_at_to_users.rb +5 -0
  90. data/db/migrate/20190321203224_add_profile_photo_id_to_user.rb +5 -0
  91. data/db/migrate/20190519230202_add_ability_cache_to_user.rb +5 -0
  92. data/db/migrate/20190628194704_add_last_notification_email_sent_at_to_user.rb +5 -0
  93. data/db/migrate/20200211034208_add_invitation_to_membership.rb +5 -0
  94. data/db/migrate/20200211044616_drop_invitation_roles_table.rb +10 -0
  95. data/db/migrate/20200213052748_add_former_user_fields_to_membership.rb +8 -0
  96. data/db/migrate/20200213235037_add_former_user_to_user.rb +7 -0
  97. data/db/migrate/20200219013834_add_added_by_to_membership.rb +5 -0
  98. data/db/migrate/20200219015116_rename_from_user_to_from_membership.rb +5 -0
  99. data/db/migrate/20200726222314_add_being_destroyed_to_team.rb +5 -0
  100. data/db/migrate/20200727171308_add_devise_two_factor_to_users.rb +9 -0
  101. data/db/migrate/20200727175949_add_devise_two_factor_backupable_to_users.rb +5 -0
  102. data/db/migrate/20210304133200_add_time_zone_to_team.rb +5 -0
  103. data/db/migrate/20210816072419_add_locale_to_users.rb +5 -0
  104. data/db/migrate/20210816072508_add_locale_to_teams.rb +5 -0
  105. data/db/migrate/20211020200855_add_doorkeeper_application_to_memberships.rb +5 -0
  106. data/db/migrate/20211027002944_add_doorkeeper_application_to_users.rb +5 -0
  107. data/lib/bullet_train/engine.rb +4 -0
  108. data/lib/bullet_train/version.rb +3 -0
  109. data/lib/bullet_train.rb +6 -0
  110. data/lib/tasks/bullet_train_tasks.rake +4 -0
  111. metadata +168 -0
@@ -0,0 +1,59 @@
1
+ class Account::UsersController < Account::ApplicationController
2
+ load_and_authorize_resource
3
+
4
+ before_action do
5
+ # for magic locales.
6
+ @child_object = @user
7
+ end
8
+
9
+ # GET /account/users/1/edit
10
+ def edit
11
+ end
12
+
13
+ # GET /account/users/1
14
+ def show
15
+ end
16
+
17
+ def updating_password?
18
+ params[:user].key?(:password)
19
+ end
20
+
21
+ # PATCH/PUT /account/users/1
22
+ # PATCH/PUT /account/users/1.json
23
+ def update
24
+ respond_to do |format|
25
+ if updating_password? ? @user.update_with_password(user_params) : @user.update_without_password(user_params)
26
+ # if you update your own user account, devise will normally kick you out, so we do this instead.
27
+ bypass_sign_in current_user.reload
28
+ format.html { redirect_to [:edit, :account, @user], notice: t("users.notifications.updated") }
29
+ format.json { render :show, status: :ok, location: [:account, @user] }
30
+ else
31
+ format.html { render :edit, status: :unprocessable_entity }
32
+ format.json { render json: @user.errors, status: :unprocessable_entity }
33
+ end
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ # Never trust parameters from the scary internet, only allow the white list through.
40
+ def user_params
41
+ # TODO enforce permissions on updating the user's team name.
42
+ params.require(:user).permit(
43
+ :email,
44
+ :first_name,
45
+ :last_name,
46
+ :time_zone,
47
+ :current_password,
48
+ :password,
49
+ :password_confirmation,
50
+ :profile_photo_id,
51
+ :locale,
52
+ # 🚅 super scaffolding will insert new fields above this line.
53
+ current_team_attributes: [:name],
54
+ # 🚅 super scaffolding will insert new arrays above this line.
55
+ )
56
+
57
+ # 🚅 super scaffolding will insert processing for new fields above this line.
58
+ end
59
+ end
@@ -0,0 +1,2 @@
1
+ module Account::InvitationsHelper
2
+ end
@@ -0,0 +1,9 @@
1
+ module Account::MembershipsHelper
2
+ def membership_destroy_locale_key(membership)
3
+ if membership.user == current_user
4
+ ".destroy_own"
5
+ else
6
+ ".destroy"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,80 @@
1
+ module Account::TeamsHelper
2
+ def current_team
3
+ current_user&.current_team
4
+ end
5
+
6
+ def other_teams
7
+ return [] unless current_user
8
+ current_user.teams.reject { |team| team == current_user.current_team }
9
+ end
10
+
11
+ def users_as_select_options(users, values = [])
12
+ values = Array(values)
13
+ users.map { |user|
14
+ "<option value=\"#{user.id}\" data-image=\"#{user_profile_photo_url(user)}\" #{"selected=\"selected\"" if values.include?(user.id)}>#{user.name}</option>"
15
+ }.join.html_safe
16
+ end
17
+
18
+ def memberships_as_select_options(memberships, values = [])
19
+ values = Array(values)
20
+ memberships.map { |membership|
21
+ "<option value=\"#{membership.id}\" data-image=\"#{membership_profile_photo_url(membership)}\" #{"selected=\"selected\"" if values.include?(membership.id)}>#{membership.name}</option>"
22
+ }.join.html_safe
23
+ end
24
+
25
+ def photo_for(object)
26
+ background_color = Colorizer.colorize_similarly((object.name.to_s + object.created_at.to_s).to_s, 0.5, 0.6).delete("#")
27
+ "https://ui-avatars.com/api/?" + {
28
+ color: "ffffff",
29
+ background: background_color,
30
+ bold: true,
31
+ name: "#{object.name.first}#{object.name.split.one? ? "" : object.name.split.first(2).last.first}",
32
+ size: 200,
33
+ }.to_param
34
+ end
35
+
36
+ # TODO this should only be used for certain locales/languages.
37
+ def describe_users_for_user_on_team(users, for_user, team)
38
+ # if this list of users represents everyone on the team, return the team name.
39
+ if (team.users & users) == team.users
40
+ team.name.strip
41
+ else
42
+ ((users - [for_user]) + [for_user]).map do |user|
43
+ if user == for_user
44
+ "You"
45
+ elsif team.users.where("users.first_name ILIKE ?", user.first_name).one?
46
+ user.first_name
47
+ elsif team.users.where("users.first_name ILIKE ? AND LEFT(users.last_name, 1) ILIKE ?", user.first_name, user.last_name.first).one?
48
+ "#{user.first_name} #{user.last_name.first}."
49
+ else
50
+ user.name
51
+ end
52
+ end.to_sentence.strip
53
+ end
54
+ end
55
+
56
+ # TODO this should only be used for certain locales/languages.
57
+ def describe_memberships_for_membership_on_team(memberships, for_membership, team)
58
+ # if this list of users represents everyone on the team, return the team name.
59
+ if (team.memberships & memberships) == team.memberships
60
+ team.name.strip
61
+ else
62
+ # place the membership that would be "you" at the end of the array.
63
+ ((memberships - [for_membership]) + [for_membership]).map do |membership|
64
+ if membership == for_membership
65
+ "You"
66
+ elsif membership.first_name.present? && team.memberships.map(&:first_name).select(&:present?).select { |first_name| first_name.downcase == membership.first_name.downcase }.one?
67
+ membership.first_name
68
+ elsif membership.first_name_last_initial.present? && team.memberships.map(&:first_name_last_initial).select { |first_name_last_initial| first_name_last_initial.downcase == membership.first_name_last_initial.downcase }.one?
69
+ membership.user.first_name_last_initial
70
+ else
71
+ membership.full_name
72
+ end
73
+ end.to_sentence.strip
74
+ end
75
+ end
76
+
77
+ def can_invite?
78
+ can?(:create, Invitation.new(team: current_team))
79
+ end
80
+ end
@@ -0,0 +1,81 @@
1
+ module Account::UsersHelper
2
+ def profile_photo_for(url: nil, email: nil, first_name: nil, last_name: nil)
3
+ if cloudinary_enabled? && !url.blank?
4
+ cl_image_path(url, width: 100, height: 100, crop: :fill)
5
+ else
6
+ background_color = Colorizer.colorize_similarly(email.to_s, 0.5, 0.6).delete("#")
7
+ "https://ui-avatars.com/api/?" + {
8
+ color: "ffffff",
9
+ background: background_color,
10
+ bold: true,
11
+ # email.to_s should not be necessary once we fix the edge case of cancelling an unclaimed membership
12
+ name: [first_name, last_name].join(" ").strip.presence || email,
13
+ size: 200,
14
+ }.to_param
15
+ end
16
+ end
17
+
18
+ def user_profile_photo_url(user)
19
+ profile_photo_for(
20
+ url: user.profile_photo_id,
21
+ email: user.email,
22
+ first_name: user.first_name,
23
+ last_name: user.last_name
24
+ )
25
+ end
26
+
27
+ def membership_profile_photo_url(membership)
28
+ if membership.user
29
+ user_profile_photo_url(membership.user)
30
+ else
31
+ profile_photo_for(
32
+ url: membership.user_profile_photo_id,
33
+ email: membership.invitation&.email || membership.user_email,
34
+ first_name: membership.user_first_name,
35
+ last_name: membership.user_last_name
36
+ )
37
+ end
38
+ end
39
+
40
+ def profile_header_photo_for(url: nil, email: nil, first_name: nil, last_name: nil)
41
+ if cloudinary_enabled? && !url.blank?
42
+ cl_image_path(url, width: 700, height: 200, crop: :fill)
43
+ else
44
+ background_color = Colorizer.colorize_similarly(email.to_s, 0.5, 0.6).delete("#")
45
+ "https://ui-avatars.com/api/?" + {
46
+ color: "ffffff",
47
+ background: background_color,
48
+ bold: true,
49
+ # email.to_s should not be necessary once we fix the edge case of cancelling an unclaimed membership
50
+ name: "#{first_name&.first || email.to_s[0]} #{last_name&.first || email.to_s[1]}",
51
+ size: 200,
52
+ }.to_param
53
+ end
54
+ end
55
+
56
+ def user_profile_header_photo_url(user)
57
+ profile_header_photo_for(
58
+ url: user.profile_photo_id,
59
+ email: user.email,
60
+ first_name: user.first_name,
61
+ last_name: user.last_name
62
+ )
63
+ end
64
+
65
+ def membership_profile_header_photo_url(membership)
66
+ if membership.user
67
+ user_profile_header_photo_url(membership.user)
68
+ else
69
+ profile_header_photo_for(
70
+ url: membership.user_profile_photo_id,
71
+ email: membership.invitation&.email || membership.user_email,
72
+ first_name: membership.user_first_name,
73
+ last_name: membership.user&.last_name || membership.user_last_name
74
+ )
75
+ end
76
+ end
77
+
78
+ def current_membership
79
+ current_user.memberships.where(team: current_team).first
80
+ end
81
+ end
@@ -0,0 +1,10 @@
1
+ module InvitationOnlyHelper
2
+ def invited?
3
+ session[:invitation_key].present? && invitation_keys.include?(session[:invitation_key])
4
+ end
5
+
6
+ def show_sign_up_options?
7
+ return true unless invitation_only?
8
+ invited?
9
+ end
10
+ end
@@ -0,0 +1,17 @@
1
+ module InvitationsHelper
2
+ def handle_outstanding_invitation
3
+ # was this user registering to claim an invitation?
4
+ if session[:invitation_uuid].present?
5
+
6
+ # try to find the invitation, if it still exists.
7
+ invitation = Invitation.find_by_uuid(session[:invitation_uuid])
8
+
9
+ # if the invitation was found, claim it for this user.
10
+ invitation&.accept_for(current_user)
11
+
12
+ # remove the uuid from the session.
13
+ session.delete(:invitation_uuid)
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,73 @@
1
+ class Invitation < ApplicationRecord
2
+ # 🚫 DEFAULT BULLET TRAIN INVITATION FUNCTIONALITY
3
+ # Typically you should avoid adding your own functionality in this section to avoid merge conflicts in the future.
4
+ # (If you specifically want to change Bullet Train's default behavior, that's OK and you can do that here.)
5
+
6
+ belongs_to :team
7
+ belongs_to :from_membership, class_name: "Membership"
8
+ has_one :membership, dependent: :nullify
9
+ has_many :roles, through: :membership
10
+
11
+ accepts_nested_attributes_for :membership
12
+
13
+ validates :email, presence: true
14
+
15
+ before_create :generate_uuid
16
+ after_create :set_added_by_membership
17
+ after_create :send_invitation_email
18
+
19
+ # ✅ YOUR APPLICATION'S INVITATION FUNCTIONALITY
20
+ # This is the place where you should implement your own features on top of Bullet Train's functionality. There
21
+ # are a bunch of Super Scaffolding hooks here by default to try and help keep generated code logically organized.
22
+
23
+ # 🚅 add concerns above.
24
+
25
+ # 🚅 add belongs_to associations above.
26
+
27
+ # 🚅 add has_many associations above.
28
+
29
+ # 🚅 add oauth providers above.
30
+
31
+ # 🚅 add has_one associations above.
32
+
33
+ # 🚅 add scopes above.
34
+
35
+ # 🚅 add validations above.
36
+
37
+ # 🚅 add callbacks above.
38
+
39
+ # 🚅 add delegations above.
40
+
41
+ # 🚅 add methods above.
42
+
43
+ # 🚫 DEFAULT BULLET TRAIN INVITATION FUNCTIONALITY
44
+ # We put these at the bottom of this file to keep them out of the way. You should define your own methods above here.
45
+
46
+ def set_added_by_membership
47
+ membership.update(added_by: from_membership)
48
+ end
49
+
50
+ def send_invitation_email
51
+ UserMailer.invited(uuid).deliver_later
52
+ end
53
+
54
+ def generate_uuid
55
+ self.uuid = SecureRandom.hex
56
+ end
57
+
58
+ def accept_for(user)
59
+ User.transaction do
60
+ user.memberships << membership
61
+ user.update(current_team: team, former_user: false)
62
+ destroy
63
+ end
64
+ end
65
+
66
+ def name
67
+ I18n.t("invitations.values.name", team_name: team.name)
68
+ end
69
+
70
+ def is_for?(user)
71
+ user.email.downcase.strip == email.downcase.strip
72
+ end
73
+ end
@@ -0,0 +1,164 @@
1
+ class Membership < ApplicationRecord
2
+ # 🚫 DEFAULT BULLET TRAIN MEMBERSHIP FUNCTIONALITY
3
+ # Typically you should avoid adding your own functionality in this section to avoid merge conflicts in the future.
4
+ # (If you specifically want to change Bullet Train's default behavior, that's OK and you can do that here.)
5
+
6
+ # See `docs/permissions.md` for details.
7
+ include Roles::Support
8
+
9
+ belongs_to :user, optional: true
10
+ belongs_to :team
11
+ belongs_to :invitation, optional: true, dependent: :destroy
12
+ belongs_to :added_by, class_name: "Membership", optional: true
13
+ belongs_to :platform_agent_of, class_name: "Platform::Application", optional: true
14
+
15
+ has_many :scaffolding_completely_concrete_tangible_things_assignments, class_name: "Scaffolding::CompletelyConcrete::TangibleThings::Assignment", dependent: :destroy
16
+ has_many :scaffolding_completely_concrete_tangible_things, through: :scaffolding_completely_concrete_tangible_things_assignments, source: :tangible_thing
17
+ has_many :reassignments_scaffolding_completely_concrete_tangible_things_reassignments, class_name: "Memberships::Reassignments::ScaffoldingCompletelyConcreteTangibleThingsReassignment", dependent: :destroy, foreign_key: :membership_id
18
+
19
+ has_many :scaffolding_absolutely_abstract_creative_concepts_collaborators, class_name: "Scaffolding::AbsolutelyAbstract::CreativeConcepts::Collaborator", dependent: :destroy
20
+
21
+ after_destroy do
22
+ # if we're destroying a user's membership to the team they have set as
23
+ # current, then we need to remove that so they don't get an error.
24
+ if user&.current_team == team
25
+ user.current_team = nil
26
+ user.save
27
+ end
28
+ end
29
+
30
+ scope :current_and_invited, -> { includes(:invitation).where("user_id IS NOT NULL OR invitations.id IS NOT NULL").references(:invitation) }
31
+ scope :current, -> { where("user_id IS NOT NULL") }
32
+ scope :tombstones, -> { includes(:invitation).where("user_id IS NULL AND invitations.id IS NULL").references(:invitation) }
33
+
34
+ # ✅ YOUR APPLICATION'S MEMBERSHIP FUNCTIONALITY
35
+ # This is the place where you should implement your own features on top of Bullet Train's functionality. There
36
+ # are a bunch of Super Scaffolding hooks here by default to try and help keep generated code logically organized.
37
+
38
+ # 🚅 add concerns above.
39
+
40
+ # 🚅 add belongs_to associations above.
41
+
42
+ # 🚅 add has_many associations above.
43
+
44
+ # 🚅 add oauth providers above.
45
+
46
+ # 🚅 add has_one associations above.
47
+
48
+ # 🚅 add scopes above.
49
+
50
+ # 🚅 add validations above.
51
+
52
+ # 🚅 add callbacks above.
53
+
54
+ # 🚅 add delegations above.
55
+
56
+ # 🚅 add methods above.
57
+
58
+ # 🚫 DEFAULT BULLET TRAIN TEAM FUNCTIONALITY
59
+ # We put these at the bottom of this file to keep them out of the way. You should define your own methods above here.
60
+
61
+ def name
62
+ full_name
63
+ end
64
+
65
+ def label_string
66
+ full_name
67
+ end
68
+
69
+ # we overload this method so that when setting the list of role ids
70
+ # associated with a membership, admins can never remove the last admin
71
+ # of a team.
72
+ def role_ids=(ids)
73
+ # if this membership was an admin, and the new list of role ids don't include admin.
74
+ if admin? && !ids.include?(Role.admin.id)
75
+ unless team.admins.count > 1
76
+ raise RemovingLastTeamAdminException.new("You can't remove the last team admin.")
77
+ end
78
+ end
79
+
80
+ super(ids)
81
+ end
82
+
83
+ def unclaimed?
84
+ user.nil? && !invitation.nil?
85
+ end
86
+
87
+ def tombstone?
88
+ user.nil? && invitation.nil?
89
+ end
90
+
91
+ def last_admin?
92
+ return false unless admin?
93
+ return false unless user.present?
94
+ team.memberships.current.select(&:admin?) == [self]
95
+ end
96
+
97
+ def nullify_user
98
+ if last_admin?
99
+ raise RemovingLastTeamAdminException.new("You can't remove the last team admin.")
100
+ end
101
+
102
+ if (user_was = user)
103
+ unless user_first_name.present?
104
+ self.user_first_name = user.first_name
105
+ end
106
+
107
+ unless user_last_name.present?
108
+ self.user_last_name = user.last_name
109
+ end
110
+
111
+ unless user_profile_photo_id.present?
112
+ self.user_profile_photo_id = user.profile_photo_id
113
+ end
114
+
115
+ unless user_email.present?
116
+ self.user_email = user.email
117
+ end
118
+
119
+ self.user = nil
120
+ save
121
+
122
+ user_was.invalidate_ability_cache
123
+
124
+ user_was.update(
125
+ current_team: user_was.teams.first,
126
+ former_user: user_was.teams.empty?
127
+ )
128
+ end
129
+
130
+ # we do this here just in case by some weird chance an active membership had an invitation attached.
131
+ invitation&.destroy
132
+ end
133
+
134
+ def email
135
+ user&.email || user_email.presence || invitation&.email
136
+ end
137
+
138
+ def full_name
139
+ user&.full_name || [first_name.presence, last_name.presence].join(" ").presence || email
140
+ end
141
+
142
+ def first_name
143
+ user&.first_name || user_first_name
144
+ end
145
+
146
+ def last_name
147
+ user&.last_name || user_last_name
148
+ end
149
+
150
+ def last_initial
151
+ return nil unless last_name.present?
152
+ "#{last_name}."
153
+ end
154
+
155
+ def first_name_last_initial
156
+ [first_name, last_initial].map(&:present?).join(" ")
157
+ end
158
+
159
+ # TODO utilize this.
160
+ # members shouldn't receive notifications unless they are either an active user or an outstanding invitation.
161
+ def should_receive_notifications?
162
+ invitation.present? || user.present?
163
+ end
164
+ end
@@ -0,0 +1,12 @@
1
+ class Memberships::Reassignments::Assignment < ApplicationRecord
2
+ belongs_to :scaffolding_completely_concrete_tangible_things_reassignment, class_name: "Memberships::Reassignments::ScaffoldingCompletelyConcreteTangibleThingsReassignment"
3
+ belongs_to :membership
4
+
5
+ validate :validate_membership
6
+
7
+ def validate_membership
8
+ unless scaffolding_completely_concrete_tangible_things_reassignment.valid_memberships.include?(membership)
9
+ errors.add(:membership, :invalid)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,38 @@
1
+ class Memberships::Reassignments::ScaffoldingCompletelyConcreteTangibleThingsReassignment < ApplicationRecord
2
+ # 🚅 add concerns above.
3
+
4
+ belongs_to :membership # this is the member being reassigned from.
5
+ has_one :team, through: :membership
6
+ # 🚅 add belongs_to associations above.
7
+
8
+ has_many :assignments
9
+ has_many :memberships, through: :assignments # these are the members being reassigned to.
10
+ # 🚅 add has_many associations above.
11
+
12
+ # 🚅 add has_one associations above.
13
+
14
+ # 🚅 add scopes above.
15
+
16
+ # 🚅 add validations above.
17
+
18
+ after_save :reassign
19
+ # 🚅 add callbacks above.
20
+
21
+ # 🚅 add delegations above.
22
+
23
+ def valid_memberships
24
+ team.memberships.current_and_invited
25
+ end
26
+
27
+ def reassign
28
+ membership.scaffolding_completely_concrete_tangible_things_assignments.each do |existing_assignment|
29
+ memberships.each do |target_membership|
30
+ unless existing_assignment.tangible_thing.memberships.include?(target_membership)
31
+ existing_assignment.tangible_thing.memberships << target_membership
32
+ end
33
+ end
34
+ existing_assignment.destroy
35
+ end
36
+ end
37
+ # 🚅 add methods above.
38
+ end
@@ -0,0 +1,5 @@
1
+ module Memberships::Reassignments
2
+ def self.table_name_prefix
3
+ "memberships_reassignments_"
4
+ end
5
+ end
@@ -0,0 +1,81 @@
1
+ class Team < ApplicationRecord
2
+ # 🚫 DEFAULT BULLET TRAIN TEAM FUNCTIONALITY
3
+ # Typically you should avoid adding your own functionality in this section to avoid merge conflicts in the future.
4
+ # (If you specifically want to change Bullet Train's default behavior, that's OK and you can do that here.)
5
+
6
+ # Outgoing webhooks.
7
+ include Webhooks::Outgoing::TeamSupport
8
+
9
+ # super scaffolding
10
+ unless scaffolding_things_disabled?
11
+ has_many :scaffolding_absolutely_abstract_creative_concepts, class_name: "Scaffolding::AbsolutelyAbstract::CreativeConcept", dependent: :destroy, enable_updates: true
12
+ end
13
+
14
+ # memberships and invitations
15
+ has_many :memberships, dependent: :destroy
16
+ has_many :users, through: :memberships
17
+ has_many :invitations
18
+
19
+ # oauth for grape api
20
+ has_many :platform_applications, class_name: "Platform::Application", dependent: :destroy, foreign_key: :team_id
21
+
22
+ # integrations
23
+ has_many :integrations_stripe_installations, class_name: "Integrations::StripeInstallation", dependent: :destroy if stripe_enabled?
24
+
25
+ # validations
26
+ validates :name, presence: true
27
+ validates :time_zone, inclusion: {in: ActiveSupport::TimeZone.all.map(&:name)}, allow_nil: true
28
+
29
+ # ✅ YOUR APPLICATION'S TEAM FUNCTIONALITY
30
+ # This is the place where you should implement your own features on top of Bullet Train's functionality. There
31
+ # are a bunch of Super Scaffolding hooks here by default to try and help keep generated code logically organized.
32
+
33
+ # 🚅 add concerns above.
34
+
35
+ # 🚅 add belongs_to associations above.
36
+
37
+ # 🚅 add has_many associations above.
38
+
39
+ # 🚅 add oauth providers above.
40
+
41
+ # 🚅 add has_one associations above.
42
+
43
+ # 🚅 add scopes above.
44
+
45
+ # 🚅 add validations above.
46
+
47
+ # 🚅 add callbacks above.
48
+
49
+ # 🚅 add delegations above.
50
+
51
+ # 🚅 add methods above.
52
+
53
+ # 🚫 DEFAULT BULLET TRAIN TEAM FUNCTIONALITY
54
+ # We put these at the bottom of this file to keep them out of the way. You should define your own methods above here.
55
+
56
+ def admins
57
+ memberships.current_and_invited.admins
58
+ end
59
+
60
+ def admin_users
61
+ admins.map(&:user).compact
62
+ end
63
+
64
+ def primary_contact
65
+ admin_users.min { |user| user.created_at }
66
+ end
67
+
68
+ def formatted_email_address
69
+ primary_contact.email
70
+ end
71
+
72
+ def invalidate_caches
73
+ users.map(&:invalidate_ability_cache)
74
+ end
75
+
76
+ def team
77
+ # some generic features appeal to the `team` method for security or scoping purposes, but sometimes those same
78
+ # generic functions need to function for a team model as well, so we do this.
79
+ self
80
+ end
81
+ end