Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ group :test do
gem 'rubocop-rspec_rails'
# RSpec for unit testing
gem 'rspec'
gem 'rspec-rebound'
# RSpec Rails integration
gem 'rspec-rails'
# Selenium WebDriver for browser automation
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,8 @@ GEM
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-rebound (0.2.1)
rspec-core (~> 3.3)
rspec-support (3.13.4)
rswag (2.16.0)
rswag-api (= 2.16.0)
Expand Down Expand Up @@ -851,6 +853,7 @@ DEPENDENCIES
redis (~> 5.4)
rspec
rspec-rails
rspec-rebound
rswag
rubocop
rubocop-capybara
Expand Down
6 changes: 6 additions & 0 deletions app/controllers/better_together/settings_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

module BetterTogether
class SettingsController < ApplicationController
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,62 @@
module BetterTogether
module Users
# Override default Devise registrations controller
class RegistrationsController < ::Devise::RegistrationsController
class RegistrationsController < ::Devise::RegistrationsController # rubocop:todo Metrics/ClassLength
include DeviseLocales

skip_before_action :check_platform_privacy
before_action :set_required_agreements, only: %i[new create]
before_action :configure_account_update_params, only: [:update]

# PUT /resource
# We need to use a copy of the resource because we don't want to change
# the current user in place.
def update # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
self.resource = resource_class.to_adapter.get!(send(:"current_#{resource_name}").to_key)
prev_unconfirmed_email = resource.unconfirmed_email if resource.respond_to?(:unconfirmed_email)

resource_updated = update_resource(resource, account_update_params)
yield resource if block_given?
if resource_updated
set_flash_message_for_update(resource, prev_unconfirmed_email)
bypass_sign_in resource, scope: resource_name if sign_in_after_change_password?

respond_to do |format|
format.html { respond_with resource, location: after_update_path_for(resource) }
format.turbo_stream do
flash.now[:notice] = I18n.t('devise.registrations.updated')
render turbo_stream: [
turbo_stream.replace(
'flash_messages',
partial: 'layouts/better_together/flash_messages',
locals: { flash: }
),
turbo_stream.replace(
'account-settings',
partial: 'devise/registrations/edit_form'
)
]
end
end
else
clean_up_passwords resource
set_minimum_password_length

respond_to do |format|
format.html { respond_with resource, location: after_update_path_for(resource) }
format.turbo_stream do
render turbo_stream: [
turbo_stream.replace('form_errors', partial: 'layouts/better_together/errors',
locals: { object: resource }),
turbo_stream.replace(
'account-settings',
partial: 'devise/registrations/edit_form'
)
]
end
end
end
end

def new
super do |user|
Expand Down Expand Up @@ -62,6 +113,15 @@ def create # rubocop:todo Metrics/MethodLength, Metrics/AbcSize

protected

def account_update_params
devise_parameter_sanitizer.sanitize(:account_update)
end

def configure_account_update_params
devise_parameter_sanitizer.permit(:account_update,
keys: %i[email password password_confirmation current_password])
end

def set_required_agreements
@privacy_policy_agreement = BetterTogether::Agreement.find_by(identifier: 'privacy_policy')
@terms_of_service_agreement = BetterTogether::Agreement.find_by(identifier: 'terms_of_service')
Expand All @@ -84,6 +144,10 @@ def after_inactive_sign_up_path_for(resource)
super
end

def after_update_path_for(_resource)
better_together.edit_user_registration_path
end

def person_params
params.require(:user).require(:person_attributes).permit(%i[identifier name description])
end
Expand Down
10 changes: 6 additions & 4 deletions app/policies/better_together/user_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
module BetterTogether
class UserPolicy < ApplicationPolicy # rubocop:todo Style/Documentation
def index?
user.present?
permitted_to?('manage_platform')
end

def show?
user.present?
user.present? && (record == user || permitted_to?('manage_platform'))
end

def create?
Expand All @@ -19,7 +19,7 @@ def new?
end

def update?
false
permitted_to?('manage_platform')
end

def edit?
Expand All @@ -36,7 +36,9 @@ def me?

class Scope < Scope # rubocop:todo Style/Documentation
def resolve
scope.all
return scope.where(id: user.id) unless permitted_to?('manage_platform')

scope.order(created_at: :desc)
end
end
end
Expand Down
12 changes: 3 additions & 9 deletions app/views/better_together/people/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,15 @@
<!-- Details Tab -->
<div class="nav-tab-pane tab-pane fade show active" id="person-details" role="tabpanel" aria-labelledby="person-details-tab">
<div class="mb-3">
<%= required_label(form, :name, class: "form-label") %>
<%= render partial: 'better_together/shared/translated_string_field', locals: { model: person, form: form, attribute: 'name' } %>
<small class="form-text text-muted"><%= t('helpers.hint.person.name') %></small>
<%= render partial: 'better_together/shared/translated_string_field', locals: { model: person, form: form, attribute: 'name' } %>
</div>

<div class="mb-3">
<%= form.label :description, class: "form-label" %>
<%= render partial: 'better_together/shared/translated_rich_text_field', locals: { model: person, form: form, attribute: 'description_html' } %>
<small class="form-text text-muted"><%= t('helpers.hint.person.description') %></small>
<%= render partial: 'better_together/shared/translated_rich_text_field', locals: { model: person, form: form, attribute: 'description_html' } %>
</div>

<div class="mb-3">
<%= form.label :slug, class: "form-label" %>
<%= render partial: 'better_together/shared/translated_string_field', locals: { model: person, form: form, attribute: 'slug' } %>
<small class="form-text text-muted"><%= t('helpers.hint.person.slug') %></small>
<%= render partial: 'better_together/shared/translated_string_field', locals: { model: person, form: form, attribute: 'slug' } %>
</div>
</div>

Expand Down
124 changes: 124 additions & 0 deletions app/views/better_together/settings/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<!-- filepath: /home/rob/projects/better-together/community-engine-rails/app/views/better_together/settings/index.html.erb -->
<% content_for :page_title, t('.title') %>

<div class="container-fluid mt-4" data-controller="better_together--tabs">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2 mb-0">
<i class="fa-solid fa-cog me-2" aria-hidden="true"></i>
<%= t('.title') %>
</h1>
</div>
</div>
</div>

<div class="row">
<!-- Vertical Pills Navigation -->
<div class="col-md-3 mb-3">
<div class="nav flex-column nav-pills" id="settings-tabs" role="tablist" aria-orientation="vertical">
<!-- <button class="nav-link active" id="platform-tab" data-bs-toggle="pill" data-bs-target="#platform" type="button" role="tab" aria-controls="platform" aria-selected="true" data-better_together--tabs-target="tab">
<i class="fa-solid fa-server me-2" aria-hidden="true"></i>
<%= t('.tabs.platform') %>
</button> -->
<!-- <button class="nav-link" id="personal-tab" data-bs-toggle="pill" data-bs-target="#personal" type="button" role="tab" aria-controls="personal" aria-selected="false" data-better_together--tabs-target="tab">
<i class="fa-solid fa-user me-2" aria-hidden="true"></i>
<%= t('.tabs.personal') %>
</button> -->
<button class="nav-link" id="account-tab" data-bs-toggle="pill" data-bs-target="#account" type="button" role="tab" aria-controls="account" aria-selected="false" data-better_together--tabs-target="tab">
<i class="fa-solid fa-key me-2" aria-hidden="true"></i>
<%= t('.tabs.account') %>
</button>
<!-- <button class="nav-link" id="privacy-tab" data-bs-toggle="pill" data-bs-target="#privacy" type="button" role="tab" aria-controls="privacy" aria-selected="false" data-better_together--tabs-target="tab">
<i class="fa-solid fa-shield-alt me-2" aria-hidden="true"></i>
<%= t('.tabs.privacy') %>
</button> -->
</div>
</div>

<!-- Tab Content -->
<div class="col-md-9 tab-content" id="settings-tabs-content">
<!-- Platform Tab -->
<div class="nav-tab-pane tab-pane fade" id="platform" role="tabpanel" aria-labelledby="platform-tab">
<div class="card border-0 shadow-sm">
<div class="card-body">
<h3 class="card-title h5">
<i class="fa-solid fa-server me-2" aria-hidden="true"></i>
<%= t('.platform.title') %>
</h3>
<p class="text-muted mb-4"><%= t('.platform.description') %></p>

<!-- Platform settings content will go here -->
<div class="alert alert-info" role="status">
<i class="fa-solid fa-info-circle me-2" aria-hidden="true"></i>
<%= t('.platform.coming_soon') %>
</div>
</div>
</div>
</div>

<!-- Personal Tab -->
<div class="nav-tab-pane tab-pane fade" id="personal" role="tabpanel" aria-labelledby="personal-tab">
<div class="card border-0 shadow-sm">
<div class="card-body">
<h3 class="card-title h5">
<i class="fa-solid fa-user me-2" aria-hidden="true"></i>
<%= t('.personal.title') %>
</h3>
<p class="text-muted mb-4"><%= t('.personal.description') %></p>

<!-- Personal settings content will go here -->
<div class="alert alert-info" role="status">
<i class="fa-solid fa-info-circle me-2" aria-hidden="true"></i>
<%= t('.personal.coming_soon') %>
</div>
</div>
</div>
</div>

<!-- Account Tab -->
<div class="nav-tab-pane tab-pane fade show active" id="account" role="tabpanel" aria-labelledby="account-tab">
<div class="card border-0 shadow-sm">
<div class="card-body">
<h3 class="card-title h5">
<i class="fa-solid fa-key me-2" aria-hidden="true"></i>
<%= t('.account.title') %>
</h3>
<p class="text-muted mb-4"><%= t('.account.description') %></p>

<!-- Account settings content -->
<%= turbo_frame_tag "account-settings",
src: edit_user_registration_path,
loading: "lazy",
class: "d-block" do %>
<div class="d-flex justify-content-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<% end %>
</div>
</div>
</div>

<!-- Privacy Tab -->
<div class="nav-tab-pane tab-pane fade" id="privacy" role="tabpanel" aria-labelledby="privacy-tab">
<div class="card border-0 shadow-sm">
<div class="card-body">
<h3 class="card-title h5">
<i class="fa-solid fa-shield-alt me-2" aria-hidden="true"></i>
<%= t('.privacy.title') %>
</h3>
<p class="text-muted mb-4"><%= t('.privacy.description') %></p>

<!-- Privacy settings content will go here -->
<div class="alert alert-info" role="status">
<i class="fa-solid fa-info-circle me-2" aria-hidden="true"></i>
<%= t('.privacy.coming_soon') %>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
60 changes: 60 additions & 0 deletions app/views/devise/registrations/_edit_form.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<%= turbo_frame_tag "account-settings" do %>
<div class="container mt-0">
<div class="row">
<div class="col-12">
<%= form_for(resource, as: resource_name, url: registration_path(resource_name, format: :turbo_stream), html: { method: :put, class: 'needs-validation', novalidate: true, data: { turbo_frame: "account-settings", controller: "better_together--form-validation" } }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<div id="form_errors"></div>
<!-- Email Field -->
<div class="mb-3">
<%= f.label :email, class: 'form-label' %>
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: 'form-control' %>
</div>

<% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
<div class="alert alert-info">
<%= t('devise.registrations.edit.currently_waiting_confirmation_for_email', email: resource.unconfirmed_email) %>
</div>
<% end %>

<!-- Password Field -->
<div class="mb-3">
<%= f.label :password, class: 'form-label' %>
<i>(<%= t('devise.registrations.edit.leave_blank_if_you_don_t_want_to_change_it') %>)</i>
<%= f.password_field :password, autocomplete: "new-password", class: 'form-control' %>
<% if @minimum_password_length %>
<small class="form-text text-muted">
<%= t('devise.shared.minimum_password_length', count: @minimum_password_length) %>
</small>
<% end %>
</div>

<!-- Password Confirmation Field -->
<div class="mb-3">
<%= f.label :password_confirmation, class: 'form-label' %>
<%= f.password_field :password_confirmation, autocomplete: "new-password", class: 'form-control' %>
</div>

<!-- Current Password Field -->
<div class="mb-3">
<%= required_label f, :current_password, class: 'form-label' %>
<i>(<%= t('devise.registrations.edit.we_need_your_current_password_to_confirm_your_changes') %>)</i>
<%= f.password_field :current_password, autocomplete: "current-password", class: 'form-control', required: true %>
</div>

<!-- Update Button -->
<div class="text-center mb-4">
<%= f.submit t('devise.registrations.edit.update'), class: 'btn btn-primary' %>
</div>
<% end %>

<% if params[:cancel] %>
<h3><%= t('devise.registrations.edit.cancel_my_account') %></h3>
<div class="mb-3">
<%= t('devise.registrations.edit.unhappy') %> <%= button_to t('devise.registrations.edit.cancel_my_account'), registration_path(resource_name), data: { confirm: t('devise.registrations.edit.are_you_sure'), turbo_confirm: t('devise.registrations.edit.are_you_sure') }, method: :delete, class: 'btn btn-danger' %>
</div>
<% end %>
</div>
</div>
</div>
<% end %>
Loading
Loading