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 { i18n } from "discourse-i18n";
export default class SidebarEditNavigationMenuTagsModal extends Component {
export default class FullnameInput extends Component {
@service siteSettings;
get showFullname() {
return (
this.siteSettings.full_name_required || this.siteSettings.enable_names
);
}
get showFullnameInstructions() {
return (
this.siteSettings.show_signup_form_full_name_instructions &&

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import { click, fillIn, visit } from "@ember/test-helpers";
import { test } from "qunit";
import sinon from "sinon";
import LoginMethod from "discourse/models/login-method";
import Site from "discourse/models/site";
import pretender, {
parsePostData,
response,
@ -135,10 +136,12 @@ acceptance("Create Account", function () {
});
});
acceptance("Create Account - full_name_required", function (needs) {
needs.settings({ full_name_required: true });
acceptance("Create Account - full name requirement", function () {
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 click("header .sign-up-button");
@ -168,4 +171,66 @@ acceptance("Create Account - full_name_required", function (needs) {
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 { test } from "qunit";
import PreloadStore from "discourse/lib/preload-store";
import Site from "discourse/models/site";
import pretender, { response } from "discourse/tests/helpers/create-pretender";
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
import { i18n } from "discourse-i18n";
@ -50,9 +51,7 @@ function preloadInvite({
PreloadStore.store("invite_info", info);
}
acceptance("Invite accept", function (needs) {
needs.settings({ full_name_required: true });
acceptance("Invite accept", function () {
test("email invite link", async function (assert) {
PreloadStore.store("invite_info", {
invited_by: {
@ -164,12 +163,38 @@ acceptance("Invite accept", function (needs) {
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();
await visit("/invites/my-valid-invite-token");
assert.dom("#new-account-name").exists();
assert
.dom(".name-input .required")
.doesNotExist("Full name is implicitly required");
.dom(".name-input.name-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"],
uncategorized_category_id: 17,
is_readonly: false,
full_name_required_for_signup: false,
full_name_visible_in_signup: true,
categories: [
{
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,
:lazy_load_categories,
:valid_flag_applies_to_types,
:full_name_required_for_signup,
:full_name_visible_in_signup,
)
has_many :archetypes, embed: :objects, serializer: ArchetypeSerializer
@ -381,6 +383,14 @@ class SiteSerializer < ApplicationSerializer
scope.is_admin?
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
def ordered_flags(flags)

View File

@ -29,7 +29,7 @@ class UserAnonymizer
@user.reload
@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.title = nil
@user.uploaded_avatar_id = nil

View File

@ -2539,6 +2539,10 @@ en:
categories_boxes: "Boxes with Subcategories"
categories_boxes_with_topics: "Boxes 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:
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."
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."
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."

View File

@ -707,9 +707,10 @@ users:
logout_redirect:
client: true
default: ""
full_name_required:
client: true
default: false
full_name_requirement:
type: enum
default: hidden_at_signup
enum: "FullNameRequirement"
enable_names:
client: 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"
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
if saved

View File

@ -81,7 +81,8 @@ task "admin:create" => :environment do
admin.password = password
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
saved = admin.save

View File

@ -2,6 +2,8 @@
class UserFullNameValidator < ActiveModel::EachValidator
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

View File

@ -7,7 +7,7 @@ RSpec.describe UserFullNameValidator do
let(:record) { Fabricate.build(:user, name: @name) }
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
@name = nil
@ -23,7 +23,7 @@ RSpec.describe UserFullNameValidator do
end
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
@name = nil

View File

@ -861,6 +861,12 @@
},
"navigation_menu_site_top_tags": {
"type": "array"
},
"full_name_required_for_signup": {
"type": "boolean"
},
"full_name_visible_in_signup": {
"type": "boolean"
}
},
"required": [
@ -893,6 +899,8 @@
"categories",
"archetypes",
"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
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
end

View File

@ -76,7 +76,7 @@ RSpec.describe UserAnonymizer do
end
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
user.update!(name: "Bibi", date_of_birth: 19.years.ago, title: "Super Star")
@ -127,7 +127,7 @@ RSpec.describe UserAnonymizer do
end
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
prev_username = user.username

View File

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