feat: Módulo Site Users para customização de perfis de usuário

Módulo Drupal para gerenciamento de campos e fotos de perfil de usuários:
- Campos customizados: nome, telefone, categoria, departamento, biografia
- Suporte a múltiplas fotos com seleção de foto padrão
- Controle de permissões granular para visualização e edição
- Bloco de informações do usuário para exibição em páginas
- Configurações administrativas para limite de fotos e integração LDAP

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-04 07:25:51 -03:00
commit 6215759045
28 changed files with 1554 additions and 0 deletions

141
README.md Normal file
View File

@@ -0,0 +1,141 @@
# Site Users
Módulo Drupal para customização de perfis de usuário, incluindo campos adicionais e gerenciamento de fotos.
## Funcionalidades
- **Campos de perfil customizados:**
- Nome completo
- Telefone
- Categoria
- Código do departamento
- Biografia
- **Gerenciamento de fotos:**
- Suporte a múltiplas fotos por usuário
- Seleção de foto padrão
- Limite configurável de quantidade de fotos
- Integração com Media Library
- **Controle de permissões granular:**
- Visualizar/editar campos do próprio perfil
- Visualizar/editar campos de qualquer usuário
- Gerenciar fotos próprias ou de outros usuários
- **Bloco de informações:**
- Exibe dados do usuário em páginas de perfil
- Template customizável via Twig
## Requisitos
- Drupal 10 ou 11
- Módulos core: User, Telephone, Text, Media, Media Library
## Instalação
1. Copie o módulo para `modules/custom/site_users`
2. Ative o módulo via Drush ou interface administrativa:
```bash
drush en site_users
```
## Configuração
Acesse as configurações em:
**Administração > Configuração > Módulos Locais > Site Users**
`/admin/config/local-modules/site-users`
### Opções disponíveis
| Configuração | Descrição | Padrão |
|--------------|-----------|--------|
| Quantidade de fotos | Número máximo de fotos por usuário | 5 |
| Atributo LDAP | Nome do atributo LDAP para foto (se aplicável) | jpegPhoto |
## Permissões
| Permissão | Descrição |
|-----------|-----------|
| `administer site_users settings` | Administrar configurações do módulo |
| `view own user profile fields` | Visualizar campos do próprio perfil |
| `view any user profile fields` | Visualizar campos de qualquer usuário |
| `edit own user profile fields` | Editar campos do próprio perfil |
| `edit any user profile fields` | Editar campos de qualquer usuário |
| `manage own user photos` | Gerenciar próprias fotos |
| `manage user photos` | Gerenciar fotos de qualquer usuário |
## Uso do Bloco
O módulo fornece o bloco **"Informações do Usuário"** que pode ser posicionado em regiões do tema para exibir informações do perfil nas páginas `/user/{id}`.
Para adicionar o bloco:
1. Acesse **Estrutura > Layout de blocos**
2. Adicione o bloco "Informações do Usuário" na região desejada
3. Configure a visibilidade para páginas de usuário
## Customização de Templates
Os templates podem ser sobrescritos no tema:
- `site-user-info-block.html.twig` - Bloco de informações do usuário
- `user--full.html.twig` - Página completa do usuário
### Variáveis disponíveis no bloco
```twig
{{ user_info.uid }} {# ID do usuário #}
{{ user_info.username }} {# Nome de exibição #}
{{ user_info.name }} {# Nome completo #}
{{ user_info.phone }} {# Telefone #}
{{ user_info.category }} {# Categoria #}
{{ user_info.dept_code }} {# Código do departamento #}
{{ user_info.bio }} {# Biografia #}
{{ user_info.photo_url }} {# URL da foto padrão #}
{{ user_info.photo_alt }} {# Texto alternativo da foto #}
{{ user }} {# Entidade completa do usuário #}
```
## API
### Obter foto padrão de um usuário
```php
$photo = site_users_get_default_photo($user);
if ($photo instanceof \Drupal\media\MediaInterface) {
// Usar a foto
}
```
## Estrutura do Módulo
```
site_users/
├── config/
│ ├── install/
│ │ └── site_users.settings.yml
│ └── optional/
│ └── field.*.yml
├── css/
│ └── site-user-info-block.css
├── src/
│ ├── Form/
│ │ └── SiteUsersSettingsForm.php
│ └── Plugin/Block/
│ └── UserInfoBlock.php
├── templates/
│ ├── site-user-info-block.html.twig
│ └── user--full.html.twig
├── site_users.info.yml
├── site_users.install
├── site_users.libraries.yml
├── site_users.links.menu.yml
├── site_users.module
├── site_users.permissions.yml
└── site_users.routing.yml
```
## Licença
Este módulo é software livre distribuído sob a licença GPL-2.0-or-later.

View File

@@ -0,0 +1,3 @@
photos:
max_count: 5
ldap_attribute: 'jpegPhoto'

View File

@@ -0,0 +1,20 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.user.field_user_bio
module:
- text
- user
id: user.user.field_user_bio
field_name: field_user_bio
entity_type: user
bundle: user
label: Biografia
description: 'Uma breve descrição sobre o usuário.'
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: text_long

View File

@@ -0,0 +1,19 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.user.field_user_category
module:
- user
id: user.user.field_user_category
field_name: field_user_category
entity_type: user
bundle: user
label: Categoria
description: 'Categoria do usuário.'
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: string

View File

@@ -0,0 +1,30 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.user.field_user_default_photo
- media.type.image
module:
- media
- user
id: user.user.field_user_default_photo
field_name: field_user_default_photo
entity_type: user
bundle: user
label: Foto Padrão
description: 'Selecione a foto principal do perfil.'
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings:
handler: 'default:media'
handler_settings:
target_bundles:
image: image
sort:
field: _none
direction: ASC
auto_create: false
auto_create_bundle: ''
field_type: entity_reference

View File

@@ -0,0 +1,19 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.user.field_user_dept_code
module:
- user
id: user.user.field_user_dept_code
field_name: field_user_dept_code
entity_type: user
bundle: user
label: Código do Departamento
description: 'Código do departamento do usuário.'
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: string

View File

@@ -0,0 +1,19 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.user.field_user_name
module:
- user
id: user.user.field_user_name
field_name: field_user_name
entity_type: user
bundle: user
label: Nome
description: 'Nome completo do usuário.'
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: string

View File

@@ -0,0 +1,20 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.user.field_user_phone
module:
- telephone
- user
id: user.user.field_user_phone
field_name: field_user_phone
entity_type: user
bundle: user
label: Telefone
description: 'Número de telefone do usuário.'
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: telephone

View File

@@ -0,0 +1,30 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.user.field_user_photos
- media.type.image
module:
- media
- user
id: user.user.field_user_photos
field_name: field_user_photos
entity_type: user
bundle: user
label: Fotos
description: 'Fotos do usuário.'
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings:
handler: 'default:media'
handler_settings:
target_bundles:
image: image
sort:
field: _none
direction: ASC
auto_create: false
auto_create_bundle: ''
field_type: entity_reference

View File

@@ -0,0 +1,18 @@
langcode: pt-br
status: true
dependencies:
module:
- text
- user
id: user.field_user_bio
field_name: field_user_bio
entity_type: user
type: text_long
settings: { }
module: text
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

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

View File

@@ -0,0 +1,19 @@
langcode: pt-br
status: true
dependencies:
module:
- media
- user
id: user.field_user_default_photo
field_name: field_user_default_photo
entity_type: user
type: entity_reference
settings:
target_type: media
module: core
locked: false
cardinality: 1
translatable: false
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
langcode: pt-br
status: true
dependencies:
module:
- telephone
- user
id: user.field_user_phone
field_name: field_user_phone
entity_type: user
type: telephone
settings: { }
module: telephone
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,19 @@
langcode: pt-br
status: true
dependencies:
module:
- media
- user
id: user.field_user_photos
field_name: field_user_photos
entity_type: user
type: entity_reference
settings:
target_type: media
module: core
locked: false
cardinality: -1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,97 @@
/**
* @file
* Estilos para o bloco de informações do usuário.
*/
.site-user-info-block {
display: flex;
gap: 1.5rem;
padding: 1.5rem;
background-color: #f8f9fa;
border-radius: 8px;
margin-bottom: 1.5rem;
}
.site-user-info-block__photo {
flex-shrink: 0;
}
.site-user-info-block__image {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
}
.site-user-info-block__no-photo {
width: 120px;
height: 120px;
border-radius: 50%;
background-color: #6c757d;
display: flex;
align-items: center;
justify-content: center;
}
.site-user-info-block__initials {
font-size: 3rem;
font-weight: bold;
color: #fff;
}
.site-user-info-block__details {
flex-grow: 1;
}
.site-user-info-block__name {
margin: 0 0 0.5rem 0;
font-size: 1.5rem;
font-weight: 600;
color: #212529;
}
.site-user-info-block__category {
font-size: 1.1rem;
color: #495057;
margin-bottom: 0.75rem;
}
.site-user-info-block__dept,
.site-user-info-block__phone {
font-size: 0.95rem;
color: #6c757d;
margin-bottom: 0.25rem;
}
.site-user-info-block__label {
font-weight: 500;
}
.site-user-info-block__phone a {
color: #0d6efd;
text-decoration: none;
}
.site-user-info-block__phone a:hover {
text-decoration: underline;
}
.site-user-info-block__bio {
margin-top: 1rem;
font-size: 0.95rem;
color: #495057;
line-height: 1.5;
}
/* Responsivo */
@media (max-width: 576px) {
.site-user-info-block {
flex-direction: column;
align-items: center;
text-align: center;
}
.site-user-info-block__details {
width: 100%;
}
}

11
site_users.info.yml Normal file
View File

@@ -0,0 +1,11 @@
name: Site Users
type: module
description: 'Customizações de usuários do site, incluindo campos e templates.'
core_version_requirement: ^10 || ^11
package: Custom
dependencies:
- drupal:user
- drupal:telephone
- drupal:text
- drupal:media
- drupal:media_library

266
site_users.install Normal file
View File

@@ -0,0 +1,266 @@
<?php
/**
* @file
* Install, update and uninstall functions for the Site Users module.
*/
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Implements hook_install().
*/
function site_users_install() {
// Configurar form display para os campos de usuário.
$form_display = EntityFormDisplay::load('user.user.default');
if ($form_display) {
// Campo Nome.
if (!$form_display->getComponent('field_user_name')) {
$form_display->setComponent('field_user_name', [
'type' => 'string_textfield',
'weight' => 10,
'settings' => [
'size' => 60,
'placeholder' => '',
],
'region' => 'content',
]);
}
// Campo Telefone.
if (!$form_display->getComponent('field_user_phone')) {
$form_display->setComponent('field_user_phone', [
'type' => 'telephone_default',
'weight' => 11,
'settings' => [
'placeholder' => '',
],
'region' => 'content',
]);
}
// Campo Categoria.
if (!$form_display->getComponent('field_user_category')) {
$form_display->setComponent('field_user_category', [
'type' => 'string_textfield',
'weight' => 12,
'settings' => [
'size' => 60,
'placeholder' => '',
],
'region' => 'content',
]);
}
// Campo Código do Departamento.
if (!$form_display->getComponent('field_user_dept_code')) {
$form_display->setComponent('field_user_dept_code', [
'type' => 'string_textfield',
'weight' => 13,
'settings' => [
'size' => 60,
'placeholder' => '',
],
'region' => 'content',
]);
}
// Campo Fotos - Media Library widget.
if (!$form_display->getComponent('field_user_photos')) {
$form_display->setComponent('field_user_photos', [
'type' => 'media_library_widget',
'weight' => 14,
'settings' => [
'media_types' => [],
],
'region' => 'content',
]);
}
// Campo Biografia.
if (!$form_display->getComponent('field_user_bio')) {
$form_display->setComponent('field_user_bio', [
'type' => 'text_textarea',
'weight' => 15,
'settings' => [
'rows' => 5,
'placeholder' => '',
],
'region' => 'content',
]);
}
// Campo Foto Padrão - oculto no form (será gerenciado via hook_form_alter).
$form_display->removeComponent('field_user_default_photo');
$form_display->save();
}
// Configurar view display para os campos de usuário.
$view_display = EntityViewDisplay::load('user.user.default');
if ($view_display) {
// Campo Nome.
if (!$view_display->getComponent('field_user_name')) {
$view_display->setComponent('field_user_name', [
'type' => 'string',
'weight' => 10,
'label' => 'above',
'settings' => [
'link_to_entity' => FALSE,
],
'region' => 'content',
]);
}
// Campo Telefone.
if (!$view_display->getComponent('field_user_phone')) {
$view_display->setComponent('field_user_phone', [
'type' => 'telephone_link',
'weight' => 11,
'label' => 'above',
'settings' => [
'title' => '',
],
'region' => 'content',
]);
}
// Campo Categoria.
if (!$view_display->getComponent('field_user_category')) {
$view_display->setComponent('field_user_category', [
'type' => 'string',
'weight' => 12,
'label' => 'above',
'settings' => [
'link_to_entity' => FALSE,
],
'region' => 'content',
]);
}
// Campo Código do Departamento.
if (!$view_display->getComponent('field_user_dept_code')) {
$view_display->setComponent('field_user_dept_code', [
'type' => 'string',
'weight' => 13,
'label' => 'above',
'settings' => [
'link_to_entity' => FALSE,
],
'region' => 'content',
]);
}
// Campo Fotos.
if (!$view_display->getComponent('field_user_photos')) {
$view_display->setComponent('field_user_photos', [
'type' => 'entity_reference_entity_view',
'weight' => 14,
'label' => 'above',
'settings' => [
'view_mode' => 'default',
'link' => FALSE,
],
'region' => 'content',
]);
}
// Campo Biografia.
if (!$view_display->getComponent('field_user_bio')) {
$view_display->setComponent('field_user_bio', [
'type' => 'text_default',
'weight' => 15,
'label' => 'above',
'settings' => [],
'region' => 'content',
]);
}
// Campo Foto Padrão.
if (!$view_display->getComponent('field_user_default_photo')) {
$view_display->setComponent('field_user_default_photo', [
'type' => 'entity_reference_entity_view',
'weight' => 5,
'label' => 'hidden',
'settings' => [
'view_mode' => 'default',
'link' => FALSE,
],
'region' => 'content',
]);
}
$view_display->save();
}
}
/**
* Adiciona o campo field_user_default_photo para seleção de foto padrão.
*/
function site_users_update_10001() {
// Criar field storage se não existir.
if (!FieldStorageConfig::loadByName('user', 'field_user_default_photo')) {
FieldStorageConfig::create([
'field_name' => 'field_user_default_photo',
'entity_type' => 'user',
'type' => 'entity_reference',
'settings' => [
'target_type' => 'media',
],
'cardinality' => 1,
'translatable' => FALSE,
])->save();
}
// Criar field instance se não existir.
if (!FieldConfig::loadByName('user', 'user', 'field_user_default_photo')) {
FieldConfig::create([
'field_name' => 'field_user_default_photo',
'entity_type' => 'user',
'bundle' => 'user',
'label' => 'Foto Padrão',
'description' => 'Selecione a foto principal do perfil.',
'required' => FALSE,
'settings' => [
'handler' => 'default:media',
'handler_settings' => [
'target_bundles' => [
'image' => 'image',
],
'sort' => [
'field' => '_none',
'direction' => 'ASC',
],
'auto_create' => FALSE,
'auto_create_bundle' => '',
],
],
])->save();
}
// Configurar view display.
$view_display = EntityViewDisplay::load('user.user.default');
if ($view_display && !$view_display->getComponent('field_user_default_photo')) {
$view_display->setComponent('field_user_default_photo', [
'type' => 'entity_reference_entity_view',
'weight' => 5,
'label' => 'hidden',
'settings' => [
'view_mode' => 'default',
'link' => FALSE,
],
'region' => 'content',
])->save();
}
// Ocultar do form display (será gerenciado via hook_form_alter).
$form_display = EntityFormDisplay::load('user.user.default');
if ($form_display) {
$form_display->removeComponent('field_user_default_photo')->save();
}
return t('Campo de foto padrão criado com sucesso.');
}

5
site_users.libraries.yml Normal file
View File

@@ -0,0 +1,5 @@
user-info-block:
version: 1.x
css:
component:
css/site-user-info-block.css: {}

View File

@@ -0,0 +1,6 @@
site_users.settings:
title: 'Site Users'
description: 'Configurações de campos e fotos de usuários.'
route_name: site_users.settings
parent: site_tools.admin_config
weight: 10

350
site_users.module Normal file
View File

@@ -0,0 +1,350 @@
<?php
/**
* @file
* Módulo Site Users - customizações de usuários do site.
*/
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\media\MediaInterface;
use Drupal\user\UserInterface;
/**
* Implements hook_theme().
*/
function site_users_theme($existing, $type, $theme, $path) {
return [
'user__full' => [
'template' => 'user--full',
'base hook' => 'user',
],
'site_users_info_block' => [
'template' => 'site-user-info-block',
'variables' => [
'user_info' => [],
'user' => NULL,
],
],
];
}
/**
* Implements hook_theme_suggestions_HOOK_alter() for user templates.
*/
function site_users_theme_suggestions_user_alter(array &$suggestions, array $variables) {
$view_mode = $variables['elements']['#view_mode'] ?? 'default';
$suggestions[] = 'user__' . $view_mode;
}
/**
* Implements hook_entity_field_access().
*/
function site_users_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
// Apenas para entidade user.
if ($field_definition->getTargetEntityTypeId() !== 'user') {
return AccessResult::neutral();
}
// Lista de campos controlados pelo módulo.
$profile_fields = [
'field_user_name',
'field_user_phone',
'field_user_category',
'field_user_dept_code',
'field_user_bio',
];
$photo_fields = [
'field_user_photos',
'field_user_default_photo',
];
$field_name = $field_definition->getName();
// Verificar se é um campo de perfil.
if (in_array($field_name, $profile_fields)) {
return site_users_check_profile_field_access($operation, $account, $items);
}
// Verificar se é um campo de fotos.
if (in_array($field_name, $photo_fields)) {
return site_users_check_photo_field_access($operation, $account, $items);
}
return AccessResult::neutral();
}
/**
* Verifica acesso aos campos de perfil.
*/
function site_users_check_profile_field_access($operation, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
// Administradores têm acesso total.
if ($account->hasPermission('administer site_users settings')) {
return AccessResult::allowed()->cachePerPermissions();
}
// Determinar se é o próprio usuário.
$is_own = FALSE;
if ($items) {
$entity = $items->getEntity();
$is_own = ($entity->id() == $account->id());
}
if ($operation === 'view') {
// Pode ver qualquer perfil.
if ($account->hasPermission('view any user profile fields')) {
return AccessResult::allowed()->cachePerPermissions();
}
// Pode ver apenas o próprio perfil.
if ($is_own && $account->hasPermission('view own user profile fields')) {
return AccessResult::allowed()->cachePerPermissions()->cachePerUser();
}
return AccessResult::forbidden()->cachePerPermissions()->cachePerUser();
}
if ($operation === 'edit') {
// Pode editar qualquer perfil.
if ($account->hasPermission('edit any user profile fields')) {
return AccessResult::allowed()->cachePerPermissions();
}
// Pode editar apenas o próprio perfil.
if ($is_own && $account->hasPermission('edit own user profile fields')) {
return AccessResult::allowed()->cachePerPermissions()->cachePerUser();
}
return AccessResult::forbidden()->cachePerPermissions()->cachePerUser();
}
return AccessResult::neutral();
}
/**
* Verifica acesso ao campo de fotos.
*/
function site_users_check_photo_field_access($operation, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
// Administradores têm acesso total.
if ($account->hasPermission('administer site_users settings')) {
return AccessResult::allowed()->cachePerPermissions();
}
// Determinar se é o próprio usuário.
$is_own = FALSE;
if ($items) {
$entity = $items->getEntity();
$is_own = ($entity->id() == $account->id());
}
if ($operation === 'view') {
// Fotos seguem a mesma regra dos campos de perfil para visualização.
if ($account->hasPermission('view any user profile fields')) {
return AccessResult::allowed()->cachePerPermissions();
}
if ($is_own && $account->hasPermission('view own user profile fields')) {
return AccessResult::allowed()->cachePerPermissions()->cachePerUser();
}
return AccessResult::forbidden()->cachePerPermissions()->cachePerUser();
}
if ($operation === 'edit') {
// Pode gerenciar fotos de qualquer usuário.
if ($account->hasPermission('manage user photos')) {
return AccessResult::allowed()->cachePerPermissions();
}
// Pode gerenciar apenas as próprias fotos.
if ($is_own && $account->hasPermission('manage own user photos')) {
return AccessResult::allowed()->cachePerPermissions()->cachePerUser();
}
return AccessResult::forbidden()->cachePerPermissions()->cachePerUser();
}
return AccessResult::neutral();
}
/**
* Implements hook_form_FORM_ID_alter() for user_form.
*/
function site_users_form_user_form_alter(&$form, FormStateInterface $form_state, $form_id) {
if (isset($form['field_user_photos'])) {
$form['#validate'][] = 'site_users_validate_photos_count';
_site_users_add_default_photo_selector($form, $form_state);
}
}
/**
* Implements hook_form_FORM_ID_alter() for user_register_form.
*/
function site_users_form_user_register_form_alter(&$form, FormStateInterface $form_state, $form_id) {
if (isset($form['field_user_photos'])) {
$form['#validate'][] = 'site_users_validate_photos_count';
_site_users_add_default_photo_selector($form, $form_state);
}
}
/**
* Adiciona o seletor de foto padrão ao formulário.
*/
function _site_users_add_default_photo_selector(&$form, FormStateInterface $form_state) {
/** @var \Drupal\user\UserInterface $user */
$user = $form_state->getFormObject()->getEntity();
// Obter fotos atuais do usuário.
$photos = [];
if ($user->hasField('field_user_photos') && !$user->get('field_user_photos')->isEmpty()) {
foreach ($user->get('field_user_photos')->referencedEntities() as $media) {
if ($media instanceof MediaInterface) {
$photos[$media->id()] = $media->label();
}
}
}
// Obter foto padrão atual.
$default_photo_id = NULL;
if ($user->hasField('field_user_default_photo') && !$user->get('field_user_default_photo')->isEmpty()) {
$default_photo_id = $user->get('field_user_default_photo')->target_id;
}
// Adicionar seletor de foto padrão após o campo de fotos.
$form['default_photo_selector'] = [
'#type' => 'container',
'#weight' => $form['field_user_photos']['#weight'] + 0.5 ?? 15,
'#states' => [
'visible' => [
':input[name="field_user_photos[selection]"]' => ['filled' => TRUE],
],
],
];
if (!empty($photos)) {
$form['default_photo_selector']['field_user_default_photo_select'] = [
'#type' => 'radios',
'#title' => t('Foto padrão'),
'#description' => t('Selecione a foto principal do perfil.'),
'#options' => $photos,
'#default_value' => $default_photo_id,
];
}
else {
$form['default_photo_selector']['field_user_default_photo_select'] = [
'#type' => 'item',
'#title' => t('Foto padrão'),
'#markup' => t('Adicione fotos para selecionar uma como padrão.'),
];
}
// Adicionar submit handler para salvar a foto padrão.
$form['actions']['submit']['#submit'][] = '_site_users_save_default_photo';
}
/**
* Submit handler para salvar a foto padrão selecionada.
*/
function _site_users_save_default_photo(&$form, FormStateInterface $form_state) {
$selected_photo = $form_state->getValue('field_user_default_photo_select');
/** @var \Drupal\user\UserInterface $user */
$user = $form_state->getFormObject()->getEntity();
if ($user->hasField('field_user_default_photo')) {
$user->set('field_user_default_photo', $selected_photo);
$user->save();
}
}
/**
* Validação customizada para quantidade máxima de fotos.
*/
function site_users_validate_photos_count(&$form, FormStateInterface $form_state) {
$photos = $form_state->getValue('field_user_photos');
if (empty($photos)) {
return;
}
// Contar fotos selecionadas (ignorar valores vazios).
$count = 0;
foreach ($photos as $delta => $photo) {
if (is_numeric($delta) && !empty($photo['target_id'])) {
$count++;
}
}
// Obter limite configurado.
$config = \Drupal::config('site_users.settings');
$max_count = $config->get('photos.max_count') ?? 5;
if ($count > $max_count) {
$form_state->setErrorByName('field_user_photos', t('Você pode adicionar no máximo @max fotos. Atualmente há @count fotos selecionadas.', [
'@max' => $max_count,
'@count' => $count,
]));
}
}
/**
* Implements hook_ENTITY_TYPE_presave() for user entities.
*/
function site_users_user_presave(UserInterface $user) {
// Garantir consistência da foto padrão.
if (!$user->hasField('field_user_photos') || !$user->hasField('field_user_default_photo')) {
return;
}
// Obter IDs das fotos atuais.
$photo_ids = [];
foreach ($user->get('field_user_photos')->getValue() as $item) {
if (!empty($item['target_id'])) {
$photo_ids[] = $item['target_id'];
}
}
// Obter foto padrão atual.
$default_photo_id = $user->get('field_user_default_photo')->target_id;
// Se não há fotos, limpar foto padrão.
if (empty($photo_ids)) {
$user->set('field_user_default_photo', NULL);
return;
}
// Se a foto padrão não está entre as fotos, selecionar a primeira.
if (empty($default_photo_id) || !in_array($default_photo_id, $photo_ids)) {
$user->set('field_user_default_photo', $photo_ids[0]);
}
}
/**
* Obtém a foto padrão de um usuário.
*
* @param \Drupal\user\UserInterface $user
* O usuário.
*
* @return \Drupal\media\MediaInterface|null
* A entidade de mídia da foto padrão, ou NULL se não houver.
*/
function site_users_get_default_photo(UserInterface $user): ?MediaInterface {
if (!$user->hasField('field_user_default_photo') || !$user->hasField('field_user_photos')) {
return NULL;
}
// Tentar obter a foto padrão configurada.
if (!$user->get('field_user_default_photo')->isEmpty()) {
$default_photo = $user->get('field_user_default_photo')->entity;
if ($default_photo instanceof MediaInterface) {
return $default_photo;
}
}
// Fallback: retornar a primeira foto disponível.
if (!$user->get('field_user_photos')->isEmpty()) {
$first_photo = $user->get('field_user_photos')->first()->entity;
if ($first_photo instanceof MediaInterface) {
return $first_photo;
}
}
return NULL;
}

View File

@@ -0,0 +1,30 @@
administer site_users settings:
title: 'Administrar configurações do Site Users'
description: 'Permite administrar as configurações do módulo Site Users.'
restrict access: true
view any user profile fields:
title: 'Visualizar campos de perfil de qualquer usuário'
description: 'Permite visualizar os campos customizados (nome, telefone, categoria, etc.) de qualquer usuário.'
view own user profile fields:
title: 'Visualizar campos do próprio perfil'
description: 'Permite visualizar os próprios campos customizados de perfil.'
edit any user profile fields:
title: 'Editar campos de perfil de qualquer usuário'
description: 'Permite editar os campos customizados de qualquer usuário.'
restrict access: true
edit own user profile fields:
title: 'Editar campos do próprio perfil'
description: 'Permite editar os próprios campos customizados de perfil.'
manage user photos:
title: 'Gerenciar fotos de usuários'
description: 'Permite adicionar, editar e remover fotos de qualquer usuário.'
restrict access: true
manage own user photos:
title: 'Gerenciar próprias fotos'
description: 'Permite adicionar, editar e remover as próprias fotos de perfil.'

7
site_users.routing.yml Normal file
View File

@@ -0,0 +1,7 @@
site_users.settings:
path: '/admin/config/local-modules/site-users'
defaults:
_form: '\Drupal\site_users\Form\SiteUsersSettingsForm'
_title: 'Configurações do Site Users'
requirements:
_permission: 'administer site_users settings'

View File

@@ -0,0 +1,73 @@
<?php
namespace Drupal\site_users\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Formulário de configuração do módulo Site Users.
*/
class SiteUsersSettingsForm extends ConfigFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'site_users_settings_form';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['site_users.settings'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('site_users.settings');
// Fieldset para configurações de fotos.
$form['photos'] = [
'#type' => 'fieldset',
'#title' => $this->t('Configurações de Fotos'),
'#collapsible' => FALSE,
];
$form['photos']['photos_max_count'] = [
'#type' => 'number',
'#title' => $this->t('Quantidade de fotos permitidas'),
'#description' => $this->t('Número máximo de fotos que um usuário pode adicionar ao perfil.'),
'#default_value' => $config->get('photos.max_count') ?? 5,
'#min' => 1,
'#max' => 100,
'#required' => TRUE,
];
$form['photos']['photos_ldap_attribute'] = [
'#type' => 'textfield',
'#title' => $this->t('Atributo LDAP da foto'),
'#description' => $this->t('Se LDAP estiver habilitado, informe o nome do atributo que contém a foto do usuário (ex: thumbnailPhoto, jpegPhoto).'),
'#default_value' => $config->get('photos.ldap_attribute') ?? 'jpegPhoto',
'#maxlength' => 255,
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('site_users.settings')
->set('photos.max_count', $form_state->getValue('photos_max_count'))
->set('photos.ldap_attribute', $form_state->getValue('photos_ldap_attribute'))
->save();
parent::submitForm($form, $form_state);
}
}

View File

@@ -0,0 +1,192 @@
<?php
namespace Drupal\site_users\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\user\UserInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Bloco com informações básicas do usuário.
*
* Exibe automaticamente as informações do usuário da rota /user/{id}.
*
* @Block(
* id = "site_users_info_block",
* admin_label = @Translation("Informações do Usuário"),
* category = @Translation("Site Users")
* )
*/
class UserInfoBlock extends BlockBase implements ContainerFactoryPluginInterface {
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new UserInfoBlock instance.
*
* @param array $configuration
* The plugin configuration.
* @param string $plugin_id
* The plugin ID.
* @param mixed $plugin_definition
* The plugin definition.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
RouteMatchInterface $route_match,
EntityTypeManagerInterface $entity_type_manager
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->routeMatch = $route_match;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('current_route_match'),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function build() {
$user = $this->getUserFromContext();
if (!$user instanceof UserInterface) {
return [];
}
// Obter foto padrão.
$default_photo = NULL;
$default_photo_url = NULL;
if (function_exists('site_users_get_default_photo')) {
$default_photo = site_users_get_default_photo($user);
if ($default_photo) {
// Usar o source field do media (forma padrão do Drupal).
$source_field = $default_photo->getSource()->getConfiguration()['source_field'];
if ($default_photo->hasField($source_field) && !$default_photo->get($source_field)->isEmpty()) {
$file = $default_photo->get($source_field)->entity;
if ($file) {
$default_photo_url = \Drupal::service('file_url_generator')->generateAbsoluteString($file->getFileUri());
}
}
}
}
// Coletar informações do usuário.
$user_info = [
'uid' => $user->id(),
'username' => $user->getDisplayName(),
'name' => $this->getFieldValue($user, 'field_user_name'),
'phone' => $this->getFieldValue($user, 'field_user_phone'),
'category' => $this->getFieldValue($user, 'field_user_category'),
'dept_code' => $this->getFieldValue($user, 'field_user_dept_code'),
'bio' => $this->getFieldValue($user, 'field_user_bio'),
'photo_url' => $default_photo_url,
'photo_alt' => $default_photo ? $default_photo->label() : '',
];
return [
'#theme' => 'site_users_info_block',
'#user_info' => $user_info,
'#user' => $user,
'#attached' => [
'library' => [
'site_users/user-info-block',
],
],
];
}
/**
* Obtém o usuário da rota atual.
*
* @return \Drupal\user\UserInterface|null
* O usuário ou NULL se não estiver em uma página de usuário.
*/
protected function getUserFromContext(): ?UserInterface {
// Obter da rota /user/{user}.
$user = $this->routeMatch->getParameter('user');
if ($user instanceof UserInterface) {
return $user;
}
// Se for apenas o ID, carregar o usuário.
if (is_numeric($user)) {
return $this->entityTypeManager->getStorage('user')->load($user);
}
return NULL;
}
/**
* Obtém o valor de um campo do usuário.
*
* @param \Drupal\user\UserInterface $user
* O usuário.
* @param string $field_name
* O nome do campo.
*
* @return string|null
* O valor do campo ou NULL.
*/
protected function getFieldValue(UserInterface $user, string $field_name): ?string {
if ($user->hasField($field_name) && !$user->get($field_name)->isEmpty()) {
return $user->get($field_name)->value;
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
$tags = parent::getCacheTags();
$user = $this->getUserFromContext();
if ($user instanceof UserInterface) {
$tags = Cache::mergeTags($tags, $user->getCacheTags());
}
return $tags;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return Cache::mergeContexts(parent::getCacheContexts(), ['route', 'user']);
}
}

View File

@@ -0,0 +1,64 @@
{#
/**
* @file
* Template para o bloco de informações do usuário.
*
* Variáveis disponíveis:
* - user_info: Array com informações do usuário:
* - uid: ID do usuário
* - username: Nome de usuário (display name)
* - name: Nome completo
* - phone: Telefone
* - category: Categoria
* - dept_code: Código do departamento
* - bio: Biografia
* - photo_url: URL da foto padrão
* - photo_alt: Texto alternativo da foto
* - user: Entidade do usuário.
*/
#}
<div class="site-user-info-block">
<div class="site-user-info-block__photo">
{% if user_info.photo_url %}
<img src="{{ user_info.photo_url }}" alt="{{ user_info.photo_alt }}" class="site-user-info-block__image" />
{% else %}
<div class="site-user-info-block__no-photo">
<span class="site-user-info-block__initials">
{{ user_info.name ? user_info.name|first|upper : user_info.username|first|upper }}
</span>
</div>
{% endif %}
</div>
<div class="site-user-info-block__details">
<h2 class="site-user-info-block__name">
{{ user_info.name ?: user_info.username }}
</h2>
{% if user_info.category %}
<div class="site-user-info-block__category">
{{ user_info.category }}
</div>
{% endif %}
{% if user_info.dept_code %}
<div class="site-user-info-block__dept">
<span class="site-user-info-block__label">{{ 'Departamento:'|t }}</span>
{{ user_info.dept_code }}
</div>
{% endif %}
{% if user_info.phone %}
<div class="site-user-info-block__phone">
<span class="site-user-info-block__label">{{ 'Telefone:'|t }}</span>
<a href="tel:{{ user_info.phone }}">{{ user_info.phone }}</a>
</div>
{% endif %}
{% if user_info.bio %}
<div class="site-user-info-block__bio">
{{ user_info.bio }}
</div>
{% endif %}
</div>
</div>

View File

@@ -0,0 +1,18 @@
{#
/**
* @file
* Template para página de perfil de usuário (view mode: full).
*
* Variáveis disponíveis:
* - content: Campos do usuário renderizados.
* - user: Objeto do usuário.
* - attributes: Atributos HTML do elemento wrapper.
*
* @see template_preprocess_user()
*/
#}
<article{{ attributes.addClass('user-profile') }}>
{% if content %}
{{ content }}
{% endif %}
</article>