mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
Primerize the administration user form. wp/72005
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
<%= @form.fields_for_custom_fields :custom_field_values, @form.object, field_options do |cf_form| %>
|
||||
<%= content_tag :div, class: css_classes do
|
||||
cf_form.cf_form_field(container_class:)
|
||||
end %>
|
||||
<% end %>
|
||||
@@ -1,11 +0,0 @@
|
||||
<fieldset class="form--fieldset">
|
||||
<legend class="form--fieldset-legend"><%= title %></legend>
|
||||
|
||||
<% @section.attribute_order.each do |key| %>
|
||||
<% if built_in?(key) %>
|
||||
<%= render "users/form/built_in_fields/#{key}", f: @form, contract: @contract, user: @user %>
|
||||
<% elsif (cf = visible_custom_field(key)) %>
|
||||
<%= render Users::Form::CustomFieldFieldComponent.new(custom_field: cf, form: @form) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</fieldset>
|
||||
@@ -0,0 +1,33 @@
|
||||
<% if editing? %>
|
||||
<%= render(Primer::Box.new(mb: 3)) do %>
|
||||
<%= render(Primer::Beta::Text.new(tag: :div, font_weight: :bold)) { I18n.t("attributes.status") } %>
|
||||
<%= render(Primer::Beta::Text.new(tag: :div)) { helpers.full_user_status(@user, true) } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= render(form_list) %>
|
||||
|
||||
<% if show_no_login_message? %>
|
||||
<%= render(Primer::Beta::Text.new(tag: :p, color: :muted)) { I18n.t("user.no_login") } %>
|
||||
<% end %>
|
||||
|
||||
<% if show_external_auth? %>
|
||||
<%= render("users/form/authentication/external", user: @user) %>
|
||||
<% end %>
|
||||
|
||||
<%= helpers.render_user_form_hooks(user: @user, form: @builder) %>
|
||||
|
||||
<% if show_consent? %>
|
||||
<%= render("users/consent", user: @user) %>
|
||||
<% end %>
|
||||
|
||||
<% if show_preferences? %>
|
||||
<%= fields_for(:pref, @user.pref, builder: @builder.class) do |pref_f| %>
|
||||
<%= render(Users::Form::PreferencesForm.new(pref_f)) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= render(Primer::Beta::Button.new(type: :submit, scheme: :primary, name: "submit")) { creating? ? I18n.t(:button_create) : I18n.t(:button_save) } %>
|
||||
<% if creating? %>
|
||||
<%= render(Primer::Beta::Button.new(type: :submit, name: "continue")) { I18n.t(:button_create_and_continue) } %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,95 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Users
|
||||
# Coordinates the administration user form: receives the surrounding
|
||||
# `settings_primer_form_with` builder, composes the inner forms into a
|
||||
# Primer::Forms::FormList and renders the read-only / plain bits (status,
|
||||
# consent, external auth, hooks, submit) and the pref-scoped preferences form
|
||||
# around it. Create vs edit is derived from `user.new_record?`.
|
||||
class FormComponent < ApplicationComponent
|
||||
def initialize(builder:, user:, contract:)
|
||||
super()
|
||||
@builder = builder
|
||||
@user = user
|
||||
@contract = contract
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def creating? = @user.new_record?
|
||||
def editing? = !creating?
|
||||
|
||||
def form_list
|
||||
Primer::Forms::FormList.new(*input_forms)
|
||||
end
|
||||
|
||||
def input_forms
|
||||
forms = [Users::Form::AttributesForm.new(@builder, user: @user, contract: @contract)]
|
||||
forms << Users::Form::AuthenticationSourceForm.new(@builder, user: @user) if show_auth_source?
|
||||
if show_password?
|
||||
forms << Users::Form::PasswordForm.new(@builder, user: @user,
|
||||
assign_random_password_checked: assign_random_password_checked?)
|
||||
end
|
||||
forms
|
||||
end
|
||||
|
||||
def show_auth_source?
|
||||
return false if editing? && @user.uses_external_authentication?
|
||||
|
||||
creating? ? can_users_have_auth_source? : (User.current.admin? || can_users_have_auth_source?)
|
||||
end
|
||||
|
||||
def show_password?
|
||||
editing? && User.current.admin? && !@user.uses_external_authentication? && !disable_password_login?
|
||||
end
|
||||
|
||||
def show_preferences? = editing? && User.current.admin?
|
||||
def show_external_auth? = editing? && @user.uses_external_authentication?
|
||||
|
||||
def show_no_login_message?
|
||||
editing? && User.current.admin? && !@user.uses_external_authentication? && disable_password_login?
|
||||
end
|
||||
|
||||
def show_consent? = editing? && Setting.consent_required?
|
||||
|
||||
def can_users_have_auth_source?
|
||||
LdapAuthSource.any? && !disable_password_login?
|
||||
end
|
||||
|
||||
def disable_password_login?
|
||||
OpenProject::Configuration.disable_password_login?
|
||||
end
|
||||
|
||||
def assign_random_password_checked?
|
||||
helpers.params.dig(:user, :assign_random_password).present?
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -52,13 +52,17 @@ module CustomFields::CustomFieldRendering
|
||||
|
||||
def render_custom_fields(form:)
|
||||
custom_fields.each do |custom_field|
|
||||
form.fields_for(:custom_field_values) do |builder|
|
||||
custom_field_input(builder, custom_field)
|
||||
end
|
||||
if custom_field.has_comment?
|
||||
form.fields_for(:custom_comments) do |builder|
|
||||
custom_comment_input(builder, custom_field)
|
||||
end
|
||||
render_custom_field(form:, custom_field:)
|
||||
end
|
||||
end
|
||||
|
||||
def render_custom_field(form:, custom_field:)
|
||||
form.fields_for(:custom_field_values) do |builder|
|
||||
custom_field_input(builder, custom_field)
|
||||
end
|
||||
if custom_field.has_comment?
|
||||
form.fields_for(:custom_comments) do |builder|
|
||||
custom_comment_input(builder, custom_field)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Users
|
||||
module Form
|
||||
# Admin flag + the custom-field sections (built-in fields interleaved with
|
||||
# custom fields per UserCustomFieldSection, honoring attribute_order).
|
||||
class AttributesForm < ApplicationForm
|
||||
include CustomFields::CustomFieldRendering
|
||||
|
||||
form do |f|
|
||||
admin_flag(f) if User.current.admin?
|
||||
user_sections(f)
|
||||
end
|
||||
|
||||
def initialize(user:, contract:)
|
||||
super()
|
||||
@user = user
|
||||
@contract = contract
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def custom_fields
|
||||
@custom_fields ||= @user.available_custom_fields
|
||||
end
|
||||
|
||||
def admin_flag(form)
|
||||
form.check_box(name: :admin,
|
||||
label: User.human_attribute_name(:admin),
|
||||
disabled: @user == User.current)
|
||||
end
|
||||
|
||||
def user_sections(form)
|
||||
UserCustomFieldSection.includes(:custom_fields).each do |section| # rubocop:disable Rails/FindEach -- honor default scope ordering
|
||||
render_section(form, section)
|
||||
end
|
||||
end
|
||||
|
||||
def render_section(form, section)
|
||||
visible_cfs_by_key = section.custom_fields.visible(User.current).index_by(&:column_name)
|
||||
|
||||
form.fieldset_group(title: section_title(section)) do |group|
|
||||
section.attribute_order.each do |key|
|
||||
if UserCustomFieldSection::BUILT_IN_ATTRIBUTES.include?(key)
|
||||
render_built_in(group, key)
|
||||
elsif (custom_field = visible_cfs_by_key[key])
|
||||
render_custom_field(form: group, custom_field:)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def section_title(section)
|
||||
section.name.presence || I18n.t("settings.user_custom_fields.label_untitled_section")
|
||||
end
|
||||
|
||||
def render_built_in(group, key) # rubocop:disable Metrics/AbcSize
|
||||
case key
|
||||
when "firstname", "lastname", "mail"
|
||||
group.text_field(name: key.to_sym,
|
||||
label: User.human_attribute_name(key),
|
||||
required: true,
|
||||
disabled: !@contract.writable?(key.to_sym),
|
||||
input_width: :medium)
|
||||
when "login"
|
||||
return if @user.new_record?
|
||||
|
||||
group.text_field(name: :login,
|
||||
label: User.human_attribute_name(:login),
|
||||
required: true,
|
||||
disabled: !@contract.writable?(:login),
|
||||
input_width: :medium)
|
||||
when "language"
|
||||
group.select_list(name: :language,
|
||||
label: User.human_attribute_name(:language),
|
||||
include_blank: "--- #{I18n.t(:actionview_instancetag_blank_option)} ---",
|
||||
input_width: :medium) do |list|
|
||||
helpers.lang_options_for_select.each { |label, value| list.option(label:, value:) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,80 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Users
|
||||
module Form
|
||||
# LDAP authentication source select. For a new record it also renders the
|
||||
# hidden login field the admin--users controller reveals when an LDAP source
|
||||
# is selected; for a persisted record the login is already a built-in field,
|
||||
# so the select is wrapped in a titled "Authentication" fieldset only.
|
||||
#
|
||||
# The coordinator decides whether to include this form at all.
|
||||
class AuthenticationSourceForm < ApplicationForm
|
||||
form do |f|
|
||||
if @user.new_record?
|
||||
ldap_auth_source_select(f)
|
||||
hidden_login(f)
|
||||
else
|
||||
f.fieldset_group(title: I18n.t(:label_authentication)) do |group|
|
||||
ldap_auth_source_select(group)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(user:)
|
||||
super()
|
||||
@user = user
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ldap_auth_source_select(target)
|
||||
target.select_list(
|
||||
name: :ldap_auth_source_id,
|
||||
label: User.human_attribute_name(:auth_source),
|
||||
include_blank: I18n.t(:label_internal),
|
||||
input_width: :medium,
|
||||
data: { action: "admin--users#toggleAuthenticationFields" }
|
||||
) do |list|
|
||||
LdapAuthSource.order(:name).each { |source| list.option(label: source.name, value: source.id) }
|
||||
end
|
||||
end
|
||||
|
||||
def hidden_login(form)
|
||||
form.group(hidden: true, data: { "admin--users-target": "authSourceFields" }) do |group|
|
||||
group.text_field(name: :login,
|
||||
label: User.human_attribute_name(:login),
|
||||
required: true,
|
||||
input_width: :medium)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,124 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Users
|
||||
module Form
|
||||
# The internal password / activation block as a single Primer group. All three
|
||||
# Stimulus controllers sit on the wrapper: the password-requirements controller
|
||||
# finds both its passwordInput and requirement targets within it.
|
||||
# assign_random_password and send_information are not model
|
||||
# attributes, so they render with an explicit checked: and no hidden companion
|
||||
# (the controller relies on their absence when unchecked); send_information is
|
||||
# unscoped (top-level param). Included by the coordinator only for editing an
|
||||
# internal user as admin while password login is enabled.
|
||||
class PasswordForm < ApplicationForm
|
||||
form do |f|
|
||||
f.group(
|
||||
hidden: !@user.change_password_allowed?,
|
||||
data: {
|
||||
controller: "disable-when-checked password-force-change password-requirements",
|
||||
"admin--users-target": "passwordFields"
|
||||
}
|
||||
) do |group|
|
||||
assign_random_password(group)
|
||||
password_fields(group) unless disable_password_choice?
|
||||
send_information(group)
|
||||
force_password_change(group)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(user:, assign_random_password_checked:)
|
||||
super()
|
||||
@user = user
|
||||
@assign_random_password_checked = assign_random_password_checked
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def disable_password_choice?
|
||||
OpenProject::Configuration.disable_password_choice?
|
||||
end
|
||||
|
||||
def assign_random_password(group)
|
||||
group.check_box(name: :assign_random_password,
|
||||
id: "user_assign_random_password",
|
||||
checked: @assign_random_password_checked,
|
||||
include_hidden: false,
|
||||
label: I18n.t("user.assign_random_password"),
|
||||
data: {
|
||||
"disable-when-checked-target": "cause",
|
||||
"password-force-change-target": "assignRandomPassword"
|
||||
})
|
||||
end
|
||||
|
||||
def password_fields(group)
|
||||
group.text_field(name: :password,
|
||||
id: "user_password",
|
||||
type: :password,
|
||||
required: @user.new_record?,
|
||||
label: User.human_attribute_name(:password),
|
||||
caption: helpers.password_complexity_requirements,
|
||||
input_width: :medium,
|
||||
data: {
|
||||
"disable-when-checked-target": "effect",
|
||||
"password-requirements-target": "passwordInput"
|
||||
})
|
||||
group.text_field(name: :password_confirmation,
|
||||
id: "user_password_confirmation",
|
||||
type: :password,
|
||||
required: @user.new_record?,
|
||||
label: User.human_attribute_name(:password_confirmation),
|
||||
input_width: :medium,
|
||||
data: { "disable-when-checked-target": "effect" })
|
||||
end
|
||||
|
||||
def send_information(group)
|
||||
group.check_box(name: :send_information,
|
||||
id: "send_information",
|
||||
scope_name_to_model: false,
|
||||
scope_id_to_model: false,
|
||||
checked: false,
|
||||
include_hidden: false,
|
||||
label: I18n.t(:label_send_information),
|
||||
caption: I18n.t("users.send_information_hint"),
|
||||
data: { "password-force-change-target": "sendInformationCheckbox" })
|
||||
end
|
||||
|
||||
def force_password_change(group)
|
||||
group.check_box(name: :force_password_change,
|
||||
id: "user_force_password_change",
|
||||
checked: @user.force_password_change,
|
||||
label: User.human_attribute_name(:force_password_change),
|
||||
caption: I18n.t("users.force_password_change_hint"),
|
||||
data: { "password-force-change-target": "forceChangeCheckbox" })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,87 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Users
|
||||
module Form
|
||||
# Preferences section of the administration user form: time zone, color mode
|
||||
# and keyboard shortcuts. Bound to the user's preference (scope: :pref).
|
||||
#
|
||||
# My::LookAndFeelForm could not be reused directly because it renders its own
|
||||
# submit button and extra fields (comments sorting, contrast); the time zone
|
||||
# option building mirrors My::TimeZoneForm.
|
||||
class PreferencesForm < ApplicationForm
|
||||
form do |f|
|
||||
f.fieldset_group(title: I18n.t(:label_preferences)) do |group|
|
||||
group.select_list(
|
||||
name: :time_zone,
|
||||
label: UserPreference.human_attribute_name(:time_zone),
|
||||
include_blank: false,
|
||||
input_width: :medium
|
||||
) do |list|
|
||||
time_zone_options.each { |label, value| list.option(label:, value:) }
|
||||
end
|
||||
|
||||
group.select_list(
|
||||
name: :theme,
|
||||
label: UserPreference.human_attribute_name(:theme),
|
||||
caption: UserPreference.human_attribute_name(:mode_guideline),
|
||||
include_blank: false,
|
||||
input_width: :medium
|
||||
) do |list|
|
||||
helpers.theme_options_for_select.each { |label, value| list.option(label:, value:) }
|
||||
end
|
||||
|
||||
group.check_box(
|
||||
name: :disable_keyboard_shortcuts,
|
||||
label: UserPreference.human_attribute_name(:disable_keyboard_shortcuts),
|
||||
caption: helpers.link_translate(:"user_preferences.disable_keyboard_shortcuts_caption",
|
||||
links: { docs_url: %i[shortcuts] })
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def time_zone_options
|
||||
UserPreferences::UpdateContract
|
||||
.assignable_time_zones
|
||||
.group_by { |zone| zone.tzinfo.canonical_zone }
|
||||
.map { |canonical_zone, zones| time_zone_entry(canonical_zone, zones) }
|
||||
end
|
||||
|
||||
def time_zone_entry(canonical_zone, zones)
|
||||
zone_names = zones.map(&:name).join(", ")
|
||||
offset = ActiveSupport::TimeZone.seconds_to_utc_offset(canonical_zone.base_utc_offset)
|
||||
|
||||
["(UTC#{offset}) #{zone_names}", canonical_zone.identifier]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -153,4 +153,34 @@ module UsersHelper
|
||||
def can_users_have_auth_source?
|
||||
LdapAuthSource.any? && !OpenProject::Configuration.disable_password_login?
|
||||
end
|
||||
|
||||
# Renders the user form extension hooks inside the (Primer) user form.
|
||||
#
|
||||
# - +view_users_primer_form+ receives the Primer form builder and is the API
|
||||
# plugins should use going forward.
|
||||
# - +view_users_form+ is deprecated but kept working: it receives a legacy
|
||||
# TabularFormBuilder rendered inside the same form, so existing plugins keep
|
||||
# submitting their fields. It is only rendered (and the deprecation logged)
|
||||
# when a listener is actually registered.
|
||||
def render_user_form_hooks(user:, form:)
|
||||
safe_join([
|
||||
call_hook(:view_users_primer_form, user:, form:),
|
||||
legacy_user_form_hook(user:)
|
||||
].compact)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def legacy_user_form_hook(user:)
|
||||
return unless OpenProject::Hook.hook_listeners(:view_users_form).any?
|
||||
|
||||
OpenProject::Deprecation.warn(
|
||||
"The `view_users_form` hook is deprecated; migrate to `view_users_primer_form` " \
|
||||
"which receives a Primer form builder."
|
||||
)
|
||||
|
||||
fields_for(:user, user, builder: TabularFormBuilder) do |legacy_form|
|
||||
call_hook(:view_users_form, user:, form: legacy_form)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<%# locals: (user:) -%>
|
||||
<section class="form--section">
|
||||
<h3 class="form--section-title"><%= t("consent.title") %></h3>
|
||||
<% consent_link = admin_settings_users_path(anchor: "consent_settings") %>
|
||||
|
||||
<%= render ::Components::OnOffStatusComponent.new(
|
||||
{
|
||||
is_on: @user.consented_at.present?,
|
||||
on_text: format_time(@user.consented_at),
|
||||
is_on: user.consented_at.present?,
|
||||
on_text: format_time(user.consented_at),
|
||||
on_description: link_translate("consent.user_has_consented", links: { consent_settings: consent_link }),
|
||||
off_text: t(:label_never),
|
||||
off_description: link_translate("consent.not_yet_consented", links: { consent_settings: consent_link })
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
<%#-- copyright
|
||||
OpenProject is an open source project management software.
|
||||
Copyright (C) the OpenProject GmbH
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License version 3.
|
||||
|
||||
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
Copyright (C) 2010-2013 the ChiliProject Team
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<%= error_messages_for @user %>
|
||||
|
||||
<section class="form--section">
|
||||
<% if current_user.admin? %>
|
||||
<%= render partial: "users/form/admin_flag", locals: { f: f } %>
|
||||
<% end %>
|
||||
|
||||
<% UserCustomFieldSection.includes(:custom_fields).each do |section| # rubocop:disable Rails/FindEach -- to honor default scope ordering %>
|
||||
<%= render Users::Form::CustomFieldSectionComponent.new(section:, form: f, contract: @contract, user: @user) %>
|
||||
<% end %>
|
||||
<%= call_hook(:view_users_form, user: @user, form: f) %>
|
||||
</section>
|
||||
|
||||
<% if Setting.consent_required? %>
|
||||
<%= render partial: "users/consent" %>
|
||||
<% end %>
|
||||
|
||||
<%= render partial: "users/form/authentication/form", locals: { f: f } %>
|
||||
|
||||
<% if current_user.admin? %>
|
||||
<%= render partial: "users/form/preferences", locals: { f: f } %>
|
||||
<% end %>
|
||||
@@ -27,32 +27,20 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<section class="form--section">
|
||||
<div class="form--field">
|
||||
<%= styled_label_tag "FIXME", I18n.t("attributes.status") %>
|
||||
<div class="form--field-container">
|
||||
<span>
|
||||
<span class=""><%= full_user_status(@user, true) %></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<%= error_messages_for @user %>
|
||||
|
||||
<%= labelled_tabular_form_for @user,
|
||||
url: { controller: "/users",
|
||||
action: "update",
|
||||
tab: nil },
|
||||
html: {
|
||||
method: :put,
|
||||
autocomplete: "off"
|
||||
},
|
||||
data: {
|
||||
controller: "admin--users",
|
||||
turbo: false,
|
||||
"admin--users-password-auth-selected-value": @user.ldap_auth_source_id.blank?
|
||||
},
|
||||
as: :user do |f| %>
|
||||
<%= render partial: "users/form", locals: { f: f } %>
|
||||
|
||||
<%= styled_button_tag t(:button_save), class: "-primary -with-icon icon-checkmark" %>
|
||||
<% end %>
|
||||
<%=
|
||||
settings_primer_form_with(
|
||||
model: @user,
|
||||
url: { controller: "/users", action: "update", tab: nil },
|
||||
method: :put,
|
||||
html: { autocomplete: "off" },
|
||||
data: {
|
||||
controller: "admin--users",
|
||||
turbo: false,
|
||||
"admin--users-password-auth-selected-value": @user.ldap_auth_source_id.blank?
|
||||
}
|
||||
) do |f|
|
||||
render(Users::FormComponent.new(builder: f, user: @user, contract: @contract))
|
||||
end
|
||||
%>
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
<%#-- copyright
|
||||
OpenProject is an open source project management software.
|
||||
Copyright (C) the OpenProject GmbH
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License version 3.
|
||||
|
||||
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
Copyright (C) 2010-2013 the ChiliProject Team
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
++#%>
|
||||
<%= fields_for :pref, @user.pref, builder: TabularFormBuilder, lang: current_language do |pref_fields| %>
|
||||
<%= render Settings::TimeZoneSettingComponent.new(
|
||||
"time_zone",
|
||||
form: pref_fields,
|
||||
include_blank: false,
|
||||
container_class: defined?(input_size) ? "-#{input_size}" : "-wide"
|
||||
) %>
|
||||
<div class="form--field">
|
||||
<%= pref_fields.select :theme, theme_options_for_select, container_class: "-middle" %> <!-- TODO -->
|
||||
<div class="form--field-instructions">
|
||||
<%= I18n.t("activerecord.attributes.user_preference.mode_guideline") %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form--field">
|
||||
<%= pref_fields.check_box :disable_keyboard_shortcuts,
|
||||
label: I18n.t("activerecord.attributes.user_preference.disable_keyboard_shortcuts") %>
|
||||
<span class="form--field-instructions">
|
||||
<%= link_translate(:"user_preferences.disable_keyboard_shortcuts_caption",
|
||||
links: { docs_url: %i[shortcuts] }) %>
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -1,40 +0,0 @@
|
||||
<%#-- copyright
|
||||
OpenProject is an open source project management software.
|
||||
Copyright (C) the OpenProject GmbH
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License version 3.
|
||||
|
||||
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
Copyright (C) 2010-2013 the ChiliProject Team
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<section class="form--section">
|
||||
<% if current_user.admin? %>
|
||||
<%= render partial: "users/form/authentication/auth_source", locals: { f: f } %>
|
||||
<%= render partial: "users/form/admin_flag", locals: { f: f } %>
|
||||
<% end %>
|
||||
|
||||
<% UserCustomFieldSection.includes(:custom_fields).each do |section| # rubocop:disable Rails/FindEach -- to honor default scope ordering %>
|
||||
<%= render Users::Form::CustomFieldSectionComponent.new(section:, form: f, contract: @contract, user: @user) %>
|
||||
<% end %>
|
||||
<%= call_hook(:view_users_form, user: @user, form: f) %>
|
||||
</section>
|
||||
@@ -1,4 +0,0 @@
|
||||
<div class="form--field">
|
||||
<%= f.check_box :admin,
|
||||
disabled: (@user == User.current) %>
|
||||
</div>
|
||||
@@ -1,4 +0,0 @@
|
||||
<section class="form--section">
|
||||
<h3 class="form--section-title"><%= t(:label_preferences) %></h3>
|
||||
<%= render partial: "users/preferences", locals: { input_size: :middle } %>
|
||||
</section>
|
||||
@@ -1,22 +0,0 @@
|
||||
<% if can_users_have_auth_source? %>
|
||||
<div class="form--field">
|
||||
<%= f.collection_select :ldap_auth_source_id,
|
||||
LdapAuthSource.all,
|
||||
:id,
|
||||
:name,
|
||||
{
|
||||
label: :"activerecord.attributes.user.auth_source",
|
||||
container_class: "-middle",
|
||||
include_blank: t(:label_internal)
|
||||
},
|
||||
data: {
|
||||
action: "admin--users#toggleAuthenticationFields"
|
||||
} %>
|
||||
</div>
|
||||
|
||||
<div class="form--field"
|
||||
hidden
|
||||
data-admin--users-target="authSourceFields">
|
||||
<%= f.text_field :login, required: true, size: 25 %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -1,19 +1,16 @@
|
||||
<div class="form--field -reduced-margin">
|
||||
<%= styled_label_tag nil, I18n.t("user.authentication_provider") %>
|
||||
<div class="form--field-container">
|
||||
<%= @user.human_authentication_provider %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form--field-instructions">
|
||||
<%= I18n.t("user.authentication_settings_disabled_due_to_external_authentication") %>
|
||||
</div>
|
||||
<%# locals: (user:) -%>
|
||||
<%= render(Primer::Box.new(mb: 3)) do %>
|
||||
<%= render(Primer::Beta::Text.new(tag: :div, font_weight: :bold)) { I18n.t("user.authentication_provider") } %>
|
||||
<%= render(Primer::Beta::Text.new(tag: :div)) { user.human_authentication_provider } %>
|
||||
<%= render(Primer::Beta::Text.new(tag: :div, color: :muted, font_size: :small)) do %>
|
||||
<%= I18n.t("user.authentication_settings_disabled_due_to_external_authentication") %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<div class="form--field -top-margin -reduced-margin">
|
||||
<%= styled_label_tag nil, User.human_attribute_name(:identity_url) %>
|
||||
<div class="form--field-container -empty">
|
||||
<%= @user.identity_url.split(":", 2).last %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form--field-instructions">
|
||||
<%= I18n.t("user.identity_url_text") %>
|
||||
</div>
|
||||
<%= render(Primer::Box.new) do %>
|
||||
<%= render(Primer::Beta::Text.new(tag: :div, font_weight: :bold)) { User.human_attribute_name(:identity_url) } %>
|
||||
<%= render(Primer::Beta::Text.new(tag: :div)) { user.identity_url.split(":", 2).last } %>
|
||||
<%= render(Primer::Beta::Text.new(tag: :div, color: :muted, font_size: :small)) do %>
|
||||
<%= I18n.t("user.identity_url_text") %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<section class="form--section">
|
||||
<% if current_user.admin? || can_users_have_auth_source? || @user.uses_external_authentication? %>
|
||||
<h3 class="form--section-title"><%= t(:label_authentication) %></h3>
|
||||
<% end %>
|
||||
|
||||
<% if @user.uses_external_authentication? %>
|
||||
<%= render partial: "users/form/authentication/external", locals: { f: f } %>
|
||||
<% else %>
|
||||
<%= render partial: "users/form/authentication/auth_source", locals: { f: f } %>
|
||||
|
||||
<% if current_user.admin? %>
|
||||
<%= render partial: "users/form/authentication/internal", locals: { f: f } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</section>
|
||||
@@ -1,12 +0,0 @@
|
||||
<% if OpenProject::Configuration.disable_password_login? %>
|
||||
<div id="no_password_info">
|
||||
<div class="form--field">
|
||||
<%= styled_label_tag nil, I18n.t(:warning) %>
|
||||
<div class="form--field-container">
|
||||
<%= I18n.t "user.no_login" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= render partial: "users/form/authentication/internal_password", locals: { f: f } %>
|
||||
<% end %>
|
||||
@@ -1,71 +0,0 @@
|
||||
<%= content_tag :div,
|
||||
data: {
|
||||
controller: "disable-when-checked password-force-change",
|
||||
"admin--users-target": "passwordFields"
|
||||
},
|
||||
hidden: !@user.change_password_allowed? do %>
|
||||
<% assign_random_password_enabled = params[:user] &&
|
||||
params[:user][:assign_random_password] %>
|
||||
<div class="form--field">
|
||||
<%= styled_label_tag "user_assign_random_password",
|
||||
I18n.t("user.assign_random_password") %>
|
||||
<div class="form--field-container">
|
||||
<%= styled_check_box_tag "user[assign_random_password]",
|
||||
"1",
|
||||
assign_random_password_enabled,
|
||||
data: {
|
||||
"disable-when-checked-target": "cause",
|
||||
"password-force-change-target": "assignRandomPassword"
|
||||
} %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% unless OpenProject::Configuration.disable_password_choice? %>
|
||||
<div class="form--field" data-controller="password-requirements">
|
||||
<%= f.password_field :password,
|
||||
required: @user.new_record?,
|
||||
disabled: assign_random_password_enabled,
|
||||
data: {
|
||||
"disable-when-checked-target": "effect",
|
||||
"password-requirements-target": "passwordInput"
|
||||
},
|
||||
container_class: "-middle" %>
|
||||
<div class="form--field-instructions">
|
||||
<%= password_complexity_requirements %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form--field">
|
||||
<%= f.password_field :password_confirmation,
|
||||
required: @user.new_record?,
|
||||
disabled: assign_random_password_enabled,
|
||||
data: {
|
||||
"disable-when-checked-target": "effect"
|
||||
},
|
||||
container_class: "-middle" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="form--field send-information">
|
||||
<%= styled_label_tag "send_information", t(:label_send_information) %>
|
||||
<div class="form--field-container">
|
||||
<%= styled_check_box_tag(
|
||||
"send_information",
|
||||
"1",
|
||||
false,
|
||||
data: { "password-force-change-target": "sendInformationCheckbox" }
|
||||
) %>
|
||||
</div>
|
||||
<div class="form--field-instructions">
|
||||
<%= t("users.send_information_hint") %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form--field">
|
||||
<%= f.check_box :force_password_change,
|
||||
disabled: assign_random_password_enabled,
|
||||
data: { "password-force-change-target": "forceChangeCheckbox" } %>
|
||||
<div class="form--field-instructions">
|
||||
<%= t("users.force_password_change_hint") %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -1,7 +0,0 @@
|
||||
<%# locals: (f:, contract:, user:) -%>
|
||||
<div class="form--field -required">
|
||||
<%= f.text_field :firstname,
|
||||
required: true,
|
||||
disabled: !contract.writable?(:firstname),
|
||||
container_class: "-middle" %>
|
||||
</div>
|
||||
@@ -1,7 +0,0 @@
|
||||
<%# locals: (f:, contract:, user:) -%>
|
||||
<div class="form--field">
|
||||
<%= f.select :language,
|
||||
lang_options_for_select,
|
||||
prompt: "--- #{t(:actionview_instancetag_blank_option)} ---",
|
||||
container_class: "-middle" %>
|
||||
</div>
|
||||
@@ -1,7 +0,0 @@
|
||||
<%# locals: (f:, contract:, user:) -%>
|
||||
<div class="form--field -required">
|
||||
<%= f.text_field :lastname,
|
||||
required: true,
|
||||
disabled: !contract.writable?(:lastname),
|
||||
container_class: "-middle" %>
|
||||
</div>
|
||||
@@ -1,10 +0,0 @@
|
||||
<%# locals: (f:, contract:, user:) -%>
|
||||
<% unless user.new_record? %>
|
||||
<div class="form--field -required">
|
||||
<%= f.text_field :login,
|
||||
required: true,
|
||||
disabled: !contract.writable?(:login),
|
||||
size: 25,
|
||||
container_class: "-middle" %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -1,7 +0,0 @@
|
||||
<%# locals: (f:, contract:, user:) -%>
|
||||
<div class="form--field -required">
|
||||
<%= f.text_field :mail,
|
||||
required: true,
|
||||
disabled: !contract.writable?(:mail),
|
||||
container_class: "-middle" %>
|
||||
</div>
|
||||
@@ -43,19 +43,17 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
<%= error_messages_for @user %>
|
||||
|
||||
<%= labelled_tabular_form_for @user,
|
||||
url: { action: "create" },
|
||||
html: { class: nil, autocomplete: "off" },
|
||||
data: {
|
||||
controller: "admin--users",
|
||||
turbo: false,
|
||||
"admin--users-password-auth-selected-value": @user.ldap_auth_source_id.blank?
|
||||
},
|
||||
as: :user do |f| %>
|
||||
<%= render partial: "simple_form", locals: { f: f, auth_sources: @ldap_auth_sources, user: @user } %>
|
||||
|
||||
<p>
|
||||
<%= styled_button_tag t(:button_create), class: "-primary -with-icon icon-checkmark" %>
|
||||
<%= styled_button_tag t(:button_create_and_continue), name: "continue", class: "-primary -with-icon icon-checkmark" %>
|
||||
</p>
|
||||
<% end %>
|
||||
<%=
|
||||
settings_primer_form_with(
|
||||
model: @user,
|
||||
url: { action: "create" },
|
||||
html: { autocomplete: "off" },
|
||||
data: {
|
||||
controller: "admin--users",
|
||||
turbo: false,
|
||||
"admin--users-password-auth-selected-value": @user.ldap_auth_source_id.blank?
|
||||
}
|
||||
) do |f|
|
||||
render(Users::FormComponent.new(builder: f, user: @user, contract: @contract))
|
||||
end
|
||||
%>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe Users::FormComponent, type: :component do
|
||||
let(:current_user) { build_stubbed(:admin) }
|
||||
let(:contract) { instance_double(Users::UpdateContract, writable?: true) }
|
||||
|
||||
before do
|
||||
User.current = current_user
|
||||
create(:user_custom_field_section, attribute_order: UserCustomFieldSection::BUILT_IN_ATTRIBUTES)
|
||||
end
|
||||
|
||||
def render_component(user:)
|
||||
render_in_view_context(user, contract) do |form_user, form_contract|
|
||||
primer_form_with(model: form_user, url: "/users") do |form|
|
||||
render(Users::FormComponent.new(builder: form, user: form_user, contract: form_contract))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "for a new user" do
|
||||
it "renders the attributes and the create buttons, but no password block" do
|
||||
render_component(user: User.new)
|
||||
|
||||
expect(page).to have_field("user[firstname]")
|
||||
expect(page).to have_button(I18n.t(:button_create))
|
||||
expect(page).to have_button(I18n.t(:button_create_and_continue))
|
||||
expect(page).to have_no_css("[data-admin--users-target='passwordFields']", visible: :all)
|
||||
end
|
||||
end
|
||||
|
||||
context "for a persisted internal user edited by an admin" do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
it "renders status, the password block, preferences and a Save button" do
|
||||
render_component(user:)
|
||||
|
||||
expect(page).to have_css("[data-admin--users-target='passwordFields']", visible: :all)
|
||||
expect(page).to have_select("pref[time_zone]")
|
||||
expect(page).to have_button(I18n.t(:button_save))
|
||||
end
|
||||
|
||||
it "renders the consent status when consent is required" do
|
||||
allow(Setting).to receive(:consent_required?).and_return(true)
|
||||
|
||||
render_component(user:)
|
||||
|
||||
expect(page).to have_text(I18n.t("consent.title"))
|
||||
end
|
||||
end
|
||||
|
||||
context "for a persisted external-auth user" do
|
||||
let(:user) { create(:user, identity_url: "saml:user-id") }
|
||||
|
||||
before { allow(user).to receive(:uses_external_authentication?).and_return(true) }
|
||||
|
||||
it "renders the read-only authentication provider instead of the password block" do
|
||||
render_component(user:)
|
||||
|
||||
expect(page).to have_text(I18n.t("user.authentication_provider"))
|
||||
expect(page).to have_no_css("[data-admin--users-target='passwordFields']", visible: :all)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -59,7 +59,7 @@ RSpec.describe "edit users", :js do
|
||||
before { visit edit_user_path(user) }
|
||||
|
||||
it "saves a custom field value" do
|
||||
within "fieldset", text: "Professional info".upcase do
|
||||
within "fieldset", text: "Professional info" do
|
||||
fill_in "Job title", with: "Software Engineer"
|
||||
fill_in "Internal code", with: "SE"
|
||||
end
|
||||
|
||||
@@ -143,19 +143,19 @@ RSpec.describe "random password generation", :js do
|
||||
# And I try to set my new password to "adminADMIN"
|
||||
fill_in "user_password", with: "adminADMIN"
|
||||
fill_in "user_password_confirmation", with: "adminADMIN"
|
||||
scroll_to_and_click(find(".button", text: "Save"))
|
||||
scroll_to_and_click(find_button("Save"))
|
||||
expect_flash(type: :error, message: "Password Must include characters of the following types")
|
||||
|
||||
# Has numeric but still missing special
|
||||
fill_in "user_password", with: "adminADMIN123"
|
||||
fill_in "user_password_confirmation", with: "adminADMIN123"
|
||||
scroll_to_and_click(find(".button", text: "Save"))
|
||||
scroll_to_and_click(find_button("Save"))
|
||||
expect_flash(type: :error, message: "Password Must include characters of the following types")
|
||||
|
||||
# All classes
|
||||
fill_in "user_password", with: "adminADMIN1!"
|
||||
fill_in "user_password_confirmation", with: "adminADMIN1!"
|
||||
scroll_to_and_click(find(".button", text: "Save"))
|
||||
scroll_to_and_click(find_button("Save"))
|
||||
expect_flash(message: I18n.t(:notice_successful_update))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -204,4 +204,26 @@ RSpec.describe CustomFields::CustomFieldRendering do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#render_custom_field" do
|
||||
let(:values_builder) { instance_double(ActionView::Helpers::FormBuilder) }
|
||||
let(:custom_field) { build(:custom_field, :string) }
|
||||
|
||||
before do
|
||||
allow(builder).to receive(:fields_for).with(:custom_field_values).and_yield(values_builder)
|
||||
allow(form_instance).to receive(:additional_custom_field_input_arguments).and_return({})
|
||||
end
|
||||
|
||||
it "renders a single custom field input" do
|
||||
allow(CustomFields::Inputs::String).to receive(:new)
|
||||
|
||||
form_instance.render_custom_field(form: builder, custom_field:)
|
||||
|
||||
expect(CustomFields::Inputs::String).to have_received(:new).with(
|
||||
values_builder,
|
||||
custom_field:,
|
||||
object: model
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+26
-29
@@ -28,41 +28,38 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Users
|
||||
module Form
|
||||
class CustomFieldFieldComponent < ApplicationComponent
|
||||
include ApplicationHelper
|
||||
require "spec_helper"
|
||||
|
||||
def initialize(custom_field:, form:)
|
||||
super()
|
||||
@custom_field = custom_field
|
||||
@form = form
|
||||
end
|
||||
RSpec.describe Users::Form::AttributesForm, type: :forms do
|
||||
before do
|
||||
User.current = current_user
|
||||
create(:user_custom_field_section, attribute_order: UserCustomFieldSection::BUILT_IN_ATTRIBUTES)
|
||||
end
|
||||
|
||||
def field_options
|
||||
{
|
||||
custom_value: @form.object.custom_value_for(@custom_field),
|
||||
custom_field: @custom_field
|
||||
}
|
||||
end
|
||||
include_context "with rendered form"
|
||||
|
||||
def css_classes
|
||||
["form--field", @custom_field.attribute_name, required_class].compact
|
||||
end
|
||||
let(:current_user) { build_stubbed(:admin) }
|
||||
let(:contract) { instance_double(Users::UpdateContract, writable?: true) }
|
||||
let(:params) { { user: model, contract: } }
|
||||
|
||||
def container_class
|
||||
case @custom_field.field_format
|
||||
when "text" then "-xxwide"
|
||||
when "date" then "-xslim"
|
||||
else "-middle"
|
||||
end
|
||||
end
|
||||
context "for a persisted user" do
|
||||
let(:model) { build_stubbed(:user) }
|
||||
|
||||
private
|
||||
it "renders the built-in fields, login and the admin flag" do
|
||||
expect(page).to have_field("user[firstname]")
|
||||
expect(page).to have_field("user[lastname]")
|
||||
expect(page).to have_field("user[mail]")
|
||||
expect(page).to have_select("user[language]")
|
||||
expect(page).to have_field("user[login]")
|
||||
expect(page).to have_field("user[admin]")
|
||||
end
|
||||
end
|
||||
|
||||
def required_class
|
||||
"-required" if @custom_field.is_required? && !@custom_field.boolean?
|
||||
end
|
||||
context "for a new user" do
|
||||
let(:model) { User.new }
|
||||
|
||||
it "omits the login field" do
|
||||
expect(page).to have_no_field("user[login]")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,63 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe Users::Form::AuthenticationSourceForm, type: :forms do
|
||||
before do
|
||||
User.current = build_stubbed(:admin)
|
||||
create(:ldap_auth_source)
|
||||
end
|
||||
|
||||
include_context "with rendered form"
|
||||
|
||||
let(:params) { { user: model } }
|
||||
|
||||
context "for a new user" do
|
||||
let(:model) { User.new }
|
||||
|
||||
it "renders the auth source select with the toggle action and a hidden login group" do
|
||||
expect(page).to have_select("user[ldap_auth_source_id]")
|
||||
expect(page).to have_css("[data-action~='admin--users#toggleAuthenticationFields']")
|
||||
expect(page).to have_css("[data-admin--users-target='authSourceFields'][hidden]", visible: :all)
|
||||
expect(page).to have_field("user[login]", visible: :all)
|
||||
end
|
||||
end
|
||||
|
||||
context "for a persisted user" do
|
||||
let(:model) { build_stubbed(:user) }
|
||||
|
||||
it "renders the select inside a titled Authentication fieldset and no hidden login" do
|
||||
expect(page).to have_select("user[ldap_auth_source_id]")
|
||||
expect(page).to have_css("fieldset", text: /#{I18n.t(:label_authentication)}/i)
|
||||
expect(page).to have_no_css("[data-admin--users-target='authSourceFields']", visible: :all)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,61 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe Users::Form::PasswordForm, type: :forms do
|
||||
include_context "with rendered form"
|
||||
|
||||
let(:model) { build_stubbed(:user) }
|
||||
let(:params) { { user: model, assign_random_password_checked: false } }
|
||||
|
||||
before { allow(model).to receive(:change_password_allowed?).and_return(true) }
|
||||
|
||||
it "renders one wrapper carrying the three controllers and the passwordFields target" do
|
||||
expect(page).to have_css(
|
||||
"[data-controller~='disable-when-checked'][data-controller~='password-force-change']" \
|
||||
"[data-controller~='password-requirements'][data-admin--users-target='passwordFields']",
|
||||
visible: :all
|
||||
)
|
||||
end
|
||||
|
||||
it "renders the password fields with their ids and password type" do
|
||||
expect(page).to have_field("user[password]", type: "password", visible: :all)
|
||||
expect(page).to have_field("user[password_confirmation]", type: "password", visible: :all)
|
||||
expect(page).to have_css("#user_password", visible: :all)
|
||||
end
|
||||
|
||||
it "renders the unscoped send_information checkbox and the scoped flags" do
|
||||
expect(page).to have_field("send_information", visible: :all)
|
||||
expect(page).to have_css("#send_information", visible: :all)
|
||||
expect(page).to have_field("user[assign_random_password]", visible: :all)
|
||||
expect(page).to have_field("user[force_password_change]", visible: :all)
|
||||
end
|
||||
end
|
||||
+8
-27
@@ -28,35 +28,16 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Users
|
||||
module Form
|
||||
class CustomFieldSectionComponent < ApplicationComponent
|
||||
def initialize(section:, form:, contract:, user:)
|
||||
super()
|
||||
@section = section
|
||||
@form = form
|
||||
@contract = contract
|
||||
@user = user
|
||||
@visible_cfs_by_key = visible_cfs_by_key(section)
|
||||
end
|
||||
require "spec_helper"
|
||||
|
||||
def title
|
||||
@section.name.presence || I18n.t("settings.user_custom_fields.label_untitled_section")
|
||||
end
|
||||
RSpec.describe Users::Form::PreferencesForm, type: :forms do
|
||||
include_context "with rendered form"
|
||||
|
||||
def built_in?(key)
|
||||
UserCustomFieldSection::BUILT_IN_ATTRIBUTES.include?(key)
|
||||
end
|
||||
let(:model) { build_stubbed(:user_preference) }
|
||||
|
||||
def visible_custom_field(key)
|
||||
@visible_cfs_by_key[key]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def visible_cfs_by_key(section)
|
||||
section.custom_fields.visible(User.current).index_by(&:column_name)
|
||||
end
|
||||
end
|
||||
it "renders the time zone, color mode and keyboard shortcuts fields" do
|
||||
expect(page).to have_select("Time zone")
|
||||
expect(page).to have_select("Color mode")
|
||||
expect(page).to have_field("Disable keyboard shortcuts")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,74 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe UsersHelper do
|
||||
describe "#render_user_form_hooks" do
|
||||
let(:form) { instance_double(Primer::Forms::Builder) }
|
||||
let(:user) { build_stubbed(:user) }
|
||||
|
||||
before do
|
||||
allow(helper).to receive(:call_hook).and_return("".html_safe)
|
||||
allow(OpenProject::Deprecation).to receive(:warn)
|
||||
allow(helper).to receive(:fields_for).and_yield(instance_double(TabularFormBuilder))
|
||||
end
|
||||
|
||||
it "always calls the new Primer hook" do
|
||||
allow(OpenProject::Hook).to receive(:hook_listeners).with(:view_users_form).and_return([])
|
||||
|
||||
helper.render_user_form_hooks(user:, form:)
|
||||
|
||||
expect(helper).to have_received(:call_hook).with(:view_users_primer_form, hash_including(form:))
|
||||
end
|
||||
|
||||
context "when no legacy listener is registered" do
|
||||
before { allow(OpenProject::Hook).to receive(:hook_listeners).with(:view_users_form).and_return([]) }
|
||||
|
||||
it "does not warn and does not render the legacy hook" do
|
||||
helper.render_user_form_hooks(user:, form:)
|
||||
|
||||
expect(OpenProject::Deprecation).not_to have_received(:warn)
|
||||
expect(helper).not_to have_received(:fields_for)
|
||||
end
|
||||
end
|
||||
|
||||
context "when a legacy listener is registered" do
|
||||
before { allow(OpenProject::Hook).to receive(:hook_listeners).with(:view_users_form).and_return([instance_double(Object)]) }
|
||||
|
||||
it "logs a deprecation and renders the legacy hook" do
|
||||
helper.render_user_form_hooks(user:, form:)
|
||||
|
||||
expect(OpenProject::Deprecation).to have_received(:warn)
|
||||
expect(helper).to have_received(:call_hook).with(:view_users_form, hash_including(:form))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -43,6 +43,7 @@ RSpec.describe "users/edit" do
|
||||
assign(:auth_sources, [])
|
||||
assign(:contract, Users::UpdateContract.new(user, current_user))
|
||||
|
||||
User.current = current_user
|
||||
without_partial_double_verification do
|
||||
allow(view).to receive(:current_user).and_return(current_user)
|
||||
end
|
||||
@@ -105,7 +106,7 @@ RSpec.describe "users/edit" do
|
||||
end
|
||||
|
||||
context "with password-based login" do
|
||||
let(:user) { build(:user, id: 42) }
|
||||
let(:user) { build_stubbed(:user) }
|
||||
|
||||
context "with password login disabled" do
|
||||
before do
|
||||
|
||||
Reference in New Issue
Block a user