Fix account page forms & add account actions rspec
All checks were successful
CI / scan_ruby (push) Successful in 21s
CI / scan_js (push) Successful in 13s
CI / lint (push) Successful in 13s
CI / test (push) Successful in 37s

#5
This commit is contained in:
sto
2025-12-10 10:44:03 +01:00
parent cce090587a
commit 8cea403dc9
12 changed files with 131 additions and 47 deletions

View File

@@ -19,6 +19,7 @@ class UsersController < ApplicationController
def update def update
authorize @user authorize @user
@user.password_change_attempt = false
if @user.update(user_params) if @user.update(user_params)
redirect_to contests_path, notice: t("users.edit.notice") redirect_to contests_path, notice: t("users.edit.notice")
else else
@@ -26,6 +27,18 @@ class UsersController < ApplicationController
end end
end end
def change_password
@user = User.find(params[:user_id])
authorize @user
@user.password_change_attempt = true
if @user.update(user_password_params)
redirect_to contests_path, notice: t("users.edit.notice")
else
render :edit, status: :unprocessable_entity
end
end
def show def show
authorize @user authorize @user
@@ -89,6 +102,10 @@ class UsersController < ApplicationController
end end
def user_params def user_params
params.expect(user: [ :username, :email_address, :lang, :password ]) params.expect(user: [ :username, :email_address, :lang ])
end
def user_password_params
params.expect(user: [ :password ])
end end
end end

View File

@@ -2,14 +2,15 @@
# #
# Table name: users # Table name: users
# #
# id :integer not null, primary key # id :integer not null, primary key
# admin :boolean default(FALSE), not null # admin :boolean default(FALSE), not null
# email_address :string not null # email_address :string not null
# lang :string default("en") # lang :string default("en")
# password_digest :string not null # password_change_attempt :boolean
# username :string # password_digest :string not null
# created_at :datetime not null # username :string
# updated_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null
# #
# Indexes # Indexes
# #
@@ -25,4 +26,5 @@ class User < ApplicationRecord
validates :username, presence: true, uniqueness: true validates :username, presence: true, uniqueness: true
validates :email_address, presence: true, uniqueness: true validates :email_address, presence: true, uniqueness: true
validates :lang, inclusion: { in: Languages::AVAILABLE_LANGUAGES.map { |lang| lang[:id] } } validates :lang, inclusion: { in: Languages::AVAILABLE_LANGUAGES.map { |lang| lang[:id] } }
validates :password, presence: true, if: -> { password_change_attempt }
end end

View File

@@ -20,7 +20,11 @@ class UserPolicy < ApplicationPolicy
end end
def update? def update?
user.admin? || user.id == record.id edit?
end
def change_password?
edit?
end end
def destroy? def destroy?

View File

@@ -30,13 +30,13 @@
= form.label :password, class: "required" = form.label :password, class: "required"
= form.submit t("helpers.buttons.save"), class: "btn btn-primary" = form.submit t("helpers.buttons.save"), class: "btn btn-primary"
- if method == :patch - if method == :patch
h4.mt-5 = t("users.edit.password_section") h4.mt-5 = t("users.edit.password_section")
= form_with model: user, method: method do |form| = form_with model: user, url: user_password_path(user) do |form|
.row.mb-3 .row.mb-3
.col .col
.form-floating .form-floating
= form.password_field :password, autocomplete: "off", class: "form-control" = form.password_field :password, autocomplete: "off", class: "form-control"
= form.label :password, class: "required" = form.label :password, class: "required"
= form.submit t("helpers.buttons.save"), class: "btn btn-primary" = form.submit t("helpers.buttons.save_password"), class: "btn btn-primary"

View File

@@ -167,6 +167,9 @@ en:
blank: Your email cannot be empty blank: Your email cannot be empty
username: username:
blank: Your username cannot be empty blank: Your username cannot be empty
taken: This username is already taken
password:
blank: Your password cannot be empty
categories: categories:
destroy: destroy:
notice: Category deleted notice: Category deleted
@@ -290,6 +293,7 @@ en:
settings: Settings settings: Settings
sign_in: Sign in sign_in: Sign in
save: Save save: Save
save_password: Save password
start: Click here to start your participation start: Click here to start your participation
stopwatch_continue: Continue stopwatch_continue: Continue
stopwatch_pause: Pause stopwatch_pause: Pause
@@ -352,7 +356,7 @@ en:
notice: Settings updated notice: Settings updated
title: "My settings" title: "My settings"
general_section: "General settings" general_section: "General settings"
password_section: "Change password" password_section: "Password"
index: index:
title: "All users" title: "All users"
new: new:

View File

@@ -138,6 +138,9 @@ fr:
blank: L'email est obligatoire blank: L'email est obligatoire
username: username:
blank: Le nom d'utilisateur.ice est obligatoire blank: Le nom d'utilisateur.ice est obligatoire
taken: Ce nom d'utilisateur.ice est déjà utilisé
password:
blank: Le mot de passe ne peut pas être vide
categories: categories:
destroy: destroy:
notice: Catégorie supprimée notice: Catégorie supprimée
@@ -261,6 +264,7 @@ fr:
settings: Paramètres settings: Paramètres
sign_in: Se connecter sign_in: Se connecter
save: Modifier save: Modifier
save_password: Modifier le mot de passe
start: Clique ici pour démarrer ta participation start: Clique ici pour démarrer ta participation
stopwatch_continue: Reprendre stopwatch_continue: Reprendre
stopwatch_pause: Pause stopwatch_pause: Pause
@@ -323,7 +327,7 @@ fr:
notice: Paramètres modifiés notice: Paramètres modifiés
title: "Mes paramètres" title: "Mes paramètres"
general_section: "Paramètres globaux" general_section: "Paramètres globaux"
password_section: "Modifier mon mot de passe" password_section: "Mot de passe"
index: index:
title: "Tous.tes les utilisateur.ices" title: "Tous.tes les utilisateur.ices"
new: new:

View File

@@ -40,7 +40,9 @@ Rails.application.routes.draw do
end end
resources :passwords, param: :token resources :passwords, param: :token
resource :session resource :session
resources :users resources :users do
patch "password", to: "users#change_password"
end
options "connect", to: "messages#cors_preflight_check" options "connect", to: "messages#cors_preflight_check"
options "message", to: "messages#cors_preflight_check" options "message", to: "messages#cors_preflight_check"

View File

@@ -0,0 +1,5 @@
class AddPasswordChangeAttemptToUser < ActiveRecord::Migration[8.0]
def change
add_column :users, :password_change_attempt, :boolean
end
end

3
db/schema.rb generated
View File

@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_12_09_081941) do ActiveRecord::Schema[8.0].define(version: 2025_12_10_092658) do
create_table "active_storage_attachments", force: :cascade do |t| create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "record_type", null: false t.string "record_type", null: false
@@ -185,6 +185,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_09_081941) do
t.string "username" t.string "username"
t.boolean "admin", default: false, null: false t.boolean "admin", default: false, null: false
t.string "lang", default: "en" t.string "lang", default: "en"
t.boolean "password_change_attempt"
t.index ["email_address"], name: "index_users_on_email_address", unique: true t.index ["email_address"], name: "index_users_on_email_address", unique: true
end end

View File

@@ -2,14 +2,15 @@
# #
# Table name: users # Table name: users
# #
# id :integer not null, primary key # id :integer not null, primary key
# admin :boolean default(FALSE), not null # admin :boolean default(FALSE), not null
# email_address :string not null # email_address :string not null
# lang :string default("en") # lang :string default("en")
# password_digest :string not null # password_change_attempt :boolean
# username :string # password_digest :string not null
# created_at :datetime not null # username :string
# updated_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null
# #
# Indexes # Indexes
# #

View File

@@ -21,21 +21,64 @@ RSpec.feature "Users", type: :feature do
expect(page).not_to have_content(I18n.t("users.index.title")) expect(page).not_to have_content(I18n.t("users.index.title"))
end end
it "should be able to create a new contest" do it "should be able to open their account info" do
visit root_path visit root_path
click_link "Create a new contest" click_link I18n.t("nav.settings")
expect(page).to have_content(I18n.t("contests.new.title")) expect(page).to have_current_path(edit_user_path(user))
end end
it "should be able to open an existing contest" do context "when updating their account info" do
visit root_path let!(:existing_user) { create(:user, username: "taken_username") }
expect(page).to have_content(contest.name) it "should allow changing to an untaken username" do
find("div.card", text: contest.name).find("a").click visit edit_user_path(user)
expect(page).to have_content(I18n.t("contests.show.title", name: contest.name)) fill_in I18n.t("activerecord.attributes.user.username"), with: "untaken_username"
expect { click_button(I18n.t("helpers.buttons.save")); user.reload }.to change(user, :username).to("untaken_username")
end
it "should prevent changing to an already taken username" do
visit edit_user_path(user)
fill_in I18n.t("activerecord.attributes.user.username"), with: "taken_username"
expect { click_button(I18n.t("helpers.buttons.save")); user.reload }.not_to change(user, :username)
expect(page).to have_content(I18n.t("activerecord.errors.models.user.attributes.username.taken"))
end
it "should prevent changing to a blank username" do
visit edit_user_path(user)
fill_in I18n.t("activerecord.attributes.user.username"), with: ""
expect { click_button(I18n.t("helpers.buttons.save")); user.reload }.not_to change(user, :username)
expect(page).to have_content(I18n.t("activerecord.errors.models.user.attributes.username.blank"))
end
it "should allow changing to a non-blank password" do
visit edit_user_path(user)
fill_in I18n.t("activerecord.attributes.user.password"), with: "new_password"
expect { click_button(I18n.t("helpers.buttons.save_password")); user.reload }
.to change(user, :password_digest)
.and change { user.authenticate("new_password") }.from(false).to(user)
end
it "should prevent changing to a blank password" do
visit edit_user_path(user)
fill_in I18n.t("activerecord.attributes.user.password"), with: ""
expect { click_button(I18n.t("helpers.buttons.save_password")); user.reload }.not_to change(user, :password)
expect(page).to have_content(I18n.t("activerecord.errors.models.user.attributes.password.blank"))
end
end end
end end

View File

@@ -2,14 +2,15 @@
# #
# Table name: users # Table name: users
# #
# id :integer not null, primary key # id :integer not null, primary key
# admin :boolean default(FALSE), not null # admin :boolean default(FALSE), not null
# email_address :string not null # email_address :string not null
# lang :string default("en") # lang :string default("en")
# password_digest :string not null # password_change_attempt :boolean
# username :string # password_digest :string not null
# created_at :datetime not null # username :string
# updated_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null
# #
# Indexes # Indexes
# #