Skip to content

Commit 54d1f87

Browse files
authored
Add Agreements and Agreement Terms (#884)
## Pull Request #884: Add Agreements and Agreement Terms ### Overview - Base branch: main - Feature branch: feature/agreements - Commits: 38 commits (from May to August 2025) - File changes: 238 files changed (+3644 / -2463) ### Key Highlights by File 1. app/builders/better_together/agreement_builder.rb - Introduces a seeding builder for default agreements: - Privacy Policy - Terms of Service - Code of Conduct - Sets default titles, descriptions, and privacy settings. - Includes linking to existing pages if available. 2. app/controllers/better_together/agreements_controller.rb - New AgreementsController (inherits from FriendlyResourceController). - Handles show action: - If @agreement.page exists, renders within the appropriate layout. - Supports Turbo Frame responses for modal display. 3. app/controllers/better_together/users/registrations_controller.rb - Adds flows for agreement acceptance during user signup: - set_required_agreements to load required Agreement records (privacy_policy, terms_of_service, and optionally code_of_conduct). - Validates checkbox acceptance in agreements_accepted?. - Creates AgreementParticipant records upon successful signup to track acceptance. 4. Styling & Layout - Adds CSS classes in app/assets/stylesheets/better_together/forms.scss for agreement modals and spacing (e.g., .agreement-modal-link, .bt-mb-3). 5. Helper Module - Adds an empty helper module BetterTogether::AgreementsHelper for future use. ### Summary This PR lays the groundwork for a structured agreements system: - Seeds default agreements for users to consent to during signup. - Renders agreements via modals using Turbo Frames for smooth UX. - Tracks agreement acceptance per user via AgreementParticipant records. ### Next Steps / Considerations - Review and refine acceptance flows and error messaging UX. - Confirm localization support and styling for agreement modals. - Ensure existing migrations (e.g., agreement_participants) are applied without conflict.
2 parents f5c013e + dbbbe00 commit 54d1f87

File tree

238 files changed

+3644
-2463
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

238 files changed

+3644
-2463
lines changed

.rubocop.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ AllCops:
55
- 'spec/dummy/db/schema.rb'
66
- 'vendor/**/*'
77
NewCops: enable
8+
TargetRubyVersion: 3.4
9+
plugins:
10+
- rubocop-rails
11+
- rubocop-rspec
12+
- rubocop-rspec_rails
13+
- rubocop-capybara
14+
- rubocop-factory_bot
815
Style/StringLiterals:
916
Exclude:
1017
- 'db/migrate/*'
18+
19+
Rails:
20+
Enabled: false
21+
Capybara:
22+
Enabled: false
23+
FactoryBot:
24+
Enabled: false

app/assets/stylesheets/better_together/forms.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
font-weight: 500;
1010
}
1111

12+
.agreement-modal-link {
13+
text-decoration: none;
14+
}
15+
1216
.bt-mb-3 {
1317
margin-bottom: 1rem;
1418
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# frozen_string_literal: true
2+
3+
# app/builders/better_together/agreement_builder.rb
4+
5+
module BetterTogether
6+
# Builder to seed initial agreements
7+
class AgreementBuilder < Builder
8+
class << self
9+
def seed_data
10+
build_privacy_policy
11+
build_terms_of_service
12+
build_code_of_conduct
13+
end
14+
15+
def clear_existing
16+
BetterTogether::AgreementParticipant.delete_all
17+
BetterTogether::AgreementTerm.delete_all
18+
BetterTogether::Agreement.delete_all
19+
end
20+
21+
# rubocop:todo Metrics/AbcSize
22+
def build_privacy_policy # rubocop:todo Metrics/MethodLength, Metrics/AbcSize
23+
agreement = BetterTogether::Agreement.find_or_create_by!(identifier: 'privacy_policy') do |a|
24+
a.protected = true
25+
a.title = 'Privacy Policy'
26+
a.description = 'Summary of how we handle your data.'
27+
a.privacy = 'public'
28+
end
29+
30+
agreement.agreement_terms.find_or_create_by!(identifier: 'privacy_policy_summary') do |term|
31+
term.protected = true
32+
term.position = 1
33+
term.content = 'We respect your privacy and protect your personal information.'
34+
end
35+
36+
# If a Page exists for the privacy policy, link it so the page content
37+
# is shown to users instead of the agreement terms.
38+
page = BetterTogether::Page.find_by(identifier: 'privacy_policy') ||
39+
BetterTogether::Page.find_by(slug: 'privacy-policy')
40+
agreement.update!(page: page) if page.present?
41+
end
42+
# rubocop:enable Metrics/AbcSize
43+
44+
# rubocop:todo Metrics/AbcSize
45+
def build_terms_of_service # rubocop:todo Metrics/MethodLength, Metrics/AbcSize
46+
agreement = BetterTogether::Agreement.find_or_create_by!(identifier: 'terms_of_service') do |a|
47+
a.protected = true
48+
a.title = 'Terms of Service'
49+
a.description = 'Rules you agree to when using the platform.'
50+
a.privacy = 'public'
51+
end
52+
53+
agreement.agreement_terms.find_or_create_by!(identifier: 'terms_of_service_summary') do |term|
54+
term.protected = true
55+
term.position = 1
56+
term.content = 'Use the platform responsibly and respectfully.'
57+
end
58+
59+
# Link a Terms of Service Page if one exists
60+
page = BetterTogether::Page.find_by(identifier: 'terms_of_service') ||
61+
BetterTogether::Page.find_by(slug: 'terms-of-service')
62+
agreement.update!(page: page) if page.present?
63+
end
64+
# rubocop:enable Metrics/AbcSize
65+
66+
# rubocop:todo Metrics/MethodLength
67+
def build_code_of_conduct # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
68+
agreement = BetterTogether::Agreement.find_or_create_by!(identifier: 'code_of_conduct') do |a|
69+
a.protected = true
70+
a.title = 'Code of Conduct'
71+
a.description = 'Community code of conduct and expectations.'
72+
a.privacy = 'public'
73+
end
74+
75+
agreement.agreement_terms.find_or_create_by!(identifier: 'code_of_conduct_summary') do |term|
76+
term.protected = true
77+
term.position = 1
78+
term.content = 'Be respectful, inclusive, and considerate to other community members.'
79+
end
80+
81+
# Link a Code of Conduct Page if one exists
82+
page = BetterTogether::Page.find_by(identifier: 'code_of_conduct') ||
83+
BetterTogether::Page.find_by(slug: 'code-of-conduct')
84+
agreement.update!(page: page) if page.present?
85+
end
86+
# rubocop:enable Metrics/MethodLength
87+
end
88+
end
89+
end
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# frozen_string_literal: true
2+
3+
module BetterTogether
4+
# CRUD for Agreements
5+
class AgreementsController < FriendlyResourceController
6+
skip_before_action :check_platform_privacy, only: :show
7+
8+
# When the agreement is requested inside a Turbo Frame (from the modal),
9+
# return only the fragment wrapped in the expected <turbo-frame id="agreement_modal_frame">...</turbo-frame>
10+
# so Turbo can swap it into the frame. For normal requests, fall back to the
11+
# default rendering (with layout).
12+
def show
13+
if @agreement.page
14+
@page = @agreement.page
15+
@content_blocks = @page.content_blocks
16+
@layout = 'layouts/better_together/page'
17+
@layout = @page.layout if @page.layout.present?
18+
end
19+
20+
return unless turbo_frame_request?
21+
22+
content = render_to_string(action: :show, layout: false)
23+
render html: "<turbo-frame id=\"agreement_modal_frame\">#{content}</turbo-frame>".html_safe
24+
end
25+
26+
protected
27+
28+
def resource_class
29+
::BetterTogether::Agreement
30+
end
31+
end
32+
end

app/controllers/better_together/users/registrations_controller.rb

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class RegistrationsController < ::Devise::RegistrationsController
77
include DeviseLocales
88

99
skip_before_action :check_platform_privacy
10+
before_action :set_required_agreements, only: %i[new create]
1011

1112
def new
1213
super do |user|
@@ -15,9 +16,15 @@ def new
1516
end
1617

1718
def create # rubocop:todo Metrics/MethodLength, Metrics/AbcSize
18-
ActiveRecord::Base.transaction do
19+
unless agreements_accepted?
20+
build_resource(sign_up_params)
21+
resource.errors.add(:base, I18n.t('devise.registrations.new.agreements_required'))
22+
respond_with resource
23+
return
24+
end
25+
26+
ActiveRecord::Base.transaction do # rubocop:todo Metrics/BlockLength
1927
super do |user|
20-
# byebug
2128
return unless user.persisted?
2229

2330
user.build_person(person_params)
@@ -46,13 +53,21 @@ def create # rubocop:todo Metrics/MethodLength, Metrics/AbcSize
4653

4754
@platform_invitation.accept!(invitee: user.person)
4855
end
56+
57+
create_agreement_participants(user.person)
4958
end
5059
end
5160
end
5261
end
5362

5463
protected
5564

65+
def set_required_agreements
66+
@privacy_policy_agreement = BetterTogether::Agreement.find_by(identifier: 'privacy_policy')
67+
@terms_of_service_agreement = BetterTogether::Agreement.find_by(identifier: 'terms_of_service')
68+
@code_of_conduct_agreement = BetterTogether::Agreement.find_by(identifier: 'code_of_conduct')
69+
end
70+
5671
def after_sign_up_path_for(resource)
5772
if is_navigational_format? && helpers.host_platform&.privacy_private?
5873
return better_together.new_user_session_path
@@ -72,6 +87,23 @@ def after_inactive_sign_up_path_for(resource)
7287
def person_params
7388
params.require(:user).require(:person_attributes).permit(%i[identifier name description])
7489
end
90+
91+
def agreements_accepted?
92+
required = [params[:privacy_policy_agreement], params[:terms_of_service_agreement]]
93+
# If a code of conduct agreement exists, require it as well
94+
required << params[:code_of_conduct_agreement] if @code_of_conduct_agreement.present?
95+
96+
required.all? { |v| v == '1' }
97+
end
98+
99+
def create_agreement_participants(person)
100+
identifiers = %w[privacy_policy terms_of_service]
101+
identifiers << 'code_of_conduct' if BetterTogether::Agreement.exists?(identifier: 'code_of_conduct')
102+
agreements = BetterTogether::Agreement.where(identifier: identifiers)
103+
agreements.find_each do |agreement|
104+
BetterTogether::AgreementParticipant.create!(agreement: agreement, person: person, accepted_at: Time.current)
105+
end
106+
end
75107
end
76108
end
77109
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
module BetterTogether
4+
# Helpers for Agreements
5+
module AgreementsHelper
6+
end
7+
end

0 commit comments

Comments
 (0)