Skip to content

Commit accd03d

Browse files
committed
Ensure reliable user registration including creating person community membership
1 parent 32908d3 commit accd03d

File tree

7 files changed

+371
-44
lines changed

7 files changed

+371
-44
lines changed

.rubocop.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
inherit_from: .rubocop_todo.yml
2+
13
AllCops:
24
Exclude:
35
- 'bin/*'

.rubocop_todo.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# This configuration was generated by
2+
# `rubocop --auto-gen-config`
3+
# on 2025-11-04 02:06:37 UTC using RuboCop version 1.81.6.
4+
# The point is for the user to remove these configuration records
5+
# one by one as the offenses are removed from the code base.
6+
# Note that changes in the inspected code, or installation of new
7+
# versions of RuboCop, may require this file to be generated again.
8+
9+
# Offense count: 52
10+
# This cop supports safe autocorrection (--autocorrect).
11+
Lint/RedundantCopDisableDirective:
12+
Enabled: false
13+
14+
# Offense count: 2
15+
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
16+
Metrics/MethodLength:
17+
Max: 14
18+
19+
# Offense count: 1
20+
# This cop supports safe autocorrection (--autocorrect).
21+
# Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions.
22+
# SupportedStyles: assign_to_condition, assign_inside_condition
23+
Style/ConditionalAssignment:
24+
Exclude:
25+
- 'app/controllers/better_together/joatu/hub_controller.rb'
26+
27+
# Offense count: 29
28+
# This cop supports safe autocorrection (--autocorrect).
29+
Style/IfUnlessModifier:
30+
Enabled: false
31+
32+
# Offense count: 1
33+
# Configuration parameters: Max.
34+
Style/SafeNavigationChainLength:
35+
Exclude:
36+
- 'spec/features/devise/registration_spec.rb'
37+
38+
# Offense count: 1
39+
# This cop supports safe autocorrection (--autocorrect).
40+
# Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
41+
# URISchemes: http, https
42+
Layout/LineLength:
43+
Max: 145

app/controllers/better_together/users/registrations_controller.rb

Lines changed: 142 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class RegistrationsController < ::Devise::RegistrationsController # rubocop:todo
77
include DeviseLocales
88

99
skip_before_action :check_platform_privacy
10+
before_action :configure_permitted_parameters
1011
before_action :set_required_agreements, only: %i[new create]
1112
before_action :set_event_invitation_from_session, only: %i[new create]
1213
before_action :configure_account_update_params, only: [:update]
@@ -63,18 +64,12 @@ def update # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
6364

6465
def new
6566
super do |user|
66-
# Pre-fill email from platform invitation
67-
user.email = @platform_invitation.invitee_email if @platform_invitation && user.email.empty?
68-
69-
if @event_invitation
70-
# Pre-fill email from event invitation
71-
user.email = @event_invitation.invitee_email if @event_invitation && user.email.empty?
72-
user.person = @event_invitation.invitee if @event_invitation.invitee.present?
73-
end
67+
setup_user_from_invitations(user)
68+
user.build_person unless user.person
7469
end
7570
end
7671

77-
def create # rubocop:todo Metrics/MethodLength
72+
def create # rubocop:todo Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
7873
unless agreements_accepted?
7974
handle_agreements_not_accepted
8075
return
@@ -87,11 +82,25 @@ def create # rubocop:todo Metrics/MethodLength
8782
return
8883
end
8984

85+
# Use transaction for all user creation and associated records
9086
ActiveRecord::Base.transaction do
91-
super do |user|
92-
handle_user_creation(user) if user.persisted?
87+
# Call Devise's default create behavior
88+
super
89+
90+
# Handle post-registration setup if user was created successfully
91+
if resource.persisted? && resource.errors.empty?
92+
handle_user_creation(resource)
93+
elsif resource.persisted?
94+
# User was created but has errors - rollback to maintain consistency
95+
raise ActiveRecord::Rollback
9396
end
9497
end
98+
rescue ActiveRecord::RecordInvalid, ActiveRecord::InvalidForeignKey => e
99+
# Clean up and show user-friendly error
100+
Rails.logger.error "Registration failed: #{e.message}"
101+
build_resource(sign_up_params) if resource.nil?
102+
resource&.errors&.add(:base, 'Registration could not be completed. Please try again.')
103+
respond_with resource
95104
end
96105

97106
protected
@@ -105,6 +114,10 @@ def configure_account_update_params
105114
keys: %i[email password password_confirmation current_password])
106115
end
107116

117+
def configure_permitted_parameters
118+
devise_parameter_sanitizer.permit(:sign_up, keys: [person_attributes: %i[identifier name description]])
119+
end
120+
108121
def set_required_agreements
109122
@privacy_policy_agreement = BetterTogether::Agreement.find_by(identifier: 'privacy_policy')
110123
@terms_of_service_agreement = BetterTogether::Agreement.find_by(identifier: 'terms_of_service')
@@ -170,32 +183,116 @@ def handle_agreements_not_accepted
170183
respond_with resource
171184
end
172185

186+
def setup_user_from_invitations(user)
187+
# Pre-fill email from platform invitation
188+
user.email = @platform_invitation.invitee_email if @platform_invitation && user.email.empty?
189+
190+
return unless @event_invitation
191+
192+
# Pre-fill email from event invitation
193+
user.email = @event_invitation.invitee_email if @event_invitation && user.email.empty?
194+
user.person = @event_invitation.invitee if @event_invitation.invitee.present?
195+
end
196+
173197
def handle_user_creation(user)
174-
setup_person_for_user(user)
175-
return unless user.save!
198+
return unless event_invitation_person_updated?(user)
176199

200+
# Reload user to ensure all nested attributes and associations are properly loaded
177201
user.reload
178-
setup_community_membership(user)
202+
person = user.person
203+
204+
return unless person_persisted?(user, person)
205+
206+
setup_community_membership(user, person)
179207
handle_platform_invitation(user)
180208
handle_event_invitation(user)
181-
create_agreement_participants(user.person)
209+
create_agreement_participants(person)
210+
end
211+
212+
def event_invitation_person_updated?(user)
213+
return true unless @event_invitation&.invitee.present?
214+
215+
return true if user.person.update(person_params)
216+
217+
Rails.logger.error "Failed to update person for event invitation: #{user.person.errors.full_messages}"
218+
false
219+
end
220+
221+
def person_persisted?(user, person)
222+
return true if person&.persisted?
223+
224+
Rails.logger.error "Person not found or not persisted for user #{user.id}"
225+
false
182226
end
183227

184228
def setup_person_for_user(user)
185-
if @event_invitation && @event_invitation.invitee.present?
186-
user.person = @event_invitation.invitee
187-
user.person.update(person_params)
188-
else
189-
user.build_person(person_params)
190-
end
229+
return update_existing_person_for_event(user) if @event_invitation&.invitee.present?
230+
231+
create_new_person_for_user(user)
232+
end
233+
234+
def update_existing_person_for_event(user)
235+
user.person = @event_invitation.invitee
236+
return if user.person.update(person_params)
237+
238+
Rails.logger.error "Failed to update person for event invitation: #{user.person.errors.full_messages}"
239+
user.errors.add(:person, 'Could not update person information')
191240
end
192241

193-
def setup_community_membership(user)
242+
def create_new_person_for_user(user)
243+
return handle_empty_person_params(user) if person_params.empty?
244+
245+
user.build_person(person_params)
246+
return unless person_validated_and_saved?(user)
247+
248+
save_person_identification(user)
249+
end
250+
251+
def handle_empty_person_params(user)
252+
Rails.logger.error 'Person params are empty, cannot build person'
253+
user.errors.add(:person, 'Person information is required')
254+
end
255+
256+
def person_validated_and_saved?(user)
257+
return save_person?(user) if user.person.valid?
258+
259+
Rails.logger.error "Person validation failed: #{user.person.errors.full_messages}"
260+
user.errors.add(:person, 'Person information is invalid')
261+
false
262+
end
263+
264+
def save_person?(user)
265+
return true if user.person.save
266+
267+
Rails.logger.error "Failed to save person: #{user.person.errors.full_messages}"
268+
user.errors.add(:person, 'Could not save person information')
269+
false
270+
end
271+
272+
def save_person_identification(user)
273+
person_identification = user.person_identification
274+
return if person_identification&.save
275+
276+
Rails.logger.error "Failed to save person identification: #{person_identification&.errors&.full_messages}"
277+
user.errors.add(:person, 'Could not link person to user')
278+
end
279+
280+
def setup_community_membership(user, person_param = nil)
281+
person = person_param || user.person
194282
community_role = determine_community_role
195-
helpers.host_community.person_community_memberships.find_or_create_by!(
196-
member: user.person,
197-
role: community_role
198-
)
283+
284+
begin
285+
helpers.host_community.person_community_memberships.find_or_create_by!(
286+
member: person,
287+
role: community_role
288+
)
289+
rescue ActiveRecord::InvalidForeignKey => e
290+
Rails.logger.error "Foreign key violation creating community membership: #{e.message}"
291+
raise e
292+
rescue StandardError => e
293+
Rails.logger.error "Unexpected error creating community membership: #{e.message}"
294+
raise e
295+
end
199296
end
200297

201298
def handle_platform_invitation(user)
@@ -235,10 +332,18 @@ def after_update_path_for(_resource)
235332
end
236333

237334
def person_params
335+
return {} unless params[:user] && params[:user][:person_attributes]
336+
238337
params.require(:user).require(:person_attributes).permit(%i[identifier name description])
338+
rescue ActionController::ParameterMissing => e
339+
Rails.logger.error "Missing person parameters: #{e.message}"
340+
{}
239341
end
240342

241343
def agreements_accepted?
344+
# Ensure required agreements are set
345+
set_required_agreements if @privacy_policy_agreement.nil?
346+
242347
required = [params[:privacy_policy_agreement], params[:terms_of_service_agreement]]
243348
# If a code of conduct agreement exists, require it as well
244349
required << params[:code_of_conduct_agreement] if @code_of_conduct_agreement.present?
@@ -247,11 +352,21 @@ def agreements_accepted?
247352
end
248353

249354
def create_agreement_participants(person)
355+
unless person&.persisted?
356+
Rails.logger.error 'Cannot create agreement participants - person not persisted'
357+
return
358+
end
359+
250360
identifiers = %w[privacy_policy terms_of_service]
251361
identifiers << 'code_of_conduct' if BetterTogether::Agreement.exists?(identifier: 'code_of_conduct')
252362
agreements = BetterTogether::Agreement.where(identifier: identifiers)
363+
253364
agreements.find_each do |agreement|
254-
BetterTogether::AgreementParticipant.create!(agreement: agreement, person: person, accepted_at: Time.current)
365+
BetterTogether::AgreementParticipant.create!(
366+
agreement: agreement,
367+
person: person,
368+
accepted_at: Time.current
369+
)
255370
end
256371
end
257372
end

app/models/better_together/identification.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ class Identification < ApplicationRecord
1010
polymorphic: true,
1111
autosave: true
1212

13+
accepts_nested_attributes_for :identity
14+
1315
validates :identity,
1416
presence: true
1517
validates :agent,

app/models/better_together/user.rb

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,31 +33,40 @@ class User < ApplicationRecord
3333
delegate :permitted_to?, to: :person, allow_nil: true
3434

3535
def build_person(attributes = {})
36-
build_person_identification(
36+
identification = build_person_identification(
3737
agent: self,
38-
identity: ::BetterTogether::Person.new(attributes)
39-
)&.identity
38+
identity_type: 'BetterTogether::Person',
39+
active: true
40+
)
41+
person = ::BetterTogether::Person.new(attributes)
42+
identification.identity = person
43+
person
4044
end
4145

42-
# Define person_attributes method to get attributes of associated Person
46+
# Override person method to ensure it builds if needed
4347
def person
44-
# Check if a Person object exists and return its attributes
45-
super.present? ? super : build_person # this build_person call can cause issues in registration
48+
person_identification&.identity
4649
end
4750

48-
# def person= arg
49-
# build_person_identification(
50-
# agent: self,
51-
# identity: arg
52-
# )&.identity
53-
# end
51+
# Custom person= method
52+
def person=(person_obj)
53+
if person_identification
54+
person_identification.identity = person_obj
55+
else
56+
build_person_identification(
57+
agent: self,
58+
identity: person_obj,
59+
identity_type: 'BetterTogether::Person',
60+
active: true
61+
)
62+
end
63+
end
5464

55-
# Define person_attributes= method
65+
# Define person_attributes= method for nested attributes
5666
def person_attributes=(attributes)
57-
# Check if a Person object already exists
58-
if person.present?
67+
if person_identification&.identity
5968
# Update existing Person object
60-
person.update(attributes)
69+
person_identification.identity.assign_attributes(attributes)
6170
else
6271
# Build new Person object if it doesn't exist
6372
build_person(attributes)

0 commit comments

Comments
 (0)