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