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.
@@ -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
@@ -1,25 +1,5 @@
1
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
-
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
@@ -0,0 +1,5 @@
1
+ module Invitations
2
+ def self.table_name_prefix
3
+ "users_"
4
+ end
5
+ end
@@ -1,40 +1,5 @@
1
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
-
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
@@ -0,0 +1,5 @@
1
+ module Memberships
2
+ def self.table_name_prefix
3
+ "memberships_"
4
+ end
5
+ end
data/app/models/team.rb CHANGED
@@ -1,35 +1,6 @@
1
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.
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
@@ -0,0 +1,5 @@
1
+ module Teams
2
+ def self.table_name_prefix
3
+ "teams_"
4
+ end
5
+ end