feat: View mode por role/usuário com seletor de visibilidade no perfil

- Cria view mode e display 'restricted' (template mostra só username + mensagem)
- Adiciona campo field_user_selected_view_mode (string, default: restricted)
- update_10006: provisiona view mode, display e campo; inicializa role_view_modes
- Seletor de visibilidade no form de edição do perfil (owner e admin)
- hook_entity_view_mode_alter lê o campo e valida existência do display
- Formulário de admin: checkboxes de view modes por role (exceto anonymous e authenticated)
- Schema YAML completo para site_users.settings incluindo role_view_modes
- Tradução pt-BR de todas as novas strings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 08:06:10 -03:00
parent f58c8f90f4
commit bd24e6eb6b
9 changed files with 431 additions and 2 deletions

View File

@@ -8,3 +8,4 @@ user_editable_fields:
field_user_bio: true field_user_bio: true
field_user_social_links: true field_user_social_links: true
field_user_photos: true field_user_photos: true
role_view_modes: { }

View File

@@ -0,0 +1,20 @@
langcode: en
status: true
dependencies:
config:
- field.storage.user.field_user_selected_view_mode
module:
- user
id: user.user.field_user_selected_view_mode
field_name: field_user_selected_view_mode
entity_type: user
bundle: user
label: 'Profile visibility'
description: 'Controls how this profile appears to other visitors.'
required: false
translatable: false
default_value:
- value: restricted
default_value_callback: ''
settings: { }
field_type: string

View File

@@ -0,0 +1,20 @@
langcode: en
status: true
dependencies:
module:
- user
id: user.field_user_selected_view_mode
field_name: field_user_selected_view_mode
entity_type: user
type: string
settings:
max_length: 64
is_ascii: true
case_sensitive: false
module: core
locked: false
cardinality: 1
translatable: false
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,32 @@
site_users.settings:
type: config_object
label: 'Site Users settings'
mapping:
photos:
type: mapping
label: 'Photo settings'
mapping:
max_count:
type: integer
label: 'Maximum number of photos'
ldap_attribute:
type: string
label: 'LDAP photo attribute'
ldap_sync_enabled:
type: boolean
label: 'Enable LDAP photo synchronization'
user_editable_fields:
type: sequence
label: 'User-editable profile fields'
sequence:
type: boolean
label: 'Field editable'
role_view_modes:
type: sequence
label: 'Allowed view modes per role'
sequence:
type: sequence
label: 'View modes for role'
sequence:
type: string
label: 'View mode machine name'

View File

@@ -165,6 +165,27 @@ function site_users_install() {
$view_display->save(); $view_display->save();
} }
// Criar view mode 'restricted'.
$view_mode_storage = \Drupal::entityTypeManager()->getStorage('entity_view_mode');
if (!$view_mode_storage->load('user.restricted')) {
\Drupal\Core\Entity\Entity\EntityViewMode::create([
'id' => 'user.restricted',
'label' => 'Restricted',
'targetEntityType' => 'user',
])->save();
}
// Criar view display 'restricted' vazio (template controla a exibição).
$display_storage = \Drupal::entityTypeManager()->getStorage('entity_view_display');
if (!$display_storage->load('user.user.restricted')) {
\Drupal\Core\Entity\Entity\EntityViewDisplay::create([
'targetEntityType' => 'user',
'bundle' => 'user',
'mode' => 'restricted',
'status' => TRUE,
])->save();
}
// Conceder permissão "access user profiles" ao papel anonymous. // Conceder permissão "access user profiles" ao papel anonymous.
$anonymous_role = \Drupal\user\Entity\Role::load('anonymous'); $anonymous_role = \Drupal\user\Entity\Role::load('anonymous');
if ($anonymous_role && !$anonymous_role->hasPermission('access user profiles')) { if ($anonymous_role && !$anonymous_role->hasPermission('access user profiles')) {
@@ -388,6 +409,84 @@ function site_users_update_10003() {
return t('Nenhuma correção necessária nos mapeamentos LDAP.'); return t('Nenhuma correção necessária nos mapeamentos LDAP.');
} }
/**
* Cria view mode 'restricted', campo field_user_selected_view_mode e config role_view_modes.
*/
function site_users_update_10006() {
$view_mode_storage = \Drupal::entityTypeManager()->getStorage('entity_view_mode');
$display_storage = \Drupal::entityTypeManager()->getStorage('entity_view_display');
// 1. Criar view mode 'restricted'.
if (!$view_mode_storage->load('user.restricted')) {
\Drupal\Core\Entity\Entity\EntityViewMode::create([
'id' => 'user.restricted',
'label' => 'Restricted',
'targetEntityType' => 'user',
])->save();
}
// 2. Criar view display 'restricted' vazio (template controla a exibição).
if (!$display_storage->load('user.user.restricted')) {
\Drupal\Core\Entity\Entity\EntityViewDisplay::create([
'targetEntityType' => 'user',
'bundle' => 'user',
'mode' => 'restricted',
'status' => TRUE,
])->save();
}
// 3. Criar field storage se não existir.
if (!FieldStorageConfig::loadByName('user', 'field_user_selected_view_mode')) {
FieldStorageConfig::create([
'field_name' => 'field_user_selected_view_mode',
'entity_type' => 'user',
'type' => 'string',
'settings' => [
'max_length' => 64,
'is_ascii' => TRUE,
'case_sensitive' => FALSE,
],
'cardinality' => 1,
'translatable' => FALSE,
])->save();
}
// 4. Criar field instance se não existir.
if (!FieldConfig::loadByName('user', 'user', 'field_user_selected_view_mode')) {
FieldConfig::create([
'field_name' => 'field_user_selected_view_mode',
'entity_type' => 'user',
'bundle' => 'user',
'label' => 'Profile visibility',
'description' => 'Controls how this profile appears to other visitors.',
'required' => FALSE,
'default_value' => [['value' => 'restricted']],
])->save();
}
// 5. Remover do form display.
$form_display = EntityFormDisplay::load('user.user.default');
if ($form_display) {
$form_display->removeComponent('field_user_selected_view_mode')->save();
}
// 6. Remover de todos os view displays existentes.
$displays = $display_storage->loadMultiple();
foreach ($displays as $display) {
if ($display->getTargetEntityTypeId() === 'user' && $display->getComponent('field_user_selected_view_mode')) {
$display->removeComponent('field_user_selected_view_mode')->save();
}
}
// 7. Adicionar role_view_modes ao config se ausente.
$config = \Drupal::configFactory()->getEditable('site_users.settings');
if ($config->get('role_view_modes') === NULL) {
$config->set('role_view_modes', [])->save();
}
return t("View mode 'restricted' e campo field_user_selected_view_mode criados.");
}
/** /**
* Corrige mapeamentos LDAP com campos de string nulos na config ativa. * Corrige mapeamentos LDAP com campos de string nulos na config ativa.
*/ */

View File

@@ -28,6 +28,10 @@ function site_users_theme($existing, $type, $theme, $path) {
'template' => 'user--full', 'template' => 'user--full',
'base hook' => 'user', 'base hook' => 'user',
], ],
'user__restricted' => [
'template' => 'user--restricted',
'base hook' => 'user',
],
'site_users_info_block' => [ 'site_users_info_block' => [
'template' => 'site-user-info-block', 'template' => 'site-user-info-block',
'variables' => [ 'variables' => [
@@ -184,6 +188,131 @@ function site_users_form_user_form_alter(&$form, FormStateInterface $form_state,
$form['#validate'][] = 'site_users_validate_photos_count'; $form['#validate'][] = 'site_users_validate_photos_count';
_site_users_add_default_photo_selector($form, $form_state); _site_users_add_default_photo_selector($form, $form_state);
} }
_site_users_add_visibility_selector($form, $form_state);
}
/**
* Adiciona o seletor de visibilidade do perfil ao formulário de usuário.
*/
function _site_users_add_visibility_selector(&$form, FormStateInterface $form_state) {
/** @var \Drupal\user\UserInterface $user */
$user = $form_state->getFormObject()->getEntity();
$current_user = \Drupal::currentUser();
$is_own = (int) $user->id() === (int) $current_user->id();
$is_admin = $current_user->hasPermission('administer users');
if (!$is_own && !$is_admin) {
return;
}
$options = _site_users_get_allowed_view_modes($user);
// Always include 'restricted'.
$options = ['restricted' => t('Restricted (username only)')] + $options;
$current_value = 'restricted';
if ($user->hasField('field_user_selected_view_mode') && !$user->get('field_user_selected_view_mode')->isEmpty()) {
$current_value = $user->get('field_user_selected_view_mode')->value;
}
// Fallback to restricted if stored value is no longer allowed.
if (!isset($options[$current_value])) {
$current_value = 'restricted';
}
$form['profile_visibility'] = [
'#type' => 'fieldset',
'#title' => t('Profile visibility'),
'#weight' => 20,
];
$form['profile_visibility']['profile_view_mode'] = [
'#type' => 'radios',
'#title' => t('How should my profile appear to other visitors?'),
'#description' => t('Choose the information displayed on your public profile page.'),
'#options' => $options,
'#default_value' => $current_value,
];
// Prepend validate handler.
array_unshift($form['#validate'], '_site_users_validate_visibility_selector');
// Prepend submit handler before the default submit.
array_unshift($form['actions']['submit']['#submit'], '_site_users_presave_visibility_selector');
}
/**
* Retorna os view modes permitidos para os roles do usuário.
*
* @param \Drupal\user\UserInterface $user
* O usuário cujo perfil está sendo editado.
*
* @return array
* Associative array of machine_name => label.
*/
function _site_users_get_allowed_view_modes(UserInterface $user): array {
$config = \Drupal::config('site_users.settings');
$role_view_modes = $config->get('role_view_modes') ?? [];
$allowed = [];
foreach ($user->getRoles() as $role_id) {
if (!empty($role_view_modes[$role_id]) && is_array($role_view_modes[$role_id])) {
foreach ($role_view_modes[$role_id] as $vm) {
$allowed[$vm] = $vm;
}
}
}
if (empty($allowed)) {
return [];
}
// Load labels for the allowed view modes.
$view_mode_storage = \Drupal::entityTypeManager()->getStorage('entity_view_mode');
$options = [];
foreach (array_keys($allowed) as $machine_name) {
$view_mode = $view_mode_storage->load('user.' . $machine_name);
$options[$machine_name] = $view_mode ? $view_mode->label() : $machine_name;
}
return $options;
}
/**
* Validate handler: confirma que o valor escolhido está na lista permitida.
*/
function _site_users_validate_visibility_selector(&$form, FormStateInterface $form_state) {
$chosen = $form_state->getValue('profile_view_mode');
if ($chosen === NULL) {
return;
}
/** @var \Drupal\user\UserInterface $user */
$user = $form_state->getFormObject()->getEntity();
$allowed = _site_users_get_allowed_view_modes($user);
$allowed['restricted'] = 'restricted';
if (!isset($allowed[$chosen])) {
$form_state->setErrorByName('profile_view_mode', t('Invalid profile visibility option selected.'));
}
}
/**
* Submit handler (prepended): seta field_user_selected_view_mode no entity antes do save.
*/
function _site_users_presave_visibility_selector(&$form, FormStateInterface $form_state) {
$chosen = $form_state->getValue('profile_view_mode');
if ($chosen === NULL) {
return;
}
/** @var \Drupal\user\UserInterface $user */
$user = $form_state->getFormObject()->getEntity();
if ($user->hasField('field_user_selected_view_mode')) {
$user->set('field_user_selected_view_mode', $chosen);
// Do not save here — the standard submit handler saves the entity.
}
} }
/** /**
@@ -338,9 +467,31 @@ function site_users_entity_view_mode_alter(string &$view_mode, EntityInterface $
$current_user = \Drupal::currentUser(); $current_user = \Drupal::currentUser();
$is_own = (int) $entity->id() === (int) $current_user->id(); $is_own = (int) $entity->id() === (int) $current_user->id();
$is_admin = $current_user->hasPermission('administer users'); $is_admin = $current_user->hasPermission('administer users');
if (!$is_own && !$is_admin) {
$view_mode = 'public'; if ($is_own || $is_admin) {
// Owner and admin always see the full view mode.
return;
} }
// Read the chosen view mode from the profile being visited.
$chosen = 'restricted';
if ($entity instanceof UserInterface
&& $entity->hasField('field_user_selected_view_mode')
&& !$entity->get('field_user_selected_view_mode')->isEmpty()
) {
$chosen = $entity->get('field_user_selected_view_mode')->value;
}
// Validate that a view display exists for the chosen value.
if ($chosen !== 'restricted') {
$display_id = 'user.user.' . $chosen;
$display_storage = \Drupal::entityTypeManager()->getStorage('entity_view_display');
if (!$display_storage->load($display_id)) {
$chosen = 'restricted';
}
}
$view_mode = $chosen;
} }
/** /**

View File

@@ -145,9 +145,62 @@ class SiteUsersSettingsForm extends ConfigFormBase {
]; ];
} }
// Fieldset para view modes por role.
$form['role_view_modes'] = [
'#type' => 'fieldset',
'#title' => $this->t('Profile visibility options by role'),
'#description' => $this->t('Select which view modes each role can choose for their public profile. The "restricted" option is always available to all users.'),
'#tree' => TRUE,
];
$available_view_modes = $this->getCustomUserViewModes();
if (empty($available_view_modes)) {
$form['role_view_modes']['_empty'] = [
'#markup' => '<p>' . $this->t('No custom view modes found for user entities. Create view modes at <a href=":url">Manage display</a>.', [
':url' => '/admin/config/people/accounts/display',
]) . '</p>',
];
}
else {
$roles = \Drupal\user\Entity\Role::loadMultiple();
foreach ($roles as $role_id => $role) {
if (in_array($role_id, ['anonymous', 'authenticated'])) {
continue;
}
$form['role_view_modes'][$role_id] = [
'#type' => 'checkboxes',
'#title' => $role->label(),
'#options' => $available_view_modes,
'#default_value' => $config->get('role_view_modes.' . $role_id) ?? [],
];
}
}
return parent::buildForm($form, $form_state); return parent::buildForm($form, $form_state);
} }
/**
* Returns custom user view modes (excluding 'default' and 'restricted').
*
* @return array
* Associative array of machine_name => label.
*/
protected function getCustomUserViewModes(): array {
$view_mode_storage = \Drupal::entityTypeManager()->getStorage('entity_view_mode');
$view_modes = $view_mode_storage->loadByProperties(['targetEntityType' => 'user']);
$options = [];
foreach ($view_modes as $view_mode) {
$id_parts = explode('.', $view_mode->id());
$machine_name = end($id_parts);
if ($machine_name === 'default' || $machine_name === 'restricted') {
continue;
}
$options[$machine_name] = $view_mode->label();
}
return $options;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@@ -177,6 +230,17 @@ class SiteUsersSettingsForm extends ConfigFormBase {
} }
} }
// Salvar role_view_modes: apenas os valores marcados (filtrar 0).
$role_view_modes_raw = $form_state->getValue('role_view_modes') ?? [];
$roles = \Drupal\user\Entity\Role::loadMultiple();
foreach ($roles as $role_id => $role) {
if ($role_id === 'anonymous' || !isset($role_view_modes_raw[$role_id])) {
continue;
}
$selected = array_values(array_filter($role_view_modes_raw[$role_id]));
$config->set('role_view_modes.' . $role_id, $selected);
}
$config->save(); $config->save();
parent::submitForm($form, $form_state); parent::submitForm($form, $form_state);

View File

@@ -0,0 +1,12 @@
{#
/**
* @file
* Template for restricted user profile view mode.
*
* Shows only the display name and a message that the profile is not public.
*/
#}
<article{{ attributes.addClass('user-profile', 'user-profile--restricted') }}>
<h1>{{ user.displayname }}</h1>
<p>{{ 'This user has not made their profile public.'|t }}</p>
</article>

View File

@@ -138,6 +138,36 @@ msgstr "Selecione quais campos os usuários podem editar no próprio perfil. Cam
msgid "Protected by another module." msgid "Protected by another module."
msgstr "Protegido por outro módulo." msgstr "Protegido por outro módulo."
# Settings form - role view modes
msgid "Profile visibility options by role"
msgstr "Opções de visibilidade do perfil por papel"
msgid "Select which view modes each role can choose for their public profile. The \"restricted\" option is always available to all users."
msgstr "Selecione quais modos de exibição cada papel pode escolher para o seu perfil público. A opção \"restrito\" está sempre disponível para todos os usuários."
msgid "No custom view modes found for user entities. Create view modes at <a href=\":url\">Manage display</a>."
msgstr "Nenhum modo de exibição customizado encontrado para usuários. Crie modos de exibição em <a href=\":url\">Gerenciar exibição</a>."
# Form elements - visibility selector (site_users.module)
msgid "Profile visibility"
msgstr "Visibilidade do perfil"
msgid "How should my profile appear to other visitors?"
msgstr "Como meu perfil deve aparecer para outros visitantes?"
msgid "Choose the information displayed on your public profile page."
msgstr "Escolha as informações exibidas na página pública do seu perfil."
msgid "Restricted (username only)"
msgstr "Restrito (somente nome de usuário)"
msgid "Invalid profile visibility option selected."
msgstr "Opção de visibilidade de perfil inválida."
# Template - restricted profile
msgid "This user has not made their profile public."
msgstr "Este usuário não tornou seu perfil público."
# Template # Template
msgid "Phone:" msgid "Phone:"
msgstr "Telefone:" msgstr "Telefone:"