mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 11:44:49 +08:00
FEATURE: User fields required for existing users - Part 2 (#27172)
We want to allow admins to make new required fields apply to existing users. In order for this to work we need to have a way to make those users fill up the fields on their next page load. This is very similar to how adding a 2FA requirement post-fact works. Users will be redirected to a page where they can fill up the remaining required fields, and until they do that they won't be able to do anything else.
This commit is contained in:
parent
867b3822f3
commit
d63f1826fe
|
@ -34,17 +34,36 @@
|
|||
<label class="optional">
|
||||
<RadioButton
|
||||
@value="optional"
|
||||
@name="optional"
|
||||
@name="requirement"
|
||||
@selection={{this.buffered.requirement}}
|
||||
@onChange={{action "changeRequirementType"}}
|
||||
/>
|
||||
<span>{{i18n "admin.user_fields.requirement.optional.title"}}</span>
|
||||
</label>
|
||||
|
||||
<label class="for_all_users">
|
||||
<RadioButton
|
||||
@value="for_all_users"
|
||||
@name="requirement"
|
||||
@selection={{this.buffered.requirement}}
|
||||
@onChange={{action "changeRequirementType"}}
|
||||
/>
|
||||
<div class="label-text">
|
||||
<span>{{i18n
|
||||
"admin.user_fields.requirement.for_all_users.title"
|
||||
}}</span>
|
||||
<div class="description">{{i18n
|
||||
"admin.user_fields.requirement.for_all_users.description"
|
||||
}}</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="on_signup">
|
||||
<RadioButton
|
||||
@value="on_signup"
|
||||
@name="on_signup"
|
||||
@name="requirement"
|
||||
@selection={{this.buffered.requirement}}
|
||||
@onChange={{action "changeRequirementType"}}
|
||||
/>
|
||||
<div class="label-text">
|
||||
<span>{{i18n "admin.user_fields.requirement.on_signup.title"}}</span>
|
||||
|
@ -57,7 +76,11 @@
|
|||
|
||||
<AdminFormRow @label="admin.user_fields.preferences">
|
||||
<label>
|
||||
<Input @type="checkbox" @checked={{this.buffered.editable}} />
|
||||
<Input
|
||||
@type="checkbox"
|
||||
@checked={{this.buffered.editable}}
|
||||
disabled={{this.editableDisabled}}
|
||||
/>
|
||||
<span>{{i18n "admin.user_fields.editable.title"}}</span>
|
||||
</label>
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import { action } from "@ember/object";
|
|||
import { schedule } from "@ember/runloop";
|
||||
import { service } from "@ember/service";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { Promise } from "rsvp";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { i18n, propertyEqual } from "discourse/lib/computed";
|
||||
import { bufferedProperty } from "discourse/mixins/buffered-content";
|
||||
|
@ -12,6 +13,7 @@ import UserField from "admin/models/user-field";
|
|||
|
||||
export default Component.extend(bufferedProperty("userField"), {
|
||||
adminCustomUserFields: service(),
|
||||
dialog: service(),
|
||||
|
||||
tagName: "",
|
||||
isEditing: false,
|
||||
|
@ -64,8 +66,29 @@ export default Component.extend(bufferedProperty("userField"), {
|
|||
return ret.join(", ");
|
||||
},
|
||||
|
||||
@discourseComputed("buffered.requirement")
|
||||
editableDisabled(requirement) {
|
||||
return requirement === "for_all_users";
|
||||
},
|
||||
|
||||
@action
|
||||
save() {
|
||||
changeRequirementType(requirement) {
|
||||
this.buffered.set("requirement", requirement);
|
||||
this.buffered.set("editable", requirement === "for_all_users");
|
||||
},
|
||||
|
||||
async _confirmChanges() {
|
||||
return new Promise((resolve) => {
|
||||
this.dialog.yesNoConfirm({
|
||||
message: I18n.t("admin.user_fields.requirement.confirmation"),
|
||||
didCancel: () => resolve(false),
|
||||
didConfirm: () => resolve(true),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@action
|
||||
async save() {
|
||||
const attrs = this.buffered.getProperties(
|
||||
"name",
|
||||
"description",
|
||||
|
@ -79,6 +102,16 @@ export default Component.extend(bufferedProperty("userField"), {
|
|||
...this.adminCustomUserFields.additionalProperties
|
||||
);
|
||||
|
||||
let confirm = true;
|
||||
|
||||
if (attrs.requirement === "for_all_users") {
|
||||
confirm = await this._confirmChanges();
|
||||
}
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.userField
|
||||
.save(attrs)
|
||||
.then(() => {
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
<LinkTo @route="preferences" @model={{this.currentUser}}>
|
||||
{{d-icon "cog"}}
|
||||
<span class="item-label">
|
||||
{{i18n "user.preferences"}}
|
||||
{{i18n "user.preferences.title"}}
|
||||
</span>
|
||||
</LinkTo>
|
||||
</li>
|
||||
|
|
|
@ -74,7 +74,7 @@
|
|||
class="user-nav__preferences"
|
||||
>
|
||||
{{d-icon "cog"}}
|
||||
<span>{{i18n "user.preferences"}}</span>
|
||||
<span>{{i18n "user.preferences.title"}}</span>
|
||||
</DNavigationItem>
|
||||
{{/if}}
|
||||
{{#if (and @isMobileView @isStaff)}}
|
||||
|
|
|
@ -42,6 +42,13 @@ export default Controller.extend({
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.showEnforcedRequiredFieldsNotice) {
|
||||
return this._missingRequiredFields(
|
||||
this.site.user_fields,
|
||||
this.model.user_fields
|
||||
);
|
||||
}
|
||||
|
||||
// Staff can edit fields that are not `editable`
|
||||
if (!this.currentUser.staff) {
|
||||
siteUserFields = siteUserFields.filterBy("editable", true);
|
||||
|
@ -53,6 +60,11 @@ export default Controller.extend({
|
|||
});
|
||||
},
|
||||
|
||||
@discourseComputed("currentUser.needs_required_fields_check")
|
||||
showEnforcedRequiredFieldsNotice(needsRequiredFieldsCheck) {
|
||||
return needsRequiredFieldsCheck;
|
||||
},
|
||||
|
||||
@discourseComputed("model.user_option.default_calendar")
|
||||
canChangeDefaultCalendar(defaultCalendar) {
|
||||
return defaultCalendar !== "none_selected";
|
||||
|
@ -81,6 +93,16 @@ export default Controller.extend({
|
|||
document.querySelector(".feature-topic-on-profile-btn")?.focus();
|
||||
},
|
||||
|
||||
_missingRequiredFields(siteFields, userFields) {
|
||||
return siteFields
|
||||
.filter(
|
||||
(siteField) =>
|
||||
siteField.requirement === "for_all_users" &&
|
||||
isEmpty(userFields[siteField.id])
|
||||
)
|
||||
.map((field) => EmberObject.create({ field, value: "" }));
|
||||
},
|
||||
|
||||
actions: {
|
||||
clearFeaturedTopicFromProfile() {
|
||||
this.dialog.yesNoConfirm({
|
||||
|
@ -132,6 +154,7 @@ export default Controller.extend({
|
|||
.then(() => {
|
||||
model.set("bio_cooked");
|
||||
this.set("saved", true);
|
||||
this.currentUser.set("needs_required_fields_check", false);
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
})
|
||||
|
|
|
@ -1,7 +1,26 @@
|
|||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import RestrictedUserRoute from "discourse/routes/restricted-user";
|
||||
|
||||
export default class PreferencesProfile extends RestrictedUserRoute {
|
||||
@service currentUser;
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.set("model", model);
|
||||
}
|
||||
|
||||
@action
|
||||
willTransition(transition) {
|
||||
super.willTransition(...arguments);
|
||||
|
||||
if (
|
||||
this.currentUser?.needs_required_fields_check &&
|
||||
!transition?.to.name.startsWith("admin")
|
||||
) {
|
||||
transition.abort();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ export default class Preferences extends RestrictedUserRoute {
|
|||
let controller = this.controllerFor(this.router.currentRouteName);
|
||||
let subpageTitle = controller?.subpageTitle;
|
||||
return subpageTitle
|
||||
? `${subpageTitle} - ${I18n.t("user.preferences")}`
|
||||
: I18n.t("user.preferences");
|
||||
? `${subpageTitle} - ${I18n.t("user.preferences.title")}`
|
||||
: I18n.t("user.preferences.title");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
{{#if this.showEnforcedRequiredFieldsNotice}}
|
||||
<div class="alert alert-error">{{i18n
|
||||
"user.preferences.profile.enforced_required_fields"
|
||||
}}</div>
|
||||
{{/if}}
|
||||
|
||||
{{#unless this.showEnforcedRequiredFieldsNotice}}
|
||||
{{#if this.canChangeBio}}
|
||||
<div class="control-group pref-bio" data-setting-name="user-bio">
|
||||
<label class="control-label">{{i18n "user.bio"}}</label>
|
||||
|
@ -53,6 +60,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
|
||||
{{#each this.userFields as |uf|}}
|
||||
<div class="control-group" data-setting-name="user-user-fields">
|
||||
|
@ -61,6 +69,7 @@
|
|||
{{/each}}
|
||||
<div class="clearfix"></div>
|
||||
|
||||
{{#unless this.showEnforcedRequiredFieldsNotice}}
|
||||
{{#if this.siteSettings.allow_profile_backgrounds}}
|
||||
{{#if this.canUploadProfileHeader}}
|
||||
<div
|
||||
|
@ -83,7 +92,10 @@
|
|||
</div>
|
||||
{{/if}}
|
||||
{{#if this.canUploadUserCardBackground}}
|
||||
<div class="control-group pref-profile-bg" data-setting-name="user-card-bg">
|
||||
<div
|
||||
class="control-group pref-profile-bg"
|
||||
data-setting-name="user-card-bg"
|
||||
>
|
||||
<label class="control-label">{{i18n
|
||||
"user.change_card_background.title"
|
||||
}}</label>
|
||||
|
@ -183,6 +195,7 @@
|
|||
@outletArgs={{hash model=this.model}}
|
||||
/>
|
||||
</span>
|
||||
{{/unless}}
|
||||
|
||||
<SaveControls
|
||||
@model={{this.model}}
|
||||
|
|
|
@ -529,7 +529,7 @@ acceptance("User menu", function (needs) {
|
|||
);
|
||||
assert.strictEqual(
|
||||
preferencesLink.textContent.trim(),
|
||||
I18n.t("user.preferences"),
|
||||
I18n.t("user.preferences.title"),
|
||||
"preferences link has the right label"
|
||||
);
|
||||
assert.ok(
|
||||
|
|
|
@ -41,6 +41,7 @@ class ApplicationController < ActionController::Base
|
|||
before_action :authorize_mini_profiler
|
||||
before_action :redirect_to_login_if_required
|
||||
before_action :block_if_requires_login
|
||||
before_action :redirect_to_profile_if_required
|
||||
before_action :preload_json
|
||||
before_action :check_xhr
|
||||
after_action :add_readonly_header
|
||||
|
@ -907,6 +908,34 @@ class ApplicationController < ActionController::Base
|
|||
(!SiteSetting.enforce_second_factor_on_external_auth && secure_session["oauth"] == "true")
|
||||
end
|
||||
|
||||
def redirect_to_profile_if_required
|
||||
return if request.format.json?
|
||||
return if !current_user
|
||||
return if !current_user.needs_required_fields_check?
|
||||
|
||||
if current_user.populated_required_custom_fields?
|
||||
current_user.bump_required_fields_version
|
||||
return
|
||||
end
|
||||
|
||||
redirect_path = path("/u/#{current_user.encoded_username}/preferences/profile")
|
||||
second_factor_path = path("/u/#{current_user.encoded_username}/preferences/second-factor")
|
||||
allowed_paths = [redirect_path, second_factor_path, path("/admin")]
|
||||
if allowed_paths.none? { |p| request.fullpath.start_with?(p) }
|
||||
rate_limiter = RateLimiter.new(current_user, "redirect_to_required_fields_log", 1, 24.hours)
|
||||
|
||||
if rate_limiter.performed!(raise_error: false)
|
||||
UserHistory.create!(
|
||||
action: UserHistory.actions[:redirected_to_required_fields],
|
||||
acting_user_id: current_user.id,
|
||||
)
|
||||
end
|
||||
|
||||
redirect_to path(redirect_path)
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def build_not_found_page(opts = {})
|
||||
if SiteSetting.bootstrap_error_pages?
|
||||
preload_json
|
||||
|
|
|
@ -3,7 +3,10 @@
|
|||
class EmailController < ApplicationController
|
||||
layout "no_ember"
|
||||
|
||||
skip_before_action :check_xhr, :preload_json, :redirect_to_login_if_required
|
||||
skip_before_action :check_xhr,
|
||||
:preload_json,
|
||||
:redirect_to_login_if_required,
|
||||
:redirect_to_profile_if_required
|
||||
|
||||
def unsubscribe
|
||||
key = UnsubscribeKey.includes(:user).find_by(key: params[:key])
|
||||
|
|
|
@ -6,6 +6,7 @@ class ExtraLocalesController < ApplicationController
|
|||
skip_before_action :check_xhr,
|
||||
:preload_json,
|
||||
:redirect_to_login_if_required,
|
||||
:redirect_to_profile_if_required,
|
||||
:verify_authenticity_token
|
||||
|
||||
OVERRIDES_BUNDLE ||= "overrides"
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FinishInstallationController < ApplicationController
|
||||
skip_before_action :check_xhr, :preload_json, :redirect_to_login_if_required
|
||||
skip_before_action :check_xhr,
|
||||
:preload_json,
|
||||
:redirect_to_login_if_required,
|
||||
:redirect_to_profile_if_required
|
||||
layout "finish_installation"
|
||||
|
||||
before_action :ensure_no_admins, except: %w[confirm_email resend_email]
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class HighlightJsController < ApplicationController
|
||||
skip_before_action :preload_json,
|
||||
:redirect_to_login_if_required,
|
||||
:redirect_to_profile_if_required,
|
||||
:check_xhr,
|
||||
:verify_authenticity_token,
|
||||
only: [:show]
|
||||
|
|
|
@ -16,6 +16,7 @@ class InvitesController < ApplicationController
|
|||
skip_before_action :check_xhr, except: [:perform_accept_invitation]
|
||||
skip_before_action :preload_json, except: [:show]
|
||||
skip_before_action :redirect_to_login_if_required
|
||||
skip_before_action :redirect_to_profile_if_required
|
||||
|
||||
before_action :ensure_invites_allowed, only: %i[show perform_accept_invitation]
|
||||
before_action :ensure_new_registrations_allowed, only: %i[show perform_accept_invitation]
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
class MetadataController < ApplicationController
|
||||
layout false
|
||||
skip_before_action :preload_json, :check_xhr, :redirect_to_login_if_required
|
||||
skip_before_action :preload_json,
|
||||
:check_xhr,
|
||||
:redirect_to_login_if_required,
|
||||
:redirect_to_profile_if_required
|
||||
|
||||
def manifest
|
||||
expires_in 1.minutes
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
class OfflineController < ApplicationController
|
||||
layout false
|
||||
skip_before_action :preload_json, :check_xhr, :redirect_to_login_if_required
|
||||
skip_before_action :preload_json,
|
||||
:check_xhr,
|
||||
:redirect_to_login_if_required,
|
||||
:redirect_to_profile_if_required
|
||||
|
||||
def index
|
||||
render :offline, content_type: "text/html"
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class PageviewController < ApplicationController
|
||||
skip_before_action :check_xhr,
|
||||
:redirect_to_login_if_required,
|
||||
:redirect_to_profile_if_required,
|
||||
:preload_json,
|
||||
:verify_authenticity_token
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class PresenceController < ApplicationController
|
||||
skip_before_action :check_xhr
|
||||
skip_before_action :check_xhr, :redirect_to_profile_if_required
|
||||
before_action :ensure_logged_in, only: [:update]
|
||||
before_action :skip_persist_session
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ class PublishedPagesController < ApplicationController
|
|||
skip_before_action :preload_json
|
||||
skip_before_action :check_xhr, :verify_authenticity_token, only: [:show]
|
||||
before_action :ensure_publish_enabled
|
||||
before_action :redirect_to_login_if_required, except: [:show]
|
||||
before_action :redirect_to_login_if_required, :redirect_to_profile_if_required, except: [:show]
|
||||
|
||||
def show
|
||||
params.require(:slug)
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class QunitController < ApplicationController
|
||||
skip_before_action *%i[check_xhr preload_json redirect_to_login_if_required]
|
||||
skip_before_action *%i[
|
||||
check_xhr
|
||||
preload_json
|
||||
redirect_to_login_if_required
|
||||
redirect_to_profile_if_required
|
||||
]
|
||||
layout false
|
||||
|
||||
def theme
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
class RobotsTxtController < ApplicationController
|
||||
layout false
|
||||
skip_before_action :preload_json, :check_xhr, :redirect_to_login_if_required
|
||||
skip_before_action :preload_json,
|
||||
:check_xhr,
|
||||
:redirect_to_login_if_required,
|
||||
:redirect_to_profile_if_required
|
||||
|
||||
OVERRIDDEN_HEADER = "# This robots.txt file has been customized at /admin/customize/robots\n"
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ class SessionController < ApplicationController
|
|||
only: %i[create forgot_password passkey_challenge passkey_login]
|
||||
before_action :rate_limit_login, only: %i[create email_login]
|
||||
skip_before_action :redirect_to_login_if_required
|
||||
skip_before_action :redirect_to_profile_if_required
|
||||
skip_before_action :preload_json,
|
||||
:check_xhr,
|
||||
only: %i[sso sso_login sso_provider destroy one_time_password]
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
class SiteController < ApplicationController
|
||||
layout false
|
||||
skip_before_action :preload_json, :check_xhr
|
||||
skip_before_action :redirect_to_login_if_required, only: %w[basic_info statistics]
|
||||
skip_before_action :redirect_to_login_if_required,
|
||||
:redirect_to_profile_if_required,
|
||||
only: %w[basic_info statistics]
|
||||
|
||||
def site
|
||||
render json: Site.json_for(guardian)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class StaticController < ApplicationController
|
||||
skip_before_action :check_xhr, :redirect_to_login_if_required
|
||||
skip_before_action :check_xhr, :redirect_to_login_if_required, :redirect_to_profile_if_required
|
||||
skip_before_action :verify_authenticity_token,
|
||||
only: %i[cdn_asset enter favicon service_worker_asset]
|
||||
skip_before_action :preload_json, only: %i[cdn_asset enter favicon service_worker_asset]
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class StylesheetsController < ApplicationController
|
||||
skip_before_action :preload_json,
|
||||
:redirect_to_login_if_required,
|
||||
:redirect_to_profile_if_required,
|
||||
:check_xhr,
|
||||
:verify_authenticity_token,
|
||||
only: %i[show show_source_map color_scheme]
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class SvgSpriteController < ApplicationController
|
||||
skip_before_action :preload_json,
|
||||
:redirect_to_login_if_required,
|
||||
:redirect_to_profile_if_required,
|
||||
:check_xhr,
|
||||
:verify_authenticity_token,
|
||||
only: %i[show search svg_icon]
|
||||
|
|
|
@ -8,6 +8,7 @@ class ThemeJavascriptsController < ApplicationController
|
|||
:handle_theme,
|
||||
:preload_json,
|
||||
:redirect_to_login_if_required,
|
||||
:redirect_to_profile_if_required,
|
||||
:verify_authenticity_token,
|
||||
only: %i[show show_map show_tests],
|
||||
)
|
||||
|
|
|
@ -11,6 +11,7 @@ class UploadsController < ApplicationController
|
|||
skip_before_action :preload_json,
|
||||
:check_xhr,
|
||||
:redirect_to_login_if_required,
|
||||
:redirect_to_profile_if_required,
|
||||
only: %i[show show_short _show_secure_deprecated show_secure]
|
||||
protect_from_forgery except: :show
|
||||
|
||||
|
|
|
@ -4,7 +4,9 @@ class UserApiKeysController < ApplicationController
|
|||
layout "no_ember"
|
||||
|
||||
requires_login only: %i[create create_otp revoke undo_revoke]
|
||||
skip_before_action :redirect_to_login_if_required, only: %i[new otp]
|
||||
skip_before_action :redirect_to_login_if_required,
|
||||
:redirect_to_profile_if_required,
|
||||
only: %i[new otp]
|
||||
skip_before_action :check_xhr, :preload_json
|
||||
|
||||
AUTH_API_VERSION ||= 4
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class UserAvatarsController < ApplicationController
|
||||
skip_before_action :preload_json,
|
||||
:redirect_to_login_if_required,
|
||||
:redirect_to_profile_if_required,
|
||||
:check_xhr,
|
||||
:verify_authenticity_token,
|
||||
only: %i[show show_letter show_proxy_letter]
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Users::OmniauthCallbacksController < ApplicationController
|
||||
skip_before_action :redirect_to_login_if_required
|
||||
skip_before_action :redirect_to_login_if_required, :redirect_to_profile_if_required
|
||||
|
||||
layout "no_ember"
|
||||
|
||||
|
|
|
@ -86,6 +86,7 @@ class UsersController < ApplicationController
|
|||
# once that happens you can't log in with social
|
||||
skip_before_action :verify_authenticity_token, only: [:create]
|
||||
skip_before_action :redirect_to_login_if_required,
|
||||
:redirect_to_profile_if_required,
|
||||
only: %i[
|
||||
check_username
|
||||
check_email
|
||||
|
@ -102,6 +103,7 @@ class UsersController < ApplicationController
|
|||
admin_login
|
||||
confirm_admin
|
||||
]
|
||||
skip_before_action :redirect_to_profile_if_required, only: %i[show staff_info update]
|
||||
|
||||
after_action :add_noindex_header, only: %i[show my_redirect]
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ class UsersEmailController < ApplicationController
|
|||
skip_before_action :check_xhr, only: %i[show_confirm_old_email show_confirm_new_email]
|
||||
|
||||
skip_before_action :redirect_to_login_if_required,
|
||||
:redirect_to_profile_if_required,
|
||||
only: %i[
|
||||
show_confirm_old_email
|
||||
show_confirm_new_email
|
||||
|
|
|
@ -1843,6 +1843,21 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def populated_required_custom_fields?
|
||||
UserField
|
||||
.required
|
||||
.pluck(:id)
|
||||
.all? { |field_id| custom_fields["#{User::USER_FIELD_PREFIX}#{field_id}"].present? }
|
||||
end
|
||||
|
||||
def needs_required_fields_check?
|
||||
(required_fields_version || 0) < UserRequiredFieldsVersion.current
|
||||
end
|
||||
|
||||
def bump_required_fields_version
|
||||
update(required_fields_version: UserRequiredFieldsVersion.current)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def badge_grant
|
||||
|
@ -2225,6 +2240,7 @@ end
|
|||
# flair_group_id :integer
|
||||
# last_seen_reviewable_id :integer
|
||||
# password_algorithm :string(64)
|
||||
# required_fields_version :integer
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
|
|
@ -15,9 +15,12 @@ class UserField < ActiveRecord::Base
|
|||
accepts_nested_attributes_for :user_field_options
|
||||
|
||||
before_save :sanitize_description
|
||||
after_create :update_required_fields_version
|
||||
after_update :update_required_fields_version, if: -> { saved_change_to_requirement? }
|
||||
after_save :queue_index_search
|
||||
|
||||
scope :public_fields, -> { where(show_on_profile: true).or(where(show_on_user_card: true)) }
|
||||
scope :required, -> { not_optional }
|
||||
|
||||
enum :requirement, { optional: 0, for_all_users: 1, on_signup: 2 }.freeze
|
||||
enum :field_type_enum, { text: 0, confirm: 1, dropdown: 2, multiselect: 3 }.freeze
|
||||
|
@ -37,6 +40,13 @@ class UserField < ActiveRecord::Base
|
|||
|
||||
private
|
||||
|
||||
def update_required_fields_version
|
||||
return if !for_all_users?
|
||||
|
||||
UserRequiredFieldsVersion.create
|
||||
Discourse.request_refresh!
|
||||
end
|
||||
|
||||
def sanitize_description
|
||||
if description_changed?
|
||||
self.description = sanitize_field(self.description, additional_attributes: ["target"])
|
||||
|
|
|
@ -144,6 +144,8 @@ class UserHistory < ActiveRecord::Base
|
|||
create_watched_word_group: 105,
|
||||
update_watched_word_group: 106,
|
||||
delete_watched_word_group: 107,
|
||||
redirected_to_required_fields: 108,
|
||||
filled_in_required_fields: 109,
|
||||
)
|
||||
end
|
||||
|
||||
|
|
14
app/models/user_required_fields_version.rb
Normal file
14
app/models/user_required_fields_version.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UserRequiredFieldsVersion < ActiveRecord::Base
|
||||
def self.current = maximum(:id) || 0
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: user_required_fields_versions
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
|
@ -58,6 +58,7 @@ class CurrentUserSerializer < BasicUserSerializer
|
|||
:associated_account_ids,
|
||||
:top_category_ids,
|
||||
:groups,
|
||||
:needs_required_fields_check?,
|
||||
:second_factor_enabled,
|
||||
:ignored_users,
|
||||
:featured_topic,
|
||||
|
|
|
@ -266,6 +266,13 @@ class UserUpdater
|
|||
end
|
||||
end
|
||||
DiscourseEvent.trigger(:user_updated, user)
|
||||
|
||||
if attributes[:custom_fields].present? && user.needs_required_fields_check?
|
||||
UserHistory.create!(
|
||||
action: UserHistory.actions[:filled_in_required_fields],
|
||||
acting_user_id: user.id,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
saved
|
||||
|
|
|
@ -1197,7 +1197,10 @@ en:
|
|||
activity_stream: "Activity"
|
||||
read: "Read"
|
||||
read_help: "Recently read topics"
|
||||
preferences: "Preferences"
|
||||
preferences:
|
||||
title: "Preferences"
|
||||
profile:
|
||||
enforced_required_fields: "You are required to provide additional information before continuing to use this site."
|
||||
feature_topic_on_profile:
|
||||
open_search: "Select a New Topic"
|
||||
title: "Select a Topic"
|
||||
|
@ -6669,9 +6672,13 @@ en:
|
|||
title: "Field Requirement"
|
||||
optional:
|
||||
title: "Optional"
|
||||
for_all_users:
|
||||
title: "For all users"
|
||||
description: "When new users sign up, they must fill out this field. When existing users return to the site and this is a new required field for them, they will also be prompted to fill it out. To re-prompt all users, delete this custom field and re-create it."
|
||||
on_signup:
|
||||
title: "On signup"
|
||||
description: "When new users sign up, they must fill out this field. Existing users are unaffected."
|
||||
confirmation: "This will prompt existing users to fill in this field and will not allow them to do anything else on your site until the field is filled. Proceed?"
|
||||
editable:
|
||||
title: "Editable after signup"
|
||||
enabled: "editable"
|
||||
|
|
|
@ -2931,7 +2931,7 @@ en:
|
|||
email_too_long: "The email you provided is too long. Mailbox names must be no more than 254 characters, and domain names must be no more than 253 characters."
|
||||
wrong_invite_code: "The invite code you entered was incorrect."
|
||||
reserved_username: "That username is not allowed."
|
||||
missing_user_field: "You have not completed all the user fields"
|
||||
missing_user_field: "You have not completed all the required user fields"
|
||||
auth_complete: "Authentication is complete."
|
||||
click_to_continue: "Click here to continue."
|
||||
already_logged_in: "Sorry! This invitation is intended for new users, who do not already have an existing account."
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateUserRequiredFieldsVersion < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
create_table :user_required_fields_versions do |t|
|
||||
t.timestamps null: false
|
||||
end
|
||||
|
||||
add_column :users, :required_fields_version, :integer
|
||||
end
|
||||
end
|
|
@ -8,7 +8,9 @@ module Chat
|
|||
|
||||
WEBHOOK_MESSAGES_PER_MINUTE_LIMIT = 10
|
||||
|
||||
skip_before_action :verify_authenticity_token, :redirect_to_login_if_required
|
||||
skip_before_action :verify_authenticity_token,
|
||||
:redirect_to_login_if_required,
|
||||
:redirect_to_profile_if_required
|
||||
|
||||
before_action :validate_payload
|
||||
|
||||
|
|
|
@ -3552,4 +3552,45 @@ RSpec.describe User do
|
|||
expect(user.new_personal_messages_notifications_count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#populated_required_fields?" do
|
||||
let!(:required_field) { Fabricate(:user_field, name: "hairstyle") }
|
||||
let!(:optional_field) { Fabricate(:user_field, name: "haircolor", requirement: "optional") }
|
||||
|
||||
context "when all required fields are populated" do
|
||||
before { user.set_user_field(required_field.id, "bald") }
|
||||
|
||||
it { expect(user.populated_required_custom_fields?).to eq(true) }
|
||||
end
|
||||
|
||||
context "when some required fields are missing values" do
|
||||
it { expect(user.populated_required_custom_fields?).to eq(false) }
|
||||
end
|
||||
end
|
||||
|
||||
describe "#needs_required_fields_check?" do
|
||||
let!(:version) { UserRequiredFieldsVersion.create! }
|
||||
|
||||
context "when version number is up to date" do
|
||||
before { user.update(required_fields_version: version.id) }
|
||||
|
||||
it { expect(user.needs_required_fields_check?).to eq(false) }
|
||||
end
|
||||
|
||||
context "when version number is out of date" do
|
||||
before { user.update(required_fields_version: version.id - 1) }
|
||||
|
||||
it { expect(user.needs_required_fields_check?).to eq(true) }
|
||||
end
|
||||
end
|
||||
|
||||
describe "#bump_required_fields_version" do
|
||||
let!(:version) { UserRequiredFieldsVersion.create! }
|
||||
|
||||
it do
|
||||
expect { user.bump_required_fields_version }.to change { user.required_fields_version }.to(
|
||||
version.id,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -256,6 +256,45 @@ RSpec.describe ApplicationController do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#redirect_to_profile_if_required" do
|
||||
fab!(:user)
|
||||
|
||||
before { sign_in(user) }
|
||||
|
||||
context "when the user is missing required custom fields" do
|
||||
before do
|
||||
Fabricate(:user_field, requirement: "for_all_users")
|
||||
UserRequiredFieldsVersion.create!
|
||||
end
|
||||
|
||||
it "redirects the user to the profile preferences" do
|
||||
get "/hot"
|
||||
expect(response).to redirect_to("/u/#{user.username}/preferences/profile")
|
||||
end
|
||||
|
||||
it "only logs user history once per day" do
|
||||
expect do
|
||||
RateLimiter.enable
|
||||
get "/hot"
|
||||
get "/hot"
|
||||
end.to change { UserHistory.count }.by(1)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the user has filled up all required custom fields" do
|
||||
before do
|
||||
Fabricate(:user_field, requirement: "for_all_users")
|
||||
UserRequiredFieldsVersion.create!
|
||||
user.bump_required_fields_version
|
||||
end
|
||||
|
||||
it "redirects the user to the profile preferences" do
|
||||
get "/hot"
|
||||
expect(response).not_to redirect_to("/u/#{user.username}/preferences/profile")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "invalid request params" do
|
||||
before do
|
||||
@old_logger = Rails.logger
|
||||
|
|
|
@ -602,6 +602,7 @@ RSpec.describe UserUpdater do
|
|||
end
|
||||
end
|
||||
|
||||
context "when updating the name" do
|
||||
it "logs the action" do
|
||||
user = Fabricate(:user, name: "Billy Bob")
|
||||
|
||||
|
@ -631,12 +632,32 @@ RSpec.describe UserUpdater do
|
|||
|
||||
expect(UserHistory.last.action).to eq(UserHistory.actions[:change_name])
|
||||
|
||||
expect do UserUpdater.new(user, user).update(name: "") end.to change { UserHistory.count }.by(
|
||||
1,
|
||||
)
|
||||
expect do UserUpdater.new(user, user).update(name: "") end.to change {
|
||||
UserHistory.count
|
||||
}.by(1)
|
||||
|
||||
expect(UserHistory.last.action).to eq(UserHistory.actions[:change_name])
|
||||
end
|
||||
end
|
||||
|
||||
context "when updating required fields" do
|
||||
it "logs the action" do
|
||||
user = Fabricate(:user)
|
||||
Fabricate(:user_field, name: "favorite_pokemon", requirement: "for_all_users")
|
||||
|
||||
UserRequiredFieldsVersion.create!
|
||||
|
||||
expect do
|
||||
UserUpdater.new(user, user).update(custom_fields: { "favorite_pokemon" => "Mudkip" })
|
||||
end.to change { UserHistory.count }.by(1)
|
||||
|
||||
user.bump_required_fields_version
|
||||
|
||||
expect do
|
||||
UserUpdater.new(user, user).update(custom_fields: { "favorite_pokemon" => "Mudkip" })
|
||||
end.not_to change { UserHistory.count }
|
||||
end
|
||||
end
|
||||
|
||||
it "clears the homepage_id when the special 'custom' id is chosen" do
|
||||
UserUpdater.new(user, user).update(homepage_id: "-1")
|
||||
|
|
|
@ -25,4 +25,38 @@ describe "Admin User Fields", type: :system, js: true do
|
|||
|
||||
expect(user_fields_page).to have_text(/Description can't be blank/)
|
||||
end
|
||||
|
||||
it "makes sure new required fields are editable after signup" do
|
||||
user_fields_page.visit
|
||||
|
||||
page.find(".user-fields .btn-primary").click
|
||||
|
||||
form = page.find(".user-field")
|
||||
editable_label = I18n.t("admin_js.admin.user_fields.editable.title")
|
||||
|
||||
user_fields_page.choose_requirement("for_all_users")
|
||||
|
||||
expect(form).to have_field(editable_label, checked: true, disabled: true)
|
||||
|
||||
user_fields_page.choose_requirement("optional")
|
||||
|
||||
expect(form).to have_field(editable_label, checked: false, disabled: false)
|
||||
end
|
||||
|
||||
it "requires confirmation when applying required fields retroactively" do
|
||||
user_fields_page.visit
|
||||
|
||||
page.find(".user-fields .btn-primary").click
|
||||
|
||||
form = page.find(".user-field")
|
||||
|
||||
form.find(".user-field-name").fill_in(with: "Favourite Pokémon")
|
||||
form.find(".user-field-desc").fill_in(with: "Hint: It's Mudkip")
|
||||
|
||||
user_fields_page.choose_requirement("for_all_users")
|
||||
|
||||
form.find(".btn-primary").click
|
||||
|
||||
expect(page).to have_text(I18n.t("admin_js.admin.user_fields.requirement.confirmation"))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,6 +8,12 @@ module PageObjects
|
|||
self
|
||||
end
|
||||
|
||||
def choose_requirement(requirement)
|
||||
form = page.find(".user-field")
|
||||
|
||||
form.choose(I18n.t("admin_js.admin.user_fields.requirement.#{requirement}.title"))
|
||||
end
|
||||
|
||||
def add_field(name: nil, description: nil, requirement: nil, preferences: [])
|
||||
page.find(".user-fields .btn-primary").click
|
||||
|
||||
|
|
12
spec/system/page_objects/pages/user_preferences_profile.rb
Normal file
12
spec/system/page_objects/pages/user_preferences_profile.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module PageObjects
|
||||
module Pages
|
||||
class UserPreferencesProfile < PageObjects::Pages::Base
|
||||
def visit(user)
|
||||
page.visit("/u/#{user.username}/preferences/profile")
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
52
spec/system/user_page/user_preferences_profile_spec.rb
Normal file
52
spec/system/user_page/user_preferences_profile_spec.rb
Normal file
|
@ -0,0 +1,52 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
describe "User preferences | Profile", type: :system do
|
||||
fab!(:user) { Fabricate(:user, active: true) }
|
||||
let(:user_preferences_profile_page) { PageObjects::Pages::UserPreferencesProfile.new }
|
||||
let(:user_preferences_page) { PageObjects::Pages::UserPreferences.new }
|
||||
|
||||
before { sign_in(user) }
|
||||
|
||||
describe "enforcing required fields" do
|
||||
before do
|
||||
UserRequiredFieldsVersion.create!
|
||||
UserField.create!(
|
||||
field_type: "text",
|
||||
name: "Favourite Pokemon",
|
||||
description: "Hint: It's Mudkip.",
|
||||
requirement: :for_all_users,
|
||||
editable: true,
|
||||
)
|
||||
end
|
||||
|
||||
it "redirects to the profile page to fill up required fields" do
|
||||
visit("/")
|
||||
|
||||
expect(page).to have_current_path("/u/bruce0/preferences/profile")
|
||||
|
||||
expect(page).to have_selector(
|
||||
".alert-error",
|
||||
text: I18n.t("js.user.preferences.profile.enforced_required_fields"),
|
||||
)
|
||||
end
|
||||
|
||||
it "disables client-side routing while missing required fields" do
|
||||
user_preferences_profile_page.visit(user)
|
||||
|
||||
find("#site-logo").click
|
||||
|
||||
expect(page).to have_current_path("/u/bruce0/preferences/profile")
|
||||
end
|
||||
|
||||
it "allows user to fill up required fields" do
|
||||
user_preferences_profile_page.visit(user)
|
||||
|
||||
find(".user-field-favourite-pokemon input").fill_in(with: "Mudkip")
|
||||
find(".save-button .btn-primary").click
|
||||
|
||||
visit("/")
|
||||
|
||||
expect(page).to have_current_path("/")
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue
Block a user