bullet_train 1.0.1 → 1.0.5
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 +4 -4
- data/app/controllers/account/invitations_controller.rb +1 -144
- data/app/controllers/account/memberships_controller.rb +1 -130
- data/app/controllers/account/onboarding/user_details_controller.rb +1 -61
- data/app/controllers/account/onboarding/user_email_controller.rb +1 -63
- data/app/controllers/account/teams_controller.rb +1 -120
- data/app/controllers/account/users_controller.rb +1 -57
- data/app/controllers/concerns/account/invitations/controller_base.rb +150 -0
- data/app/controllers/concerns/account/memberships/controller_base.rb +136 -0
- data/app/controllers/concerns/account/onboarding/user_details/controller_base.rb +67 -0
- data/app/controllers/concerns/account/onboarding/user_email/controller_base.rb +69 -0
- data/app/controllers/concerns/account/teams/controller_base.rb +126 -0
- data/app/controllers/concerns/account/users/controller_base.rb +63 -0
- data/app/models/concerns/invitations/base.rb +46 -0
- data/app/models/concerns/memberships/base.rb +137 -0
- data/app/models/concerns/teams/base.rb +51 -0
- data/app/models/concerns/users/base.rb +164 -0
- data/app/models/invitation.rb +1 -52
- data/app/models/invitations.rb +5 -0
- data/app/models/membership.rb +1 -143
- data/app/models/memberships.rb +5 -0
- data/app/models/team.rb +1 -59
- data/app/models/teams.rb +5 -0
- data/app/models/user.rb +1 -170
- data/app/models/users.rb +5 -0
- data/config/routes.rb +40 -0
- data/lib/bullet_train/version.rb +1 -1
- metadata +16 -2
@@ -0,0 +1,137 @@
|
|
1
|
+
module Memberships::Base
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
# See `docs/permissions.md` for details.
|
6
|
+
include Roles::Support
|
7
|
+
|
8
|
+
belongs_to :user, optional: true
|
9
|
+
belongs_to :team
|
10
|
+
belongs_to :invitation, optional: true, dependent: :destroy
|
11
|
+
belongs_to :added_by, class_name: "Membership", optional: true
|
12
|
+
belongs_to :platform_agent_of, class_name: "Platform::Application", optional: true
|
13
|
+
|
14
|
+
has_many :scaffolding_completely_concrete_tangible_things_assignments, class_name: "Scaffolding::CompletelyConcrete::TangibleThings::Assignment", dependent: :destroy
|
15
|
+
has_many :scaffolding_completely_concrete_tangible_things, through: :scaffolding_completely_concrete_tangible_things_assignments, source: :tangible_thing
|
16
|
+
has_many :reassignments_scaffolding_completely_concrete_tangible_things_reassignments, class_name: "Memberships::Reassignments::ScaffoldingCompletelyConcreteTangibleThingsReassignment", dependent: :destroy, foreign_key: :membership_id
|
17
|
+
|
18
|
+
has_many :scaffolding_absolutely_abstract_creative_concepts_collaborators, class_name: "Scaffolding::AbsolutelyAbstract::CreativeConcepts::Collaborator", dependent: :destroy
|
19
|
+
|
20
|
+
after_destroy do
|
21
|
+
# if we're destroying a user's membership to the team they have set as
|
22
|
+
# current, then we need to remove that so they don't get an error.
|
23
|
+
if user&.current_team == team
|
24
|
+
user.current_team = nil
|
25
|
+
user.save
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
scope :current_and_invited, -> { includes(:invitation).where("user_id IS NOT NULL OR invitations.id IS NOT NULL").references(:invitation) }
|
30
|
+
scope :current, -> { where("user_id IS NOT NULL") }
|
31
|
+
scope :tombstones, -> { includes(:invitation).where("user_id IS NULL AND invitations.id IS NULL").references(:invitation) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def name
|
35
|
+
full_name
|
36
|
+
end
|
37
|
+
|
38
|
+
def label_string
|
39
|
+
full_name
|
40
|
+
end
|
41
|
+
|
42
|
+
# we overload this method so that when setting the list of role ids
|
43
|
+
# associated with a membership, admins can never remove the last admin
|
44
|
+
# of a team.
|
45
|
+
def role_ids=(ids)
|
46
|
+
# if this membership was an admin, and the new list of role ids don't include admin.
|
47
|
+
if admin? && !ids.include?(Role.admin.id)
|
48
|
+
unless team.admins.count > 1
|
49
|
+
raise RemovingLastTeamAdminException.new("You can't remove the last team admin.")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
super(ids)
|
54
|
+
end
|
55
|
+
|
56
|
+
def unclaimed?
|
57
|
+
user.nil? && !invitation.nil?
|
58
|
+
end
|
59
|
+
|
60
|
+
def tombstone?
|
61
|
+
user.nil? && invitation.nil?
|
62
|
+
end
|
63
|
+
|
64
|
+
def last_admin?
|
65
|
+
return false unless admin?
|
66
|
+
return false unless user.present?
|
67
|
+
team.memberships.current.select(&:admin?) == [self]
|
68
|
+
end
|
69
|
+
|
70
|
+
def nullify_user
|
71
|
+
if last_admin?
|
72
|
+
raise RemovingLastTeamAdminException.new("You can't remove the last team admin.")
|
73
|
+
end
|
74
|
+
|
75
|
+
if (user_was = user)
|
76
|
+
unless user_first_name.present?
|
77
|
+
self.user_first_name = user.first_name
|
78
|
+
end
|
79
|
+
|
80
|
+
unless user_last_name.present?
|
81
|
+
self.user_last_name = user.last_name
|
82
|
+
end
|
83
|
+
|
84
|
+
unless user_profile_photo_id.present?
|
85
|
+
self.user_profile_photo_id = user.profile_photo_id
|
86
|
+
end
|
87
|
+
|
88
|
+
unless user_email.present?
|
89
|
+
self.user_email = user.email
|
90
|
+
end
|
91
|
+
|
92
|
+
self.user = nil
|
93
|
+
save
|
94
|
+
|
95
|
+
user_was.invalidate_ability_cache
|
96
|
+
|
97
|
+
user_was.update(
|
98
|
+
current_team: user_was.teams.first,
|
99
|
+
former_user: user_was.teams.empty?
|
100
|
+
)
|
101
|
+
end
|
102
|
+
|
103
|
+
# we do this here just in case by some weird chance an active membership had an invitation attached.
|
104
|
+
invitation&.destroy
|
105
|
+
end
|
106
|
+
|
107
|
+
def email
|
108
|
+
user&.email || user_email.presence || invitation&.email
|
109
|
+
end
|
110
|
+
|
111
|
+
def full_name
|
112
|
+
user&.full_name || [first_name.presence, last_name.presence].join(" ").presence || email
|
113
|
+
end
|
114
|
+
|
115
|
+
def first_name
|
116
|
+
user&.first_name || user_first_name
|
117
|
+
end
|
118
|
+
|
119
|
+
def last_name
|
120
|
+
user&.last_name || user_last_name
|
121
|
+
end
|
122
|
+
|
123
|
+
def last_initial
|
124
|
+
return nil unless last_name.present?
|
125
|
+
"#{last_name}."
|
126
|
+
end
|
127
|
+
|
128
|
+
def first_name_last_initial
|
129
|
+
[first_name, last_initial].map(&:present?).join(" ")
|
130
|
+
end
|
131
|
+
|
132
|
+
# TODO utilize this.
|
133
|
+
# members shouldn't receive notifications unless they are either an active user or an outstanding invitation.
|
134
|
+
def should_receive_notifications?
|
135
|
+
invitation.present? || user.present?
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Teams::Base
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
# super scaffolding
|
6
|
+
unless scaffolding_things_disabled?
|
7
|
+
has_many :scaffolding_absolutely_abstract_creative_concepts, class_name: "Scaffolding::AbsolutelyAbstract::CreativeConcept", dependent: :destroy, enable_updates: true
|
8
|
+
end
|
9
|
+
|
10
|
+
# memberships and invitations
|
11
|
+
has_many :memberships, dependent: :destroy
|
12
|
+
has_many :users, through: :memberships
|
13
|
+
has_many :invitations
|
14
|
+
|
15
|
+
# oauth for grape api
|
16
|
+
has_many :platform_applications, class_name: "Platform::Application", dependent: :destroy, foreign_key: :team_id
|
17
|
+
|
18
|
+
# integrations
|
19
|
+
has_many :integrations_stripe_installations, class_name: "Integrations::StripeInstallation", dependent: :destroy if stripe_enabled?
|
20
|
+
|
21
|
+
# validations
|
22
|
+
validates :name, presence: true
|
23
|
+
validates :time_zone, inclusion: {in: ActiveSupport::TimeZone.all.map(&:name)}, allow_nil: true
|
24
|
+
end
|
25
|
+
|
26
|
+
def admins
|
27
|
+
memberships.current_and_invited.admins
|
28
|
+
end
|
29
|
+
|
30
|
+
def admin_users
|
31
|
+
admins.map(&:user).compact
|
32
|
+
end
|
33
|
+
|
34
|
+
def primary_contact
|
35
|
+
admin_users.min { |user| user.created_at }
|
36
|
+
end
|
37
|
+
|
38
|
+
def formatted_email_address
|
39
|
+
primary_contact.email
|
40
|
+
end
|
41
|
+
|
42
|
+
def invalidate_caches
|
43
|
+
users.map(&:invalidate_ability_cache)
|
44
|
+
end
|
45
|
+
|
46
|
+
def team
|
47
|
+
# some generic features appeal to the `team` method for security or scoping purposes, but sometimes those same
|
48
|
+
# generic functions need to function for a team model as well, so we do this.
|
49
|
+
self
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
module Users::Base
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
if two_factor_authentication_enabled?
|
6
|
+
devise :two_factor_authenticatable, :two_factor_backupable, :omniauthable,
|
7
|
+
:registerable, :recoverable, :rememberable, :trackable, :validatable,
|
8
|
+
otp_secret_encryption_key: ENV["TWO_FACTOR_ENCRYPTION_KEY"]
|
9
|
+
else
|
10
|
+
devise :omniauthable, :database_authenticatable, :registerable,
|
11
|
+
:recoverable, :rememberable, :trackable, :validatable
|
12
|
+
end
|
13
|
+
|
14
|
+
# teams
|
15
|
+
has_many :memberships, dependent: :destroy
|
16
|
+
has_many :scaffolding_absolutely_abstract_creative_concepts_collaborators, through: :memberships
|
17
|
+
has_many :teams, through: :memberships
|
18
|
+
belongs_to :current_team, class_name: "Team", optional: true
|
19
|
+
accepts_nested_attributes_for :current_team
|
20
|
+
|
21
|
+
# oauth providers
|
22
|
+
has_many :oauth_stripe_accounts, class_name: "Oauth::StripeAccount" if stripe_enabled?
|
23
|
+
|
24
|
+
# platform functionality.
|
25
|
+
belongs_to :platform_agent_of, class_name: "Platform::Application", optional: true
|
26
|
+
|
27
|
+
# validations
|
28
|
+
validate :real_emails_only
|
29
|
+
validates :time_zone, inclusion: {in: ActiveSupport::TimeZone.all.map(&:name)}, allow_nil: true
|
30
|
+
|
31
|
+
# callbacks
|
32
|
+
after_update :set_teams_time_zone
|
33
|
+
end
|
34
|
+
|
35
|
+
# TODO we need to update this to some sort of invalid email address or something
|
36
|
+
# people know to ignore. it would be a security problem to have this pointing
|
37
|
+
# at anybody's real email address.
|
38
|
+
def email_is_oauth_placeholder?
|
39
|
+
!!email.match(/noreply\+.*@bullettrain.co/)
|
40
|
+
end
|
41
|
+
|
42
|
+
def label_string
|
43
|
+
name
|
44
|
+
end
|
45
|
+
|
46
|
+
def name
|
47
|
+
full_name.present? ? full_name : email
|
48
|
+
end
|
49
|
+
|
50
|
+
def full_name
|
51
|
+
[first_name_was, last_name_was].select(&:present?).join(" ")
|
52
|
+
end
|
53
|
+
|
54
|
+
def details_provided?
|
55
|
+
first_name.present? && last_name.present? && current_team.name.present?
|
56
|
+
end
|
57
|
+
|
58
|
+
def send_welcome_email
|
59
|
+
UserMailer.welcome(self).deliver_later
|
60
|
+
end
|
61
|
+
|
62
|
+
def create_default_team
|
63
|
+
# This creates a `Membership`, because `User` `has_many :teams, through: :memberships`
|
64
|
+
# TODO The team name should take into account the user's current locale.
|
65
|
+
default_team = teams.create(name: "Your Team", time_zone: time_zone)
|
66
|
+
memberships.find_by(team: default_team).update role_ids: [Role.admin.id]
|
67
|
+
update(current_team: default_team)
|
68
|
+
end
|
69
|
+
|
70
|
+
def real_emails_only
|
71
|
+
if ENV["REALEMAIL_API_KEY"] && !Rails.env.test?
|
72
|
+
uri = URI("https://realemail.expeditedaddons.com")
|
73
|
+
|
74
|
+
# Change the input parameters here
|
75
|
+
uri.query = URI.encode_www_form({
|
76
|
+
api_key: ENV["REAL_EMAIL_KEY"],
|
77
|
+
email: email,
|
78
|
+
fix_typos: false
|
79
|
+
})
|
80
|
+
|
81
|
+
# Results are returned as a JSON object
|
82
|
+
result = JSON.parse(Net::HTTP.get_response(uri).body)
|
83
|
+
|
84
|
+
if result["syntax_error"]
|
85
|
+
errors.add(:email, "is not a valid email address")
|
86
|
+
elsif result["domain_error"] || (result.key?("mx_records_found") && !result["mx_records_found"])
|
87
|
+
errors.add(:email, "can't actually receive emails")
|
88
|
+
elsif result["is_disposable"]
|
89
|
+
errors.add(:email, "is a disposable email address")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def multiple_teams?
|
95
|
+
teams.count > 1
|
96
|
+
end
|
97
|
+
|
98
|
+
def one_team?
|
99
|
+
!multiple_teams?
|
100
|
+
end
|
101
|
+
|
102
|
+
def formatted_email_address
|
103
|
+
if details_provided?
|
104
|
+
"\"#{first_name} #{last_name}\" <#{email}>"
|
105
|
+
else
|
106
|
+
email
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def administrating_team_ids
|
111
|
+
parent_ids_for(Role.admin, :memberships, :team)
|
112
|
+
end
|
113
|
+
|
114
|
+
def parent_ids_for(role, through, parent)
|
115
|
+
parent_id_column = "#{parent}_id"
|
116
|
+
key = "#{role.key}_#{through}_#{parent_id_column}s"
|
117
|
+
return ability_cache[key] if ability_cache && ability_cache[key]
|
118
|
+
role = nil if role.default?
|
119
|
+
value = send(through).with_role(role).distinct.pluck(parent_id_column)
|
120
|
+
current_cache = ability_cache || {}
|
121
|
+
current_cache[key] = value
|
122
|
+
update_column :ability_cache, current_cache
|
123
|
+
value
|
124
|
+
end
|
125
|
+
|
126
|
+
def invalidate_ability_cache
|
127
|
+
update_column(:ability_cache, {})
|
128
|
+
end
|
129
|
+
|
130
|
+
def otp_qr_code
|
131
|
+
issuer = I18n.t("application.name")
|
132
|
+
label = "#{issuer}:#{email}"
|
133
|
+
RQRCode::QRCode.new(otp_provisioning_uri(label, issuer: issuer))
|
134
|
+
end
|
135
|
+
|
136
|
+
def scaffolding_absolutely_abstract_creative_concepts_collaborators
|
137
|
+
Scaffolding::AbsolutelyAbstract::CreativeConcepts::Collaborator.joins(:membership).where(membership: {user_id: id})
|
138
|
+
end
|
139
|
+
|
140
|
+
def admin_scaffolding_absolutely_abstract_creative_concepts_ids
|
141
|
+
scaffolding_absolutely_abstract_creative_concepts_collaborators.admins.pluck(:creative_concept_id)
|
142
|
+
end
|
143
|
+
|
144
|
+
def editor_scaffolding_absolutely_abstract_creative_concepts_ids
|
145
|
+
scaffolding_absolutely_abstract_creative_concepts_collaborators.editors.pluck(:creative_concept_id)
|
146
|
+
end
|
147
|
+
|
148
|
+
def viewer_scaffolding_absolutely_abstract_creative_concepts_ids
|
149
|
+
scaffolding_absolutely_abstract_creative_concepts_collaborators.viewers.pluck(:creative_concept_id)
|
150
|
+
end
|
151
|
+
|
152
|
+
def developer?
|
153
|
+
return false unless ENV["DEVELOPER_EMAILS"]
|
154
|
+
# we use email_was so they can't try setting their email to the email of an admin.
|
155
|
+
return false unless email_was
|
156
|
+
ENV["DEVELOPER_EMAILS"].split(",").include?(email_was)
|
157
|
+
end
|
158
|
+
|
159
|
+
def set_teams_time_zone
|
160
|
+
teams.where(time_zone: nil).each do |team|
|
161
|
+
team.update(time_zone: time_zone) if team.users.count == 1
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
data/app/models/invitation.rb
CHANGED
@@ -1,25 +1,5 @@
|
|
1
1
|
class Invitation < ApplicationRecord
|
2
|
-
|
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
|
-
|
2
|
+
include Invitations::Core
|
23
3
|
# 🚅 add concerns above.
|
24
4
|
|
25
5
|
# 🚅 add belongs_to associations above.
|
@@ -39,35 +19,4 @@ class Invitation < ApplicationRecord
|
|
39
19
|
# 🚅 add delegations above.
|
40
20
|
|
41
21
|
# 🚅 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
22
|
end
|
data/app/models/membership.rb
CHANGED
@@ -1,40 +1,5 @@
|
|
1
1
|
class Membership < ApplicationRecord
|
2
|
-
|
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
|
-
|
2
|
+
include Memberships::Core
|
38
3
|
# 🚅 add concerns above.
|
39
4
|
|
40
5
|
# 🚅 add belongs_to associations above.
|
@@ -54,111 +19,4 @@ class Membership < ApplicationRecord
|
|
54
19
|
# 🚅 add delegations above.
|
55
20
|
|
56
21
|
# 🚅 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
22
|
end
|
data/app/models/team.rb
CHANGED
@@ -1,35 +1,6 @@
|
|
1
1
|
class Team < ApplicationRecord
|
2
|
-
|
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.
|
2
|
+
include Teams::Core
|
7
3
|
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
4
|
# 🚅 add concerns above.
|
34
5
|
|
35
6
|
# 🚅 add belongs_to associations above.
|
@@ -49,33 +20,4 @@ class Team < ApplicationRecord
|
|
49
20
|
# 🚅 add delegations above.
|
50
21
|
|
51
22
|
# 🚅 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
23
|
end
|