bullet_train 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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