FEATURE: Add option to hide full name input at signup (#30471)

This commit replaces the `full_name_required` setting with a new `full_name_requirement` setting to allow more flexibility with the name field in the signup form. The new setting has 2 options, "Required at signup" and "Optional at signup", which are equivalent to the true/false possibilities of the old setting, and a third option "Hidden at signup" that hides the name field from the signup form, making it effectively optional too.

New sites will have the "Hidden at signup" option as the default option, and existing site will continue to use the option that maps to their current configuration.

Internal topic: t/136746.
This commit is contained in:
Osama Sayegh 2024-12-30 22:26:20 +03:00 committed by GitHub
parent b728b74c49
commit 3187606d34
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 217 additions and 42 deletions

View File

@ -6,15 +6,9 @@ import TextField from "discourse/components/text-field";
import valueEntered from "discourse/helpers/value-entered"; import valueEntered from "discourse/helpers/value-entered";
import { i18n } from "discourse-i18n"; import { i18n } from "discourse-i18n";
export default class SidebarEditNavigationMenuTagsModal extends Component { export default class FullnameInput extends Component {
@service siteSettings; @service siteSettings;
get showFullname() {
return (
this.siteSettings.full_name_required || this.siteSettings.enable_names
);
}
get showFullnameInstructions() { get showFullnameInstructions() {
return ( return (
this.siteSettings.show_signup_form_full_name_instructions && this.siteSettings.show_signup_form_full_name_instructions &&

View File

@ -99,7 +99,7 @@
{{/if}} {{/if}}
</div> </div>
{{#if this.fullnameRequired}} {{#if (and this.showFullname this.fullnameRequired)}}
<FullnameInput <FullnameInput
@nameValidation={{this.nameValidation}} @nameValidation={{this.nameValidation}}
@nameTitle={{this.nameTitle}} @nameTitle={{this.nameTitle}}

View File

@ -134,12 +134,14 @@ export default class CreateAccount extends Component.extend(
@discourseComputed @discourseComputed
showFullname() { showFullname() {
return this.siteSettings.enable_names; return (
this.siteSettings.enable_names && this.site.full_name_visible_in_signup
);
} }
@discourseComputed @discourseComputed
fullnameRequired() { fullnameRequired() {
return this.siteSettings.full_name_required; return this.site.full_name_required_for_signup;
} }
@discourseComputed( @discourseComputed(

View File

@ -157,12 +157,14 @@ export default class InvitesShowController extends Controller.extend(
@discourseComputed @discourseComputed
showFullname() { showFullname() {
return this.siteSettings.enable_names; return (
this.siteSettings.enable_names && this.site.full_name_visible_in_signup
);
} }
@discourseComputed @discourseComputed
fullnameRequired() { fullnameRequired() {
return this.siteSettings.full_name_required; return this.site.full_name_required_for_signup;
} }
@discourseComputed( @discourseComputed(

View File

@ -66,7 +66,7 @@ export default class AccountController extends Controller {
@discourseComputed() @discourseComputed()
nameInstructions() { nameInstructions() {
return i18n( return i18n(
this.siteSettings.full_name_required this.site.full_name_required_for_signup
? "user.name.instructions_required" ? "user.name.instructions_required"
: "user.name.instructions" : "user.name.instructions"
); );

View File

@ -117,12 +117,14 @@ export default class SignupPageController extends Controller.extend(
@discourseComputed @discourseComputed
showFullname() { showFullname() {
return this.siteSettings.enable_names; return (
this.siteSettings.enable_names && this.site.full_name_visible_in_signup
);
} }
@discourseComputed @discourseComputed
fullnameRequired() { fullnameRequired() {
return this.siteSettings.full_name_required; return this.site.full_name_required_for_signup;
} }
@discourseComputed( @discourseComputed(

View File

@ -6,7 +6,7 @@ import { i18n } from "discourse-i18n";
export default Mixin.create({ export default Mixin.create({
get nameTitle() { get nameTitle() {
return i18n( return i18n(
this.siteSettings.full_name_required this.site.full_name_required_for_signup
? "user.name.title" ? "user.name.title"
: "user.name.title_optional" : "user.name.title_optional"
); );
@ -15,7 +15,7 @@ export default Mixin.create({
// Validate the name. // Validate the name.
nameValidation: computed("accountName", "forceValidationReason", function () { nameValidation: computed("accountName", "forceValidationReason", function () {
const { accountName, forceValidationReason } = this; const { accountName, forceValidationReason } = this;
if (this.siteSettings.full_name_required && isEmpty(accountName)) { if (this.site.full_name_required_for_signup && isEmpty(accountName)) {
return EmberObject.create({ return EmberObject.create({
failed: true, failed: true,
ok: false, ok: false,

View File

@ -109,7 +109,7 @@
/> />
</div> </div>
{{#if this.fullnameRequired}} {{#if (and this.showFullname this.fullnameRequired)}}
<FullnameInput <FullnameInput
@nameValidation={{this.nameValidation}} @nameValidation={{this.nameValidation}}
@nameTitle={{this.nameTitle}} @nameTitle={{this.nameTitle}}

View File

@ -95,7 +95,7 @@
{{/if}} {{/if}}
</div> </div>
{{#if this.fullnameRequired}} {{#if (and this.showFullname this.fullnameRequired)}}
<FullnameInput <FullnameInput
@nameValidation={{this.nameValidation}} @nameValidation={{this.nameValidation}}
@nameTitle={{this.nameTitle}} @nameTitle={{this.nameTitle}}

View File

@ -2,6 +2,7 @@ import { click, fillIn, visit } from "@ember/test-helpers";
import { test } from "qunit"; import { test } from "qunit";
import sinon from "sinon"; import sinon from "sinon";
import LoginMethod from "discourse/models/login-method"; import LoginMethod from "discourse/models/login-method";
import Site from "discourse/models/site";
import pretender, { import pretender, {
parsePostData, parsePostData,
response, response,
@ -135,10 +136,12 @@ acceptance("Create Account", function () {
}); });
}); });
acceptance("Create Account - full_name_required", function (needs) { acceptance("Create Account - full name requirement", function () {
needs.settings({ full_name_required: true }); test("full name required", async function (assert) {
const site = Site.current();
site.set("full_name_required_for_signup", true);
site.set("full_name_visible_in_signup", true);
test("full_name_required", async function (assert) {
await visit("/"); await visit("/");
await click("header .sign-up-button"); await click("header .sign-up-button");
@ -168,4 +171,66 @@ acceptance("Create Account - full_name_required", function (needs) {
assert.verifySteps(["request"]); assert.verifySteps(["request"]);
}); });
test("full name hidden at signup", async function (assert) {
const site = Site.current();
site.set("full_name_required_for_signup", false);
site.set("full_name_visible_in_signup", false);
await visit("/");
await click("header .sign-up-button");
assert.dom("#new-account-name").doesNotExist();
await fillIn("#new-account-email", "z@z.co");
await fillIn("#new-account-username", "good-tuna");
await fillIn("#new-account-password", "cool password bro");
pretender.post("/u", (request) => {
assert.step("request");
const data = parsePostData(request.requestBody);
assert.strictEqual(data.password, "cool password bro");
assert.strictEqual(data.email, "z@z.co");
assert.strictEqual(data.username, "good-tuna");
return response({ success: true });
});
await click(".d-modal__footer .btn-primary");
assert
.dom(".d-modal__footer .btn-primary")
.isDisabled("create account is disabled");
assert.verifySteps(["request"]);
});
test("full name optional at signup", async function (assert) {
const site = Site.current();
site.set("full_name_required_for_signup", false);
site.set("full_name_visible_in_signup", true);
await visit("/");
await click("header .sign-up-button");
assert.dom("#new-account-name").exists();
await fillIn("#new-account-email", "z@z.co");
await fillIn("#new-account-username", "good-tuna");
await fillIn("#new-account-password", "cool password bro");
pretender.post("/u", (request) => {
assert.step("request");
const data = parsePostData(request.requestBody);
assert.strictEqual(data.password, "cool password bro");
assert.strictEqual(data.email, "z@z.co");
assert.strictEqual(data.username, "good-tuna");
return response({ success: true });
});
await click(".d-modal__footer .btn-primary");
assert
.dom(".d-modal__footer .btn-primary")
.isDisabled("create account is disabled");
assert.verifySteps(["request"]);
});
}); });

View File

@ -1,6 +1,7 @@
import { click, currentURL, fillIn, visit } from "@ember/test-helpers"; import { click, currentURL, fillIn, visit } from "@ember/test-helpers";
import { test } from "qunit"; import { test } from "qunit";
import PreloadStore from "discourse/lib/preload-store"; import PreloadStore from "discourse/lib/preload-store";
import Site from "discourse/models/site";
import pretender, { response } from "discourse/tests/helpers/create-pretender"; import pretender, { response } from "discourse/tests/helpers/create-pretender";
import { acceptance } from "discourse/tests/helpers/qunit-helpers"; import { acceptance } from "discourse/tests/helpers/qunit-helpers";
import { i18n } from "discourse-i18n"; import { i18n } from "discourse-i18n";
@ -50,9 +51,7 @@ function preloadInvite({
PreloadStore.store("invite_info", info); PreloadStore.store("invite_info", info);
} }
acceptance("Invite accept", function (needs) { acceptance("Invite accept", function () {
needs.settings({ full_name_required: true });
test("email invite link", async function (assert) { test("email invite link", async function (assert) {
PreloadStore.store("invite_info", { PreloadStore.store("invite_info", {
invited_by: { invited_by: {
@ -164,12 +163,38 @@ acceptance("Invite accept", function (needs) {
assert.dom(".invites-show .btn-primary").isEnabled("submit is enabled"); assert.dom(".invites-show .btn-primary").isEnabled("submit is enabled");
}); });
test("invite name is required only if full name is required", async function (assert) { test("invite name optional", async function (assert) {
const site = Site.current();
site.set("full_name_required_for_signup", false);
site.set("full_name_visible_in_signup", true);
preloadInvite(); preloadInvite();
await visit("/invites/my-valid-invite-token"); await visit("/invites/my-valid-invite-token");
assert.dom("#new-account-name").exists();
assert assert
.dom(".name-input .required") .dom(".name-input.name-required")
.doesNotExist("Full name is implicitly required"); .doesNotExist("full name is not required");
});
test("invite name hidden", async function (assert) {
const site = Site.current();
site.set("full_name_required_for_signup", false);
site.set("full_name_visible_in_signup", false);
preloadInvite();
await visit("/invites/my-valid-invite-token");
assert.dom("#new-account-name").doesNotExist();
});
test("invite name required", async function (assert) {
const site = Site.current();
site.set("full_name_required_for_signup", true);
site.set("full_name_visible_in_signup", true);
preloadInvite();
await visit("/invites/my-valid-invite-token");
assert.dom("#new-account-name").exists();
assert.dom(".name-input.name-required").exists("full name is required");
}); });
}); });

View File

@ -60,6 +60,8 @@ export default {
anonymous_top_menu_items: ["latest", "hot", "categories"], anonymous_top_menu_items: ["latest", "hot", "categories"],
uncategorized_category_id: 17, uncategorized_category_id: 17,
is_readonly: false, is_readonly: false,
full_name_required_for_signup: false,
full_name_visible_in_signup: true,
categories: [ categories: [
{ {
id: 3, id: 3,

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
require "enum_site_setting"
class FullNameRequirement < EnumSiteSetting
def self.valid_value?(val)
values.any? { |v| v[:value] == val }
end
def self.values
@values ||= [
{ name: "full_name_requirement.required_at_signup", value: "required_at_signup" },
{ name: "full_name_requirement.optional_at_signup", value: "optional_at_signup" },
{ name: "full_name_requirement.hidden_at_signup", value: "hidden_at_signup" },
]
end
def self.translate_names?
true
end
end

View File

@ -48,6 +48,8 @@ class SiteSerializer < ApplicationSerializer
:system_user_avatar_template, :system_user_avatar_template,
:lazy_load_categories, :lazy_load_categories,
:valid_flag_applies_to_types, :valid_flag_applies_to_types,
:full_name_required_for_signup,
:full_name_visible_in_signup,
) )
has_many :archetypes, embed: :objects, serializer: ArchetypeSerializer has_many :archetypes, embed: :objects, serializer: ArchetypeSerializer
@ -381,6 +383,14 @@ class SiteSerializer < ApplicationSerializer
scope.is_admin? scope.is_admin?
end end
def full_name_required_for_signup
SiteSetting.full_name_requirement == "required_at_signup"
end
def full_name_visible_in_signup
SiteSetting.full_name_requirement != "hidden_at_signup"
end
private private
def ordered_flags(flags) def ordered_flags(flags)

View File

@ -29,7 +29,7 @@ class UserAnonymizer
@user.reload @user.reload
@user.password = SecureRandom.hex @user.password = SecureRandom.hex
@user.name = SiteSetting.full_name_required ? @user.username : nil @user.name = SiteSetting.full_name_requirement == "required_at_signup" ? @user.username : nil
@user.date_of_birth = nil @user.date_of_birth = nil
@user.title = nil @user.title = nil
@user.uploaded_avatar_id = nil @user.uploaded_avatar_id = nil

View File

@ -2539,6 +2539,10 @@ en:
categories_boxes: "Boxes with Subcategories" categories_boxes: "Boxes with Subcategories"
categories_boxes_with_topics: "Boxes with Featured Topics" categories_boxes_with_topics: "Boxes with Featured Topics"
subcategories_with_featured_topics: "Subcategories with Featured Topics" subcategories_with_featured_topics: "Subcategories with Featured Topics"
full_name_requirement:
required_at_signup: "Required at signup"
optional_at_signup: "Optional at signup"
hidden_at_signup: "Hidden at signup"
shortcut_modifier_key: shortcut_modifier_key:
shift: "Shift" shift: "Shift"

View File

@ -2517,7 +2517,7 @@ en:
svg_icon_subset: "Add additional FontAwesome icons that you would like to include in your assets. Use prefix 'fa-' for solid icons, 'far-' for regular icons and 'fab-' for brand icons." svg_icon_subset: "Add additional FontAwesome icons that you would like to include in your assets. Use prefix 'fa-' for solid icons, 'far-' for regular icons and 'fab-' for brand icons."
max_prints_per_hour_per_user: "Maximum number of /print page impressions (set to 0 to disable printing)" max_prints_per_hour_per_user: "Maximum number of /print page impressions (set to 0 to disable printing)"
full_name_required: "Full name is a required field of a user's profile." full_name_requirement: "Make the full name field a required, optional, or optional but hidden field in the signup form."
enable_names: "Show the user's full name on their profile, user card, and emails. Disable to hide full name everywhere." enable_names: "Show the user's full name on their profile, user card, and emails. Disable to hide full name everywhere."
display_name_on_posts: "Show a user's full name on their posts in addition to their @username." display_name_on_posts: "Show a user's full name on their posts in addition to their @username."
show_time_gap_days: "If two posts are made this many days apart, display the time gap in the topic." show_time_gap_days: "If two posts are made this many days apart, display the time gap in the topic."

View File

@ -707,9 +707,10 @@ users:
logout_redirect: logout_redirect:
client: true client: true
default: "" default: ""
full_name_required: full_name_requirement:
client: true type: enum
default: false default: hidden_at_signup
enum: "FullNameRequirement"
enable_names: enable_names:
client: true client: true
default: true default: true

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
class ChangeFullNameRequiredSetting < ActiveRecord::Migration[7.2]
def up
old_setting = DB.query_single(<<~SQL).first
SELECT value
FROM site_settings
WHERE name = 'full_name_required'
SQL
new_setting = nil
if old_setting
new_setting = old_setting == "t" ? "required_at_signup" : "optional_at_signup"
elsif Migration::Helpers.existing_site?
new_setting = "optional_at_signup"
end
DB.exec(<<~SQL)
DELETE FROM site_settings WHERE name = 'full_name_required'
SQL
DB.exec(<<~SQL, value: new_setting) if new_setting
INSERT INTO site_settings
(name, data_type, value, created_at, updated_at)
VALUES
('full_name_requirement', 7, :value, NOW(), NOW())
SQL
end
def down
raise ActiveRecord::IrreversibleMigration
end
end

View File

@ -116,7 +116,8 @@ module DiscourseDev
puts "Once site is running use https://localhost:9292/user/#{username}/become to access the account in development" puts "Once site is running use https://localhost:9292/user/#{username}/become to access the account in development"
end end
admin.name = ask("Full name: ") if SiteSetting.full_name_required admin.name = ask("Full name: ") if SiteSetting.full_name_requirement ==
"required_at_signup"
saved = admin.save saved = admin.save
if saved if saved

View File

@ -81,7 +81,8 @@ task "admin:create" => :environment do
admin.password = password admin.password = password
end end
admin.name = ask("Full name: ") if SiteSetting.full_name_required && admin.name.blank? admin.name = ask("Full name: ") if SiteSetting.full_name_requirement == "required_at_signup" &&
admin.name.blank?
# save/update user account # save/update user account
saved = admin.save saved = admin.save

View File

@ -2,6 +2,8 @@
class UserFullNameValidator < ActiveModel::EachValidator class UserFullNameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
record.errors.add(attribute, :blank) if SiteSetting.full_name_required && !record.name.present? if SiteSetting.full_name_requirement == "required_at_signup" && !record.name.present?
record.errors.add(attribute, :blank)
end
end end
end end

View File

@ -7,7 +7,7 @@ RSpec.describe UserFullNameValidator do
let(:record) { Fabricate.build(:user, name: @name) } let(:record) { Fabricate.build(:user, name: @name) }
context "when name is not required" do context "when name is not required" do
before { SiteSetting.full_name_required = false } before { SiteSetting.full_name_requirement = "optional_at_signup" }
it "allows no name" do it "allows no name" do
@name = nil @name = nil
@ -23,7 +23,7 @@ RSpec.describe UserFullNameValidator do
end end
context "when name is required" do context "when name is required" do
before { SiteSetting.full_name_required = true } before { SiteSetting.full_name_requirement = "required_at_signup" }
it "adds error for nil name" do it "adds error for nil name" do
@name = nil @name = nil

View File

@ -861,6 +861,12 @@
}, },
"navigation_menu_site_top_tags": { "navigation_menu_site_top_tags": {
"type": "array" "type": "array"
},
"full_name_required_for_signup": {
"type": "boolean"
},
"full_name_visible_in_signup": {
"type": "boolean"
} }
}, },
"required": [ "required": [
@ -893,6 +899,8 @@
"categories", "categories",
"archetypes", "archetypes",
"user_fields", "user_fields",
"auth_providers" "auth_providers",
"full_name_required_for_signup",
"full_name_visible_in_signup"
] ]
} }

View File

@ -68,7 +68,7 @@ RSpec.describe AnonymousShadowCreator do
end end
it "works even when names are required" do it "works even when names are required" do
SiteSetting.full_name_required = true SiteSetting.full_name_requirement = "required_at_signup"
expect { AnonymousShadowCreator.get(user) }.to_not raise_error expect { AnonymousShadowCreator.get(user) }.to_not raise_error
end end

View File

@ -76,7 +76,7 @@ RSpec.describe UserAnonymizer do
end end
context "when Site Settings do not require full name" do context "when Site Settings do not require full name" do
before { SiteSetting.full_name_required = false } before { SiteSetting.full_name_requirement = "optional_at_signup" }
it "resets profile to default values" do it "resets profile to default values" do
user.update!(name: "Bibi", date_of_birth: 19.years.ago, title: "Super Star") user.update!(name: "Bibi", date_of_birth: 19.years.ago, title: "Super Star")
@ -127,7 +127,7 @@ RSpec.describe UserAnonymizer do
end end
context "when Site Settings require full name" do context "when Site Settings require full name" do
before { SiteSetting.full_name_required = true } before { SiteSetting.full_name_requirement = "required_at_signup" }
it "changes name to anonymized username" do it "changes name to anonymized username" do
prev_username = user.username prev_username = user.username

View File

@ -335,6 +335,8 @@ shared_examples "social authentication scenarios" do |signup_page_object, login_
end end
describe "Social authentication", type: :system do describe "Social authentication", type: :system do
before { SiteSetting.full_name_requirement = "optional_at_signup" }
context "when desktop" do context "when desktop" do
include_examples "social authentication scenarios", include_examples "social authentication scenarios",
PageObjects::Modals::Signup.new, PageObjects::Modals::Signup.new,