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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +8 -0
- data/app/assets/config/bullet_train_manifest.js +0 -0
- data/app/controllers/account/invitations_controller.rb +146 -0
- data/app/controllers/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments_controller.rb +83 -0
- data/app/controllers/account/memberships_controller.rb +132 -0
- data/app/controllers/account/onboarding/user_details_controller.rb +63 -0
- data/app/controllers/account/onboarding/user_email_controller.rb +65 -0
- data/app/controllers/account/teams_controller.rb +122 -0
- data/app/controllers/account/users_controller.rb +59 -0
- data/app/helpers/account/invitations_helper.rb +2 -0
- data/app/helpers/account/memberships_helper.rb +9 -0
- data/app/helpers/account/teams_helper.rb +80 -0
- data/app/helpers/account/users_helper.rb +81 -0
- data/app/helpers/invitation_only_helper.rb +10 -0
- data/app/helpers/invitations_helper.rb +17 -0
- data/app/models/invitation.rb +73 -0
- data/app/models/membership.rb +164 -0
- data/app/models/memberships/reassignments/assignment.rb +12 -0
- data/app/models/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignment.rb +38 -0
- data/app/models/memberships/reassignments.rb +5 -0
- data/app/models/team.rb +81 -0
- data/app/models/user.rb +191 -0
- data/app/views/account/invitations/_breadcrumbs.html.erb +9 -0
- data/app/views/account/invitations/_form.html.erb +49 -0
- data/app/views/account/invitations/_invitation.json.jbuilder +7 -0
- data/app/views/account/invitations/index.json.jbuilder +1 -0
- data/app/views/account/invitations/new.html.erb +12 -0
- data/app/views/account/invitations/show.html.erb +30 -0
- data/app/views/account/invitations/show.json.jbuilder +1 -0
- data/app/views/account/memberships/_breadcrumbs.html.erb +8 -0
- data/app/views/account/memberships/_form.html.erb +45 -0
- data/app/views/account/memberships/_index.html.erb +66 -0
- data/app/views/account/memberships/_menu_item.html.erb +8 -0
- data/app/views/account/memberships/_tombstones.html.erb +59 -0
- data/app/views/account/memberships/edit.html.erb +22 -0
- data/app/views/account/memberships/index.html.erb +7 -0
- data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/_breadcrumbs.html.erb +10 -0
- data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/_form.html.erb +24 -0
- data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/_scaffolding_completely_concrete_tangible_things_reassignment.json.jbuilder +8 -0
- data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/index.json.jbuilder +1 -0
- data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/new.html.erb +11 -0
- data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/show.json.jbuilder +1 -0
- data/app/views/account/memberships/show.html.erb +60 -0
- data/app/views/account/onboarding/user_details/edit.html.erb +72 -0
- data/app/views/account/onboarding/user_email/edit.html.erb +33 -0
- data/app/views/account/teams/_breadcrumbs.html.erb +11 -0
- data/app/views/account/teams/_form.html.erb +22 -0
- data/app/views/account/teams/_index.html.erb +33 -0
- data/app/views/account/teams/_menu_item.html.erb +8 -0
- data/app/views/account/teams/_team.json.jbuilder +9 -0
- data/app/views/account/teams/edit.html.erb +12 -0
- data/app/views/account/teams/index.html.erb +6 -0
- data/app/views/account/teams/index.json.jbuilder +1 -0
- data/app/views/account/teams/new.html.erb +88 -0
- data/app/views/account/teams/show.html.erb +25 -0
- data/app/views/account/teams/show.json.jbuilder +1 -0
- data/app/views/account/users/_breadcrumbs.html.erb +4 -0
- data/app/views/account/users/_form.html.erb +39 -0
- data/app/views/account/users/edit.html.erb +50 -0
- data/app/views/account/users/show.html.erb +17 -0
- data/config/locales/en/invitations.en.yml +69 -0
- data/config/locales/en/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments.en.yml +42 -0
- data/config/locales/en/memberships.en.yml +99 -0
- data/config/locales/en/onboarding/user_details.en.yml +11 -0
- data/config/locales/en/onboarding/user_email.en.yml +13 -0
- data/config/locales/en/teams.en.yml +85 -0
- data/config/locales/en/users.en.yml +110 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20161115160419_devise_create_users.rb +41 -0
- data/db/migrate/20161116003852_add_api_key_to_user.rb +6 -0
- data/db/migrate/20161117154605_create_teams.rb +10 -0
- data/db/migrate/20161117154709_create_memberships.rb +11 -0
- data/db/migrate/20161203193930_add_current_team_to_user.rb +5 -0
- data/db/migrate/20161204234150_create_invitations.rb +11 -0
- data/db/migrate/20161205154821_add_team_to_invitation.rb +5 -0
- data/db/migrate/20161205164613_add_admin_to_invitation.rb +5 -0
- data/db/migrate/20170908205756_add_names_to_user.rb +6 -0
- data/db/migrate/20170915215309_add_team_to_thing.rb +5 -0
- data/db/migrate/20171105001408_remove_api_key_from_user.rb +6 -0
- data/db/migrate/20180326124105_add_timezone_to_user.rb +5 -0
- data/db/migrate/20180902142350_create_membership_roles.rb +10 -0
- data/db/migrate/20180902143758_remove_admin_from_membership.rb +5 -0
- data/db/migrate/20180902154611_create_invitation_roles.rb +10 -0
- data/db/migrate/20180902154652_migrate_admin_flag_on_invitations.rb +14 -0
- data/db/migrate/20180902195848_remove_admin_from_invitation.rb +5 -0
- data/db/migrate/20180903101707_add_last_seen_at_to_users.rb +5 -0
- data/db/migrate/20190321203224_add_profile_photo_id_to_user.rb +5 -0
- data/db/migrate/20190519230202_add_ability_cache_to_user.rb +5 -0
- data/db/migrate/20190628194704_add_last_notification_email_sent_at_to_user.rb +5 -0
- data/db/migrate/20200211034208_add_invitation_to_membership.rb +5 -0
- data/db/migrate/20200211044616_drop_invitation_roles_table.rb +10 -0
- data/db/migrate/20200213052748_add_former_user_fields_to_membership.rb +8 -0
- data/db/migrate/20200213235037_add_former_user_to_user.rb +7 -0
- data/db/migrate/20200219013834_add_added_by_to_membership.rb +5 -0
- data/db/migrate/20200219015116_rename_from_user_to_from_membership.rb +5 -0
- data/db/migrate/20200726222314_add_being_destroyed_to_team.rb +5 -0
- data/db/migrate/20200727171308_add_devise_two_factor_to_users.rb +9 -0
- data/db/migrate/20200727175949_add_devise_two_factor_backupable_to_users.rb +5 -0
- data/db/migrate/20210304133200_add_time_zone_to_team.rb +5 -0
- data/db/migrate/20210816072419_add_locale_to_users.rb +5 -0
- data/db/migrate/20210816072508_add_locale_to_teams.rb +5 -0
- data/db/migrate/20211020200855_add_doorkeeper_application_to_memberships.rb +5 -0
- data/db/migrate/20211027002944_add_doorkeeper_application_to_users.rb +5 -0
- data/lib/bullet_train/engine.rb +4 -0
- data/lib/bullet_train/version.rb +3 -0
- data/lib/bullet_train.rb +6 -0
- data/lib/tasks/bullet_train_tasks.rake +4 -0
- 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,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,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
|
data/app/models/team.rb
ADDED
|
@@ -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
|